Грокаем C++
9.33K subscribers
44 photos
1 video
3 files
620 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Наследие Cfront. Наследование
#новичкам

Сорри за тавталогию, но мимо этой темы мы не можем пройти(хотя я бы прошел, если бы не @shumilkinad, спасибо ему)

В раннем С++ уже были классы и наследование, но в С этого не было. Как же можно транслировать наследование классов в С'шный код?

Простая агрегация и никакого мошенничества.

Все классы в С++ - это простые С-структуры с отдельным набором функций, принимающих cv-специфицированный указатель на инстанс структуры(это мы выяснили в прошлом посте). Наследники же в начале объекта хранят все поля базового класса. Так давайте просто первым полем наследника сделаем инстанс базовой структуры. Таким образом мы обеспечим вложенность объектов с любым количеством наследований.

Вот есть пара родитель-наследник:

class Base {
public:
int x;
Base(int a) : x(a) {}
};

class Derived : public Base {
public:
int y;
int z;
Derived(int a, int b) : Base(a), y(b) {
z = x + y;
}
};


Примерно в такой код они транслировались:

struct Base {
int x;
};

struct Derived {
struct Base _base; // embedded base object
int y;
int z;
};

void Base_constructor(struct Base* this, int a) {
this->x = a;

// Constructor body (if presented)
}

void Derived_constructor(struct Derived* this, int a, int b) {
// Init base object
Base_constructor(&this->_base, a);

// Init fields of derived object
this->y = b;

// Constructor body
this->z = this->_base.x + this->y;
}


Первым полем структуры Derived - структура Base.

Конструкторы же - это отдельные функции. Но в С++ очень важен порядок вызовов конструкторов и деструкторов.

Поэтому конструктор самого базового класса только инициализирует свои поля и дальше выполняет инструкции из тела конструктора. При создании же наследников в начале вызывается конструктор базового класса и лишь потом инициализация полей и выполнение тела.

Разрушение же объекта выполняется в обратном порядке. Трусы снимаются только после штанов, раньше не получится.

class Base {
public:
~Base() {
// Destructor Base body
}
};

class Derived : public Base {
public:
~Derived() {
// Destructor Derived body
}
}


Превращается в:

struct Base {
};

struct Derived {
struct Base _base;

};

void Base_destructor(struct Base* this) {
// Destructor Base body
}

void Derived_destructor(struct Derived* this) {
// Destructors execute in reverse order

// Destructor Derived body

Base_destructor(&this->_base);
}


Новичкам особенно будет полезно понимать, как простой код С++ может быть написан на С, чтобы досканально понимать все абстракции языка и какие действия скрыты от наших глаз.

Have a legacy. Stay cool.

#cppcore #goodoldc #compiler
5👍3313🔥12
Наследие Cfront. Полиморфизм
#новичкам

Часто мы говорим о каких-то вещах в С++ и можем сказать, как они работают под капотом. Но стандарт языка не говорит о том, как компиляторы должны реализовывать те или иные фичи. Он только задает требование. Тем не менее подходы к реализации некоторых фичей повторяются в разных компиляторах. И отчасти это так из-за влияния Cfront.

В С++ классический динамический полиморфизм подтипов синтаксически реализуется через виртуальные функции.

class Person {
protected:
std::string name;
int age;

public:
Person(const std::string &n, int a) : name(n), age(a) {}

virtual void describe() const {
std::cout << name << " (" << age << " years)";
}

virtual ~Person() = default;
};

class Employee : public Person {
std::string position;

public:
Employee(const std::string &n, int a, const std::string &pos)
: Person(n, a), position(pos) {}

void describe() const override {
std::cout << name << " (" << age << " years) - " << position;
}
~Employee() override = default;
};


Мы более подробно разбирали полиморфизм в одном из предыдущих постов. Сейчас мы поговорим, как примерно Cfront преобразовывал этот С++ код в С код.

Основные идеи:

1️⃣ Для каждого полиморфного класса формируется статическая таблица виртуальных функций. Это по сути массив указателей на виртуальные "методы" класса, которые Cfront представлял в виде обычных функций. Порядок методов в таблице определялся порядком их объявления в классе.

2️⃣ Нужно каким-то образом связать таблицу для класса с объектом этого класса. Для этого использовалось неявное дополнительное поле класса - указатель на таблицу виртуальных функций или vptr.

Вот как Cfront преобразовывал код выше(примерно, детали могут отличаться):

struct Person {
// pointer to vtable
void (**vptr)();
struct string name;
int age;
};

struct Employee {
struct Person base;
struct string position;
};


В самом первом базовом классе появлялся указатель vptr, который в конструкторе инициализируется правильным адресом нужной таблицы. void (**vptr)() - это тип указателя на указатель на функцию, возвращающую void и не принимающую аргументов.

Но погодите, виртуальные методы как минимум принимают один неявный аргумент this, а как максимум могут самые разнообразные сигнатуры иметь. Почему указатель имеет такой тип?

Ну а как вы еще засунете в один массив разные функции? Их кастили к одному общему типу void(*)(), только так это было возможно:

void (*Person_vtable[])() = {
(void(*)())Person_describe_impl,
(void(*)())Person_dtor
};

void (*Employee_vtable[])() = {
(void(*)())Employee_describe_impl,
(void(*)())Employee_dtor
};

void Person_describe_impl(const struct Person* this) {
printf("%s (%d years)", this->name, this->age);
}

void Person_dtor(struct Person* this) {
String_dtor(this->name);
}

void Employee_describe_impl(const struct Person* this) {
// cast to proper type
const struct Employee* emp = (const struct Employee*)this;
printf("%s (%d years) - %s", emp->base.name, emp->base.age, emp->position);
}

void Person_dtor(struct Person* this) {
String_dtor(this->position);
Person_dtor(this->base);
}


Cfront генерировал Сишные реализации методов и клал их в массив, попутно приводя к типу void(*)().

Ну а в месте вызова метода компилятору ничего не мешает сделать каст к нужному типу, так как он знает сигнатуру вызываемого метода:

void print_info(const Person* person) {
person->describe();
}


void print_info(const struct Person* person) {
void (*describe_func)(const struct Person*) =
(void (*)(const struct Person*))person->vptr[0];
describe_func(person);
}


В те времена С не был стандартизирован и правила преобразования типов были несколько мягче, поэтому такая магия с приведениями работала.

В наше время не нужна трансляция С++ в С, но тем не менее концепция таблиц виртуальных функций и указателей на них осталась.

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
👍23🔥159
​​Компилятору достаточно лишь правильно положить аргументы функции в правильные регистры, согласно calling conventions. Какие аргументы нужно подготовить компилятор знает заранее, так как сигнатура виртуальных методов одинакова для всех переопределенных вариантов. Остается лишь call'ьнуть нужный указатель и происходит виртуальный вызов.

Подкапотоное устройство полиморфизма часто спрашивают на собесах. Теперь вы знаете, как отвечать почти на полный спектр вопросов по этой теме.

Know whats under the hood. Stay cool.

#compiler #cppcore
24👍14🔥9
​​Наследие Cfront. Манглирование
#новичкам

В С нет перегрузки функций, поэтому там люди вынуждены каким-то образом руками разделять имена функций.

Вместо

void print(int x);
void print(float x);


Там пишут:

void print_int(int x);
void print_float(float x);


Плюс линкеры работают с именами символов. А имя функции в С не включает в себя параметры. Поэтому во всей программе не может быть двух функций с одинаковыми именами. Линкер их банально не различит и выдаст ошибку множественного определения.

А в С++ была перегрузка и надо было каким-то образом плюсовые перегруженные функции превращать в неперегруженные сишные при трансляции кода. Для этого было придумано декорирование имен или name mangling.

Самый простой способ - добавлять к конце функции ее параметры в закодированном виде: ИмяФункции_ТипыПараметров:

void draw(int x, int y);
void draw(double x, double y);

// Cfront mangling:
draw_i_i // draw(int, int)
draw_d_d // draw(double, double)


Так как у нас не может быть двух перегрузок с разными возвращаемыми значениями, то кодирование типа возврата не нужно.

Но это самое базовое представление о декорировании имен. Давайте посмотрим, что еще может влиять на итоговое имя функции:

👉🏿 2 разных класса могут иметь методы с одинаковым названием. Так как при трансляции в С это были просто свободные функции, манглинг должен учитывать и имя класса:

void Circle::paint(Color c);
void Square::paint();
Rectangle::~Rectangle();
Shape::~Shape();

// transform into

void Circle_paint_Color(struct Circle* this, struct Color c);
void Square_paint(struct Square* this);
void Rectangle_dtor(struct Rectangle* this);
void Shape_dtor(struct Shape* this);


👉🏿 Есть же еще и пространства имен. Они помогают разграничить скоуп существования имен. И названия пространства имен тоже манглировались в имена функций:

namespace Graphics {
class Canvas {
void clear();
};
}

// transform into

void Graphics_Canvas_clear(struct Canvas* this)


👉🏿 В С++ когда-то появились шаблоны. Шаблон всегда инстанцируется с каким-то типом. И чтобы различать эти инстанциации, Cfront манглировал типы шаблонных параметров в полное имя типа:

template<typename T>
class Stack {
void push(T value);
};

Stack<int> stack;

// transforms into

Stack_int_push_i(int value);


Конкретные преобразованные имена из поста могут быть не такими, какими их генерировал Cfront, но главное уловить идею.

В современных компиляторах тоже делается манглинг имен, чтобы линкер не ругался на одинаковые символы:


void Circle::rotate(int);

// transforms into

_ZN6Circle6rotateEi

// Разбор:
_Z - префикс C++
N - вложенное имя
6Circle - длина=6, "Circle"
6rotate - длина=6, "rotate"
E - конец аргументов
i - тип int


Шаблоны, неймспейсы, noexcept и const квалификаторы - все вшивается в имя символа.

Вы также можете вручную управлять манглингом: включать и выключать его:

extern "C" void c_function();   // Без манглинга: _c_function
extern "C++" void cpp_function(); // С манглингом: _Z10cpp_functionv


Это нужно для совместимости ABI интерфейсов, предоставляемых библиотеками.

Поэтому декорирование имен живее всех живых и повсеместно используется в современных компиляторах.

Have a legacy. Stay cool

#cppcore #goodoldc #compiler
22👍21🔥13
​​Время жизни и range-based for
#новичкам

Когда говорят, что в С++ легко отстрелить себе конечность, это не просто слова. Делается это в отдельных случаях почти играючи:

struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }

private:
std::vector<int> items_;
};

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

for (int x : generateData().items()) {
process(x);
}


Все очень просто: есть функция, возвращающая объект, содержащий коллекцию, и мы хотим обработать эту коллекцию. Хотим хорошего, но с размаха получаем UB в челюсть. За що?

Перед ответом экскурс в стандарт. Есть у вас range-based for:

for(const auto& item: <range>) {
process(item);
}


Range-based for - это по сути сахар, чтобы не писать много кода. И вот во что он разворачивается:

auto&& range = <range>;
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
process(item);
}


Если <range> - это временный объект, то цикл продлевает его время жизни. Но если для вычисления <range> использовался какой-то другой временный объект, то время его жизни уже не продлевается.

std::vector<int> generate() {
return {1, 2, 3, 4, 5};
}
for (const auto& item: generate()) {
...
}
// range-based for transforms into
auto&& range = generate();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
const auto& item = *it;
...
}

В примере сверху как раз продлевается время жизни вектора, возвращенного по значению из generate.

А вот во что преобразуется цикл из самого первого примера поста:

auto&& range = generateData().items();
for (auto it = range.begin(), end = range.end(); it != end ; ++it)
{
int x = *it;
process(x);
}


range биндится лишь к ссылке на внутреннее поле Foo, но не продлевает время жизни временного объекта, возвращенного из generateData(). Поэтому он спокойно уничтожится до цикла, который будет оперировать уже висячими ссылками.

Решается проблема несколькими способами. Самый простой - надо создать lvalue объект:

Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}

auto data = generateData();
for (int x : data.items()) {
process(x);
}


Другие решения рассмотрим в следующих постах.

Avoid dangling references. Stay cool.

#cppcore #cpp11
33🔥23👍8😁5
​​WAT
#опытным

Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.

Делаем вот так:

constexpr std::array array = {"I", "love", "C++"};

int main() {
if (auto iter = std::ranges::find(array, "C++"); iter == std::end(array)) {
assert(false && "comptime arg");
}
// let's go with runtime now
if (setenv("RUNTIME", array[2], 0) != 0) {
assert(false && "setenv");
}
char *runtime_str = getenv("RUNTIME");
assert(strcmp(runtime_str, array[2]) == 0 && "equal strings");

if (auto magick_iter = std::ranges::find(array, runtime_str);
magick_iter == std::end(array)) {
assert(false && "runtime arg");
}
}


Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.

Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.

После получаем значение этой переменной и сравниваем ее с оригиналом.

Ну и в конце ищем среди массива строку эту runtime_str.

Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.

Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."

WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?

Дьявол кроется в деталях.

Вспоминаем школьную математеку CTAD. Какой тип элементов массива выведется?

Правильно, const char *.

А как std::ranges::find сравнивает такие элементы?

Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.

Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.

Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.

А вот последний ассерт просто говорит о том, что указатель runtime_str не был найден в массиве, потому что там нет такого адреса.

И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.

В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.

Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.

Express your wishes precisely. Stay cool.

#cppcore #cpp17
21👍12😁7🔥5👎1
Пользовательские литералы
#новичкам

Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.

В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:

👉🏿 Целочисленные: 5, 42, 0xFF.

👉🏿 С плавающей точкой: 3.14, 6.02e23.

👉🏿 Символьные: 'a', '\n'.

👉🏿 Строковые: "Hello, world!".

👉🏿 Логические: true, false.

👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя - nullptr.

Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.

Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.

А мы живем все-таки в мире объектов
. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.

Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.

То есть в С++ давно был механизм, с помощью которого можно было изменять тип литерала через суффикс. Стоит лишь дать возможность программистам самостоятельно определять свои суффиксы, чтобы по-своему интерпретировать литерал.

Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!

Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:

class Temperature {
private:
double kelvin; // for precicion and consistency

explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};

Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}

{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}


Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.

Просто посмотрите, насколько сократился код, уменьшилось количество скобок и увеличилась читаемость. Выглядит круто.

Это было небольшое интро, в следующий раз рассмотрим кейсы, когда пользовательские литералы могут принести реальную пользу.

Extend your capabilities. Stay cool.

#cppcore #cpp11
24👍13🔥11🤯2🤷‍♂1🤪1
​​Пользовательские литералы. А зачем?
#опытным

В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.

Поехали:

🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:

auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;


Меньше деталей, больше фокуса на происходящем.

🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании auto. Поэтому легко может произойти такая ситуация, что вы возьмете и будете совместно оперировать синтаксически одинаковыми типами, но на деле они будут обозначать разные вещи. Условно, будем складывать градусы и радианы:

double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree


Получится неожиданный результат, даже если функция работает верно.

Вот шобы такого не было, можно использовать соответствующие литералы:

class Radian {...};

Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}

SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error


🥨 Автоматический вывод типов может легко сломаться вывести не тот тип, который вы ожидаете, если вы работаете с сырыми литералами. Пользовательский литерал же сразу на месте конструирует нужный объект и компилятор будет правильно интерпретировать его тип.

Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:

using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char *>);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);


Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.

🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:

template<size_t N>
struct FixedString {
char data[N];

constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}

constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};

template <FixedString str>
class Class {};

Class<"Hello World!"> cl;


И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.

В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.

Be useful. Stay cool.

#cppcore #cpp11 #cpp20
👍159🔥7😁1🤯1
​​Доступ к приватным членам. Указатали
#новичкам

Если по-честному, то все эти спецификаторы доступа к членам класса, это чисто синтаксическое ограничение на непреднамеренное использование в коде имен непубличных членов. Ну и способ выделение в классе интерфейса, чтобы сказать другим программистам, какими легальными способами можно оперировать объектом.

Но если вы хотите непотребств, вас никто не может ограничить. С++ имеет прямой доступ к памяти, а значит вы можете посмотреть под лупой, понюхать и облизать любой байтик объекта.

То есть банально

class MyClass {
private:
int secret = 42;
};

void illegalAccess(MyClass &obj) {
int *ptr = (int *)&obj; // assume that secret is first member
std::cout << "Illegal: " << *ptr << std::endl;
}


Как бы логично предположить, что если у класса только одно поле, то сам объект будет состоять только из этого поля. И можно спокойно привести указатель на объект к указателю на поле.

Но здесь есть целых 2 проблемы.

1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.

2️⃣ Вторая еще серьезнее. Цитата из стандарта:

The interpretation of a given construct is established without regard to access control. If the interpretation established makes use of inaccessible members or base classes, the construct is ill-formed.

Если ваша кодовая конструкция интерпретируется, как использование недоступных вам мемберов, то конструкция ill-formed. Не сказано, что сама программа ifndr, но это все равно значит, что код выше не соответствует правилам языка.

Первую проблему можно обойти с помощью дыры в стандарте memcpy:

void AccessWithMemcpy(MyClass& obj) {
int value;
std::memcpy(&value, &obj, sizeof(int));
std::cout << value << std::endl;
}


Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.

Если мемберов много, то нужно будет учитывать выравнивание полей в объекте.

В общем, это все может работать на конкретной архитектуре и компиляторе, если вы сами все руками в каждом конкретном случае проверяете. Но стандарт вас осуждает и ничего не обещает.

Пусть в конце каждого поста из серии будет эпилог: получать доступ к приватным полям - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.

Be legal. Stay cool.

#cppcore #badpractice
23👍17😁9🔥7❤‍🔥1😭1
​​WAT
#новичкам

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Ответ на квиз из поста выше - на экран выведется 8.

WAT? Строковые литералы конкатенируются? Да еще и пользовательский суффикс между двух литералов применяется к конкатенации?

Вообще, да. Сейчас во всем разберемся.

Для начала. Да, c-style строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.

С длинными строками, которые целиком не влезают на экран, неудобно работать: читать и редактировать. А если у вас настроены линтеры на ограничение длины строки, то все равно придется как-то разбивать на части эту длинную строку.

Можно это делать с помощью символов экранирования, например так:

auto str = "Suuuuuuuuuuuuuuupppeeeeeeeeeeeeeeeeeeeeeeeeeeerrrr
loooooooooooooooooooooooooooooong \ striiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";


Но здесь будут проблемы с тем, что вторая часть должна начинаться с самого начала следующей строки, иначе пробелы будут включены в сам литерал. Представьте, что будет с кодом, в котором внутри функций(например для репорта ошибок) будут так разделяться литералы.

Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:

auto str = "Hello "
// void
"World!";
std::cout << str << std::endl;

// OUTPUT
// Hello World!


Не важно сколько пробелов или новых строчек находится между подряд идущими литералами. Они все объединятся при компиляции. Можно даже комменты между ними ставить, они все равно склеятся.

Ну и теперь понятно, почему пользовательский суффикс применяется к полной конкатенации c-style строки. Фаза конкатенации строковых литералов идет раньше этапа компиляции, на котором определяется значение аргументов оператора. Поэтому аргументом и является уже склеенная строка.

Однако разрешается только один пользовательский суффикс использовать. Два и больше - ошибка компиляции.

Кстати, такая склейка есть только у строковых литералов. Цифры в числовых литералах обязательно должны идти подряд:

int num1 = 123; // OK
int num2 = 12 23 // ERROR
int num3 = 1'234; // if you want to logicaly devide large number


Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).

Don't break into pieces. Be whole. Stay cool.

#cppcore #cpp11
🔥25👍7🤯65❤‍🔥1
​​Шо там не так с union?
#опытным

Объединения в С++, мягко говоря, не любят. И сегодня мы обсудим почему.

Юнион - это такой специальный класс, который хранит только одно из своих полей. Любое, но одно. Объект объединения может хранить разные поля в одной и той же памяти. А размер union равен размеру максимального поля.

union U {
int integer;
float floating;
char character;
};

U u;
u.integer = 42; // now u hold int
u.floating = 3.14; // now u hold float
u.character = 'a'; // now u hold char


U может хранить или целое число, или число с плавающей точкой, или символ. Последнее записанное поле становится "активным" полем.

Объединение позволяет, например, хранить несвязанные типы в одном массиве.

Но с юнионами нужно быть очень осторожным. Вот представьте, вам на вход функции пришел массив объединений. Как понять, какое реально поле в нем лежит?

Никак. Такого функционала нет. Поэтому приходится делать разные обвязки:

struct Helper {
enum class Type { Int, Float, Char };

union U {
int integer;
float floating;
char character;
};

Type objectType;
U value;

Helper(int i) : objectType(Type::Int), value{.integer = i} {}
Helper(float f) : objectType(Type::Float), value{.floating = f} {}
Helper(char c) : objectType(Type::Char), value{.character = c} {}
};


При создании объекта Helper мы теперь явно перечислением задаем тип, который лежит в юнион. И все кажется безопасным, пока мы не сделаем ошибку:

Helper h{42};

h.value.floating = 3.14;


Мы изменили активное поле без изменения objectType. А теперь где-то в другом месте кода:

if (h.objectType == Helper::Type::Int) {
std::cout << h.value.integet << std::endl;
}


Вот и все. Мы вышли на кривую дорожку неопределенного поведения.

Доступ до неактивного члена объединения - UB.

То есть. Мало того, что эта штука не самостоятельная. Так еще и если мы набагали, то это может обернуться вагоном трудноотловимых проблем в рантайме.

По сути, объединениями можно пользоваться, если руки из нужного места растут. Но если с руками не повезло - расстрел. И вы будете долго ковыряться в кишках проекта с надеждой хоть на какую-то подсказку о том, что произошло.

В С++ давно уже есть прекрасная типобезопасная альтернатива - std::variant. При доступе не к тому типу он хотя бы выкинет исключение std::bad_variant_access. И будет понятно хотя бы, где собака зарыта. Точнее не всегда понятно где конкретно, но точно понятно, что зарыта именно собака. А это уже облегчает поиски.

Be safe. Stay cool.

#cppcore
27🔥12👍11
​​union class
#опытным

В прошлом посте мы упомянули, что union - это такой специальный класс. Это что значит, объединение может иметь методы?

Представьте себе, да!

Начиная с С++11 union'ы могут иметь полноценные конструкторы, деструкторы и другие методы.

Но есть ограничения:

👉🏿 не должно быть виртуальных методов

👉🏿 юнион не может быть наследником

👉🏿 юнион не может быть базовым классом

👉🏿 юнион не может хранить ссылочные типы

Во всем остальном - такой же класс!

Но вот как-то не можется мне придумать юзкейсы методов объединения.

Конструкторы и деструкторы нужны, чтобы union мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.

Например:

union U {
int i;
float f;
std::string s;
};

U u;


Попытка скомпилировать это дело приведет к ошибкам:
error: use of deleted function 'U::U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string()
requires is_default_constructible_v<_Alloc>

error: use of deleted function 'U::~U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::~basic_string()


Плюс объекты хочется перемещать и мувать, там тоже могут быть нетривиальные специальные методы.

union U {
U() {}
~U() {}
int i;
float f;
std::string s;
};

U u; // ОК


Ну а если уж разрешили специальные методы определять, то и обычные разрешили до кучи.

А вы используете методы объединений в своих проектах? Если да, то расскажите зачем оно может понадобиться, будет интересно.

Expand your horizons. Stay cool.

#cppcore #cpp11
17👍8🔥8
​​Доступ к приватным членам. Интерпретация.
#опытным

Продолжаем исследовать грязь и разврат. Предыдущий пост тут.

Указатели, блин, сложная тема. А для задачи доступа к приватным полям - особенно. Для объектов классов с несколькими полями нужно считать много деталей: учитывать выравнивание полей в объекте и на этой базе высчитывать сдвиг типизированного указателя. Возможно даже несколько кастов понадобиться.

Программисты - люди ленивые, поэтому давайте как-то упрощать.

Допустим, у меня есть класс Target с множеством полей. А что, если где-то будет структурка Hack, идентичная по расположению полей? Может быть как-то можно интерпретировать данные Target, как данные Hack?

Конечно можно. В плюсах можно ВСЕ.

Конкретно интерпретировать одни данные, как другие, позволяет делать union. Он хранит один объект любого типа из списка, перечисленного внутри union. Но объединение не знает, объект какого конкретного типа он хранит. Это возлагается на плечи программиста, а union позволяет "достать" этот объект через любой тип.

struct Target {
private:
int x = 42;
double y = 3.14;
};

union HackUnion {
HackUnion() {}

Target target;
struct {
int stolen_x;
double stolen_y;
} hacker;
};

int main() {
HackUnion u;
u.target = Target();

std::cout << "Stolen x: " << u.hacker.stolen_x << std::endl;
std::cout << "Stolen y: " << u.hacker.stolen_y << std::endl;
}
// OUTPUT:
// Stolen x: 42
// Stolen y: 3.14


Внутри union определяет структуру с точно таким же layout'ом, как у Target, только члены у нее публичные. И интерпретируем данные Target как набор полей этой структуры. Так мы можем достать stolen_x и stolen_y.

И это даже не UB, а вполне валидное поведение! Стандарт гарантирует, что обе структуры layout-compatible и имеют общую начальную последовательность полей. И в этом случае union может их читать без угроз со стороны библии С++.

Еcли же классы будут не такие простые, даже просто с полями с разными доступами, то стандарт уже гарантировать не будет. Как мы помним, в общем случае доступ к неактивному члену юниона - ub.

Кстати, если уж мы хотим "интерпретировать" один объект как другой, можно использовать приведение типов реинтерпретацией aka reinterpret_cast:

struct Target {
private:
int x = 42;
double y = 3.14;
};

struct Hacker {
int stolen_x;
double stolen_y;
};

Target target;

Hacker* hacker = reinterpret_cast<Hacker*>(&target);

std::cout << "Stolen x: " << hacker->stolen_x << std::endl;
std::cout << "Stolen y: " << hacker->stolen_y << std::endl;


Тут уже без сомнения UB, потому что нарушаем strict aliasing, но это не мешает программе успешно отрабатывать.

Пс: Получать доступ к приватным полям вне класса - плохо! Мы с вами это делаем для понимания механик языка, а не для вооружения здешних обителей оружием массового закакивания кода.

#cppcore #badpractice
23👍15🔥6😁2
​​Доступ к приватным членам. Други-функции.
#новичкам

В этой серии постов мы рассматриваем в том числе легальные способы получения доступа к приватным членам.

С++ - вообще амбассадор языковых средств обходов ограничений. Есть константный объект, которых хочется изменить? Пожалуйста, const_cast. Застряли где-то в пучинах кода и не можете красиво выйти? Пожалуйста, goto.

В ту же копилку можно отнести друзей. Ключевое слово friend в С++ позволяет сущности получить доступ ко всем непубличным члена класса. Сегодня рассмотрим друзей-функций.

struct Foo {
friend void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}
private:
int a = 42;
};


Завести друга очень просто(по крайней мере в С++). Вам нужно объявить или определить функцию внутри скоупа класса и пометить ее ключевым словом friend. Тогда класс начинает доверять вашей функции настолько, что раскрывает ей все свои секретики.

Можно также просто объявить функцию внутри класса, а определить ее вне:

struct Foo {
friend void foo(Foo& foo);
private:
int a = 42;
};

void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}


Разница в том, что в первом варианте функция ищется только с помощью ADL по аргументу, а во втором - по скоупу, в котором она определена. Но это уже для гиков.

В чем особенность френдов? Их недолюбливают, но иногда без них никуда.

С одной стороны, вы даете другой сущности доступ к приватным полям класса. И сразу возникает вопрос: а зачем? Почему нельзя обойтись публичным интерфейсом?

Но иногда действительно нельзя. И хрестоматийным примером здесь будет перегрузка операторов для симметричных операций.

Например, у вас есть класс для работы с комплексными числами и вы хотите определить для этого класса операцию умножения на скаляр:

class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}

// It works only for complex * double
Complex operator*(double scalar) const {
return Complex(real * scalar, imag * scalar);
}

// This is for double * complex
friend Complex operator*(double scalar, const Complex& c);
};

Complex operator*(double scalar, const Complex& c) {
return Complex(c.real * scalar, c.imag * scalar);
}


Честный метод-operator* будет работать только в том случае, когда объект комплексного числа стоит слева. А за умножение double * complex отвечает глобальный оператор*. И конкретную его перегрузку нужно сделать другом, чтобы он имел доступ к приватным полям.

Конкретно для этого примера есть вопросики по тому, зачем делать приватными поля комплексного числа, но тут важна суть работы с friend.

Если ваш класс работает со стандартными бинарными операторами(сравнения, арифметика) и у него есть непубличные поля, то вам может понадобиться определить эти стандартные глобальные операторы друзьями. Вот ссылочка на пример из библиотеки Eigen. Тот же самый принцип работает, например, с оператором вывода в поток:

class Complex {
// ...
friend std::ostream& operator<<(std::ostream& os, const Complex& obj);
};
std::ostream& operator<<(std::ostream& os, const Complex& obj) {
os << "Real: " << obj.real << ", Imaginary: " << imag;
return os;
}


Тоже пример дружественной перегрузки вывода из Eigen.

Страуструп кстати дает один интересный иллюстрационный пример: в рэнджах момент окончания последовательности задается в более общем виде, чем итератор - sentinel(ограничитель). Это по сути объект любого типа, главное чтобы итератор мог уметь сравниться с ним. И вот для сравнения итератора и ограничителя может понадобиться определить дружественные операторы == и !=.

За пределами же кейсов с операторами использование функций-друзей должно подвергаться строгим проверкам на предмет попыток обойти кривой дизайн.

Value your friends. Stay cool.

#cppcore
22🔥8👍6😁2
​​Доступ к приватным членам. Други-классы.
#новичкам

Ключевое слово friend может использоваться также, чтобы дать доступ ко всем непубличным членам одного класса другому классу. Ну или точнее - сразу всем его методам.

class Matrix {
private:
int data[10][10];
public:
friend class MatrixAnalyzer;
};

class MatrixAnalyzer {
public:
int sum(const Matrix& m) {
int sum = 0;
for (int i = 0; i < 10; i++)
for (int j = 0; j < 10; j++)
sum += m.data[i][j]; // Access private data
return sum;
}
};


Внутри класса Matrix делаем объявление класса MatrixAnalyzer и говорим, что он нам друг. Тогда в реализации MatrixAnalyzer мы сможем пользоваться приватными членами Matrix.

С друзьями классами ситуация еще серьезнее. Вы даете доступ к непубличным членам буквально каждому методу другого класса. И все эти методы могут вертеть все поля объекта на всех потенциальных вертелах. Это реально бывает где-то нужно?

Для любой гаечки в С++ найдется свое применение. И для другов-классов тоже:

1️⃣ У вас есть класс, который содержит в своем публичном скоупе еще один класс. Но у этого вложенного класса есть какие-то приватные члены, к которым хотелось бы получить доступ.

Например, есть класс контейнера-связного списка. В него вложен публичный класс итератора и приватный класс ноды.
template <typename T>
class LinkedList {
private:
struct Node {
T data;
Node *next;
public:
Node(const T &value, Node *n = nullptr) : data(value), next(n) {}
};
Node *head = nullptr;
public:
class Iterator {
private:
Node *current;
Iterator(Node *node) : current(node) {}
friend class LinkedList<T>;
public:
// ...
};

// ...
};


Мы не хотим, чтобы какой-то левый код мог создавать итератор, поэтому пометили его конструктор приватным. Но LinkedList-то должен уметь создавать итератор. Для этого мы и помечает LinkedList другом итератора, чтобы разрешить ему вызывать приватный конструктор.

2️⃣ Юнит-тестирование. Обычно конечно тестируют только публичный интерфейс класса и стараются покрыть все пути исполнения и кейсы использования, чтобы абсолютно вся логика, включая приватные методы, была протестирована. Это называется black-box тестирование.

Но иногда очень хочется отдельно протестировать конкретные приватные методы и проверить значения приватных полей. Если ваши приватные методы реализуют какие-то логически полные части общего алгоритма, то удобно явно в коде их и тестировать, чтобы ни один баг не проскочил. Это называется white-box тестирование.

И вот для белого ящика нужно пометить класс тестов другом тестируемого класса.

class Database {
private:
bool validateConnectionString(const std::string& connStr) {
// Very complicated logic
return true;
}
friend class DatabaseTest; // HERE
};

class DatabaseTest {
public:
static bool testValidation() {
Database db;
return db.validateConnectionString("valid string");
}
};


А какой вид тестирования предпочитаете вы? Почему?

Во время подготовки к этому посту нашел еще один интересный кейс применения, но он заслуживает отдельного поста.

Have friends. Stay cool.

#cppcore
23😁13👍9🔥3
Доступ к приватным членам. Sutter hack
#опытным

Спасибо @d7d1cd за идею для поста)

В любой системе есть дырки, которые могут(и обязательно будут) эксплуатировать заинтересованные люди. Вот и в С++ так же. Сегодня мы раскроем, как можно стандартными относительно неинвазивными(не изменяя первоначальный код класса) инструментами изменять приватные поля класса.

Возьмем простой класс:

class X {
public:
X() : private_(1) {}

template <class T>
void f(const T &t) {}

int Value() { return private_; }

private:
int private_;
};


Чтобы трюк сработал, в классе должен быть шаблонный метод.

Теперь следите за руками.

Стандарт говорит, что вы самые хамские-хамы, если пытаетесь получить доступ к непубличным членам и будете за это жестко наказаны. Они должны быть использованы только внутри методов класса.

Дак, мы и не против. Давайте просто впишем новый метод класса, где изменим приватное поле, как нам нужно. И для этого даже не нужно менять код класса. И ключ ко всему - шаблонный метод.

Мы можем вне класса специлизировать шаблон метода для работы с конкретным типом. Специализация шаблона метода - это такой же метод с такими же правами, он может получать доступ к непубличным полям.

И тогда класс будет себя вести именно так, как мы ему скажем. А скажем мы ему пару ласковых:

struct Y {};

template <>
void X::f(const Y &) {
private_ = 2;
}

int main() {
X x;
std::cout << x.Value() << std::endl; // prints 1
x.f(Y());
std::cout << x.Value() << std::endl; // prints 2
}


В специализированном методе мы изменяем приватное поле и для наглядности выводим значение приватного поля в консоль. Можете сами убедиться, что это работает.

Этот трюк был описан Гербом Саттером, поэтому и называется Sutter hack.

Однако с его помощью нельзя менять поведение стандартных объектов:

The behavior of a C++ program is undefined if it declares

- an explicit specialization of any member function of a standard library class template, or
- an explicit specialization of any member function template of a standard library class or class template, or
- an explicit or partial specialization of any member class template of a standard library class or class template, or
- a deduction guide for any standard library class template.


потому что явные специализации методов из STL приводят к ub.

В общем, интересно, как на стыке двух концепций - ООП и шаблонов - появляются такие интересные спецэффекты)

Hack the life. Stay cool.

#cppcore #template #fun
👍28🔥12❤‍🔥5🤯43
​​Доступ к приватным членам. Явная инстанциация
#опытным

В прошлый раз мы уже выяснили, что явно инстанцируя шаблонный метод класса, можно написать свою реализацию, которая будет жонглировать непубличными членами в самых виртуозных позах.

Но!

Мы так и не вышли за пределы класса. Ручная специализация шаблонного метода - это такой же метод класса, поэтому он и умеет трогать приватные поля.

Хочется прям снаружи получить доступ к полю и уже не ограничиваться реализацией метода.

С++ и это может воплотить в реальность.

Хоть стандарт и бьет по рукам за упоминание имен непубличных членов за пределами скоупа класса, все-таки есть исключения из правил:

[temp.spec.partial.general]/10

The usual access checking rules do not apply to non-dependent names used to specify template arguments of the simple-template-id of the partial specialization.

[temp.spec.general]/6
The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization

Если по-человечески, то проверка доступа к имени не проверяется при явной специализации и инстанциации шаблона. То есть:

class Foo {
private:
int data = 42;
};

template <auto V>
struct Bar {};
template struct Bar<&Foo::data>;


Этот код вполне влиден. Здесь мы используем указатель на поле класса Foo::data в качестве NTTP. Это валидно, потому что указатель на поле класса - это по сути смещение от начала объекта и оно известно на момент компиляции.

Однако, даже если вы явно инстанцируете шаблон, содержащий приватные типы, вы не сможете создать такой объект напрямую.

Bar<&Foo::data> b;
// error: 'int Foo::data' is private within this context


Выход заключается в том, чтобы сохранить значение указателя в статическом члене и передать его в другой класс.

template <typename PtrType>
struct Storage {
inline static PtrType ptr;
};

template <auto V>
struct PtrTaker {
struct Transferer {
Transferer() {
Storage<decltype(V)>::ptr = V;
}
};
inline static Transferer tr;
};

template struct PtrTaker<&Foo::data>;


Когда вы явно инстанцируете PtrTaker&lt;&amp;Foo::data&gt;, его статический член tr будет инициализирован, и в его конструкторе Storage&lt;PtrType&gt;::ptr получит значение. Теперь вы можете получить доступ к нему через:

Foo foo;
std::cout << foo.Storage<int Foo::>::ptr;


Это работает!

То есть мы просто взяли и вывели на консоль значение приватного члена класса, при этом никак не меняя его код. Можно также легко его изменить. И все это четко согласовано со стандартом.

Еще один раз шаблоны сломали инкапсуляцию. Да что ж это такое!

Спасибо, @SoulslikeEnjoyer, за материалы для поста)

Exploit loopholes. Stay cool.

#cppcore #template #fun
11👍8🔥6🤯6❤‍🔥4
​​Доступ к приватным членам. Явная инстанциация и друзья
#опытным

Оказывается способов легально залезть в непубличные кишки вашего класса довольно много, и сегодня обсудим еще один метод.

Мы уже с вами говорили про дружественные функции и что им дозволено получать доступ к приватным членами класса.

Но давайте посмотрим на следующий пример:

template <typename T>
struct Foo {
friend void bar() { cout << "Got it!" << endl; }
};

void bar();
template struct Foo<int>;

bar();


У нас есть шаблонная структура и у нее есть дружественная функция. Пока шаблон не инстанцирован, компилятор не видит определения функции. Поэтому чтобы вызвать bar, нужно явно инстанцировать шаблон и объявить функцию во внешнем скоупе, чтобы компилятор мог найти ее по имени.

Давайте проследим, что произошло. Функция bar - по сути свободная функция, которая может использовать все члены Foo. Но не только их. Она еще может использовать шаблонные параметры конкретной инстанциации.

И вот тут мы возвращаемся к тому, что использование имени приватного поля абсолютно законно в контексте явной инстанциации(см. предыдущий пост).

Давайте сделаем шаблонный параметр Foo указателем на поле и инстанцируем этот шаблон с указателем на приватное поле класса:

class Private {
private:
int data{};
};

template<int Private::* Member> // pointer to data member
struct Stealer {
friend int& dataGetter(Private& iObj) {
return iObj.*Member;
}
};

template struct Stealer<&Private::data>; // explicit instantiation
int& dataGetter(Private&);

int main() {
Private obj;
dataGetter(obj) = 42;
}


Вот и все, получаем ссылку на приватное поле и крутим его, как хотим. Можно поиграться с примером тут.

И еще один из шаблоны сломали инкапсуляцию. Да что ж это такое!

Спасибо, @SoulslikeEnjoyer, за материалы для поста)

Exploit loopholes. Stay cool.

#cppcore #template #fun
17🔥15❤‍🔥6👍3😱1
​​Бросаем дичь
#новичкам

В С++ есть исключения. Вы можете их любить или ненавидеть, но от этого не сбежать(почти).

Обычно как происходит. Есть стандартный std::exception или любой кастомный базовый класс исключения my_exception::BaseException. У них куча наследников и вот вы их бросаете в подходящих ситуациях.

Но это же С++: "Вы думали, что бросать можно только исключения? Пфф. Не смешите мои подковы и подержите мое пиво."

Бросать можно почти все, что угодно, что можно считать объектом.

Например так:

throw 1;


Бросаем число. А что, какие-то проблемы?

Или вот так:

throw nullptr;
throw "This is the end!";

void panic() { std::cout << "PANIC!" << std::endl; }
throw static_cast<void(*)()>(panic); // Указатель на функцию


Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.

Самое уморительное, что можно кинуть даже лямбду. Ведь это всего лишь объект замыкания, ничего более:

throw []{std::cout << "Things are going really bad...\n"; };


Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.

Так что при споре с коллегами вы теперь можете бросаться в них всеми предметами, от стула до какашеклямбды. Благо знаете как.

Be amazed. Stay cool.

#cppcore
🔥3913😁8👍6🤯1
switch
#новичкам

switch - это такая базовая штука, которая изучается в самом начале вместе с другими core-конструкциями языка типа условий и циклов. Правда после изучения она становится той книжкой на полке, которую редко трогают и ее достают скорее, чтобы протереть. Поэтому некоторые особенности switch забываются, а в голове новичков зачастую нет понимания, когда и как его все-таки использовать.

Сегодня проясним особенности, а в следующий раз поговорим о кейсах применимости.

Но для начала пойдем с начала. switch - это чуть более компактное и продвинутое условие. Скажем, у вас есть переменная типа int и в зависимости от ее значений, вы по разному обрабатываете данные. Пусть у вас есть http статус код и вы на его основе строите какую-то логику:

switch(status_code) {
case 200: // OK
processSuccess();
break;
// ...
case 400: // Bad Request
logClientError(status_code);
break;
// ...
case 500: // Service Unavailable
logServerError(status_code);
break;
// ...
default:
handleUnknownStatus(status_code);
break;
}


То есть 1 переменная - много вариантов развития событий.

Теперь погнали по особенностям:

👉🏿 Вы будете проваливаться в нижележащие кейсы, если не напишите break в конце текущего кейса. Это накладывает свои когнитивные сложности, но дает и возможности. Если у вас одинаково обрабатываются разные значения, то вы можете проваливаться в нижележащие кейсы, пока не дойдете до нужного обработчика и не брякнитесь(встретите break):

switch(status_code) {
case 200: // OK
processSuccess();
break;
case 400: // Bad Request
case 401: // Unauthorized
case 403: // Forbidden
case 404: // Not Found
logClientError(status_code);
break;
}


👉🏿 Не очень удобно в switch работать с диапазонами значений. Скорее всего выразительнее и безопаснее воспользоваться обычными условиями, если у вас обработчики соответствуют длинным диапазонам.

Существуют расширения компилятора, которые позволяют указывать в switch диапазон:

switch(x) {
case 0 ... 9: // GNU Extension
std::cout << "0-9\n";
break;
case 10 ... 19: // GNU Extension
std::cout << "10-19\n";
break;
}


Но это непереносимо и вообще не для этого розочка цвела. switch хорошо оптимизируется через jump table(вместо кучи if-else с линейной сложностью используется массив меток обработчиков, в котором за О(1) ищется нужный), а диапазоны значений в эту концепцию не вписываются.

👉🏿 В С++ очень органично вписаны объекты и пользовательские типы, наряду с тривиальными С-совместимыми типами. Поэтому может сложиться впечатление, что switch может, например, со строками работать. Но нет, он работает только с целочисленными типами, то есть с целыми числами и перечислениями(scoped и unscoped).

Если хотите выбирать обработчики на основе пользовательских типов, нужно использовать ассоциативные контейнеры:

using Handler = std::function<void()>;
std::unordered_map<std::string, Handler> handlers = {
{"start", &start},
{"stop", []{ stop(); }}
};

void execute(const std::string& cmd) {
if (auto it = handlers.find(cmd); it != handlers.end()) {
it->second();
}
}



👉🏿 Значения кейсов должны быть константными выражениями. То есть значениями, известными на этапе компиляции. Вы не можете выбрать обработчик, соотвествующий runtime значению.

При использовании enum'ов такой проблемы в принципе не возникает, но вот с int'ами да:

constexpr int getValue() {
return 10;
}

int main() {
constexpr int y = getValue();
int z = getValue();

int x = 10;

switch (x) {
case y: // OK: y - constant expression
std::cout << "x equals y\n";
break;
case z: // ERROR: z is runtime value
std::cout << "x equals z\n";
break;
default:
std::cout << "default\n";
}

return 0;
}



Вроде простая конструкция, но все равно есть нюансы.

Explore nuances. Stay cool.

#cppcore
20👍11🔥72