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

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Идентификатор final для виртуальных методов
#новичкам

Продолжаем серию постов! Ранее мы уже упоминали идентификатор со специальным значением final в рамках наследования классов — запрещали создавать наследников того или иного класса. Это поведение распространялось на весь класс целиком, в том числе и на все его методы. Это может быть слишком строгим ограничением в какой-то ветке нашего подсемейства. Например, мы хотим его продолжать развивать, но точечно зафиксировать поведение одного конкретного переопределенного метода.

Запретить переопределение метода в наследниках можно с помощью идентификатора final (С++ 11 и выше):
struct Child : public Parent
{
void method_name() override final;
};

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

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

В качестве примера, давайте рассмотрим семейство датчиков умного дома. Пусть была разработана открытая библиотека, которая предоставляет некоторый набор интерфейсов для разных типов датчиков. Например, для подсемейства пожарных датчиков. Производители устройств могут наследовать специальный интерфейс и реализовать прошивку для своего девайса. Как у них работает этот сенсор — никто не знает, но главное, что в случае срабатывания такого датчика происходит важное, в рамках этого подсемейства, действие - вызывается бригада пожарных. Это достаточно важный смысл, который может быть заложен в наследника класса и ограничен в переопределении для производителя:
// Датчик возгорания
struct IFireSensor : public ISensor
{
// В случае срабатывания, вызываем пожарных
void onAlarm(control_panel_t *ctrl) override final
{
ctrl->call_fireman();
}
};

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

Конечно, можно написать комментарии к коду, в надежде, что их прочитают... Но вот практика показывает, что их периодически игнорируют. Если какое-то ограничение не срабатывает во время компиляции и доходит до ревью, то разработка затягивается. Пока это увидят, пока переделают... Да и вообще, это сработает, только если у проверяющего достаточно компетенций / внимательности.

Кстати, хоть final и может быть применён только к виртуальному методу, мы не можем с помощью него проверить действительно ли мы переопределяем метод. Идентификатор final может быть применён к новому объявленному методу, а override нельзя. Следовательно, мы можем добиться такой ситуации: живой пример.

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

#cppcore #cpp11
Как работает динамический полиморфизм?
#новичкам

Продолжаем серию постов! В предыдущих статьях мы немного познакомились с возможностями полиморфных классов. Давайте подумаем, как же эта штука работает? По возможности, на собеседованиях интересуются этим вопросом 😉

Наверняка у вас так или иначе пробегал вопрос в голове: как же во время выполнения программы получается выбрать нужную реализацию метода, обращаясь к указателю лишь базового класса?
struct Base
{
virtual void vmethod_1();
virtual void vmethod_2();
};

struct Derived : public Base
{
void vmethod_2() override;
};

Base *data = new Derived();

// Calls Derived::vmethod_2()
data->vmethod_2();


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

Объекты полиморфных классов отличаются тем, что содержат в себе скрытый указатель на дополнительный участок памяти. В частности, размер объекта полиморфного класса немного больше:
sizeof(Base) // returns 8


Несмотря на то, что в Base нет никаких полей, в данном случае размер не будет равен одному байту. Класс Base формально пуст, но как раз под этот скрытый указатель резервируется доп. память: живой пример. На платформе x86-64 размер указателя равен 8 байт.

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

Независимо от типа указателя на объект полиморфного класса, его скрытый указатель будет смотреть именно на ту таблицу, которая ассоциирована с конструированным классом:
// Скрытый указатель объекта
// смотрит на vtable класса Base
Base *data = new Base();

// Скрытый указатель объекта
// смотрит на vtable класса Derived
Base *data = new Derived();


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

Таблицы виртуальных методов генерируются на каждый полиморфный класс (не объект!), чтобы учесть все переопределения методов. Компилятор анализирует объявленные виртуальные методы и пронумеровывает их, а затем в этом порядке размещает в таблице. Например, для базового класса она будет выглядеть так:
|    vtable of Base   |
|---------------------|
| &Base::vmethod_1 |
|---------------------|
| &Base::vmethod_2 |


А для наследованного класса уже вот так:
|  vtable of Derived  |
|---------------------|
| &Base::vmethod_1 |
|---------------------|
| &Derived::vmethod_2 |


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

В общем случае, без каких либо оптимизаций, вызов виртуального метода состоит из следующих шагов:
1. Прочитать скрытый виртуальный указатель на таблицу
2. Сместить значение загруженного указателя до записи в таблице с адресом вызываемого метода
3. Прочитать адрес метода
4. Выполнить косвенный вызов по прочитанному адресу

Давайте мысленно препарируем участок вызывающего кода:
void virtual_call(Base *object)
{
// 1. Разыменовываем указатель `data` на класс `Base`
// 2. Читаем указатель на vtable
// 3. Смещаемся на величину 1 указателя
// 4. Читаем указатель на `vmethod_2`
// 5. Вызываем данный метод
// 6. Ого! Оказывается, это была переопределение Derived::vmethod_2
object->vmethod_2();
}


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

Продолжение в комментариях 👇

#howitworks #cppcore
C-style cast
#новичкам

Как уже неоднократно было нами отмечено, что язык C++ разрабатывался с поддержкой обратной совместимости языка C. В частности, в C++ поддерживается приведение в стиле C:
int value = (int)arg;

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

Вот давайте вспомним все операторы приведения, про которые мы успели рассказать? У нас были посты про:
- static_cast
- reinterpret_cast
- const_cast
- dynamic_cast

У каждого из них есть своя область применения и соответствующий алгоритм приведения, а так же наборы проверок! Т.к. C-style cast сочетает в себе все вышеперечисленные операторы, то большая часть проверок просто отсутствует... Они не проверяют конкретный случай, что является очень опасным моментом.

В случае невозможности желаемого приведения, C-style cast совершит другое подходящее. Рассмотрим ошибку из живого примера:
cpp
using PPrintableValue = PrintableValue *;
...
auto data = (PPrintableValue)value;

Мы хотели привести value к типу PrintableValue (int64_t -> int32_t). Но в результате неудачного нейминга псевдонима мы ошиблись. Вдруг клавиша P залипла просто? Вдруг рефакторинг неудачно прошел? В итоге мы собрали программу, смогли её запустить и привели int64_t к int32_t*, а дальше его попытались разыменовать. На первый взгляд, ошибка непонятна: мы ожидали static_cast, а получили reinterpret_cast. В больших продуктах такие ошибки могут оставаться незамеченными, пока не будет проведено полное тестирование продукта (вами или клиентом).

Давайте вспомним про приведение между ветками ромбовидного наследования из статьи про dynamic_cast. Использование C-style приведения бездумно выполнит то, что от него попросили и вляпается в ошибку, хоть красненьким и не подчеркивается :) На самом деле он выполнит reinterpret_cast, но это логическая ошибка! Нам очевидно, что этот оператор не подходит по смыслу, но может подойти static_cast. Если мы попробуем это сделать, будет ошибка компиляции:
error: invalid 'static_cast' from type 'Mother*' to type 'Father*':
Father *switched_son_of_father = static_cast<Father*>(son_of_mother);

Опустим тему с const_cast, думаю, тут и так все понятно.

И вот ладно, дело во внимательности и понимании предназначения операторов... C-style cast позволяет выполнить приведение к приватным предкам класса: живой пример. Вот от вас намеренно хотели скрыть возможность вмешательства в поведение предка, а вы это ограничение обошли и даже не заметили подвоха. Увидеть это на ревью так же сложно! Это ведет к очень забагованному поведению программы.

Оператор C-style cast скрывает в себе достаточно неочевидное поведение в некоторых ситуациях. Его сложно заметить, его сложно отлаживать. Возможно, что будет проще отказаться от него вовсе, чем помнить о всех подводных камнях. Предупреждения вам в помощь! Добавляйте опцию компилятора:
-Wold-style-cast

#cppcore #goodpractice
Double-Checked Locking Pattern. Мотивация
#новичкам

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

Начнем с того, что в стародавние времена до С++11 у нас была довольно примитивная модель памяти, которая вообще не знала о существовании потоков. И не было вот этой гарантии для статических локальных переменных:

...
If control enters the declaration concurrently
while the variable is being initialized,
the concurrent execution shall wait for
completion of the initialization.


Поэтому раньше люди не могли писать такой простой код:

Singleton& GetInstance() {
static Singleton inst{};
return inst;
}


и надеяться на то, что объект будет создаваться потокобезопасно.

Самое простое, что можно здесь придумать - влепить замок и не париться.

class Singleton {
public:
static Singleton* instance() {
Lock lock;
if (inst_ptr == NULL) {
inst_ptr = new Singleton;
}
return inst_ptr;
}
private:
Singleton() = default;
static Singleton* inst_ptr;
};


У нас есть какая-то своя RAII обертка Lock над каким-то мьютексом(до С++11 ни std::mutex, ни std::lock_guard не существовало, приходилось велосипедить(ну или бустовать, кому как удобнее)).

Обратите внимание, как это работает. Статические указатели автоматически zero-инициализируются нулем, поэтому в начале inst_ptr равен NULL. Дальше нужно проверить, если указатель еще нулевой, то значит мы ничего не проинициализировали и нужно создать объект. И делать это должен один тред. Но куда поставить лок? На весь скоуп или только внутри условия на создание объекта?

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

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

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

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

Solve your problems. Stay cool.

#multitasking #cppcore #cpp11
Что опасного в многопоточке?
#новичкам

Монстры, морские чудовища, жуткие болезни... Все это снится разработчику, ломающему голову над проблемой в его многопоточном коде. Что же такого трудного для понимания и для отлавливания может произойти?

Одна из многих проблем - когерентность кэша. У нас есть много вычислительных юнитов. У каждого из них есть свой кэш. И все они шарят общее адресное пространство процесса. Кэши напрямую не связаны с другими вычислительными юнитами, только со своими(это про кэши низких уровней). В такой архитектуре нужно четко определить механизм, по которому изменения одного кэша станут видны другому ядру. Такие механизмы есть. Например, упрощенный вариант того, что сейчас есть - модель MESI. Непростая штука и мы пока не будем разбираться в деталях. Важно вот что: на процесс, охватывающий промежуток от изменения одной кэш линии до того, как эти изменения станут доступны другому ядру, тратится время. И это не атомарная операция! То есть нет такого, что при каждом изменении кэш линии информация об этом инциденте моментально доходит до других юнитов и они тут же первым приоритетом подгружают новое значение. Это очень неэффективно. Поэтому может случиться такая ситуация, при которой переменная в одном кэше процессора уже изменилась, а в другом кэше еще осталась ее старая копия, которая используется другим процессором. Это и есть одна из граней проблемы когерентности кэша.

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

struct Class {
Class(int a, int b, int c) : x{a}, y{b}, z{c} {}
int x;
int y;
int z;
};

Class * shared;

void fun() {
shared = new Class{1, 2, 3};
}


Функция fun выполняется в каком-то потоке и и меняет значения переменной. Логично, что в начале выполняется создание объекта, а потом присвоение указателя. Но это актуально лишь для этого потока и так это видит соотвествующее ядро. Мы ведь в многопоточной среде, здесь убивают...
Может произойти так, что данные в другой процессор подтянутся в обратном порядке. То есть в начале появится инициализированный указатель, указывающий на какую-то память, а потом подтянется инфа об созданном на этой памяти объекте. Вот и получается, что этот другой поток может сделать проверку:

if (shared) 
// do smt with object

И код войдет в условие, потому что указатель ненулевой. Но память по этому указателю будет еще не инициализирована. А это, друзья, наше любимое UB.

И это в точности то, что может происходить с нашим беднягой синглтоном! Если вы думаете, что lock на мьютексе вас спасет, то нет, не спасет!

Да, лок подразумевает барьеры памяти и при unlock'e изменения флашатся. Но на незащищенном чтении-то они подтягиваются без барьеров! Это был небольшой спойлер для шарящих за барьеры. О них не сегодня.

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

Завтра как раз об этом и поговорим.

Be able to work in multitasking mode. Stay cool.

#concurrency #cppcore
Порядок вызовов конструкторов и деструкторов дочерних классов
#новичкам

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

Вот есть у вас какая-то иерархия классов.

struct Base1 {
Base1() {std::cout << "Base1" << std::endl;}
~Base1() {std::cout << "~Base1" << std::endl;}
};

struct Derived1 : Base1 {
Derived1() {std::cout << "Derived1" << std::endl;}
~Derived1() {std::cout << "~Derived1" << std::endl;}
};

struct Base2 {
Base2() {std::cout << "Base2" << std::endl;}
~Base2() {std::cout << "~Base2" << std::endl;}
};

struct Derived2 : Base2 {
Derived2() {std::cout << "Derived2" << std::endl;}
~Derived2() {std::cout << "~Derived2" << std::endl;}
};

struct MostDerived: Derived2, Derived1 {
MostDerived() {std::cout << "MostDerived" << std::endl;}
~MostDerived() {std::cout << "~MostDerived" << std::endl;}
};

MostDerived{};


Что мы увидим в консоли, если запустим этот код? Вот это:

Base2
Derived2
Base1
Derived1
MostDerived
~MostDerived
~Derived1
~Base1
~Derived2
~Base2


Мы имеем 2 невиртуальные ветки наследования в класса MostDerived. В самих классах могут быть виртуальные функции, это роли не играет. В этом случае правила конструирования объекта такое: переходим в самую левую ветку и вызываем поочереди констукторы базовых классов сверху вниз. Как только дошли до MostDerived, переходим вправо в следующую ветку и также вызываем конструкторы сверху вниз. И только после этого конструируем MostDerived.

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

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

Казалось бы, тривиальное знание для программиста. Но очень важно осознавать эти вещи для того, чтобы понять более сложные концепции или отвечать на более сложные вопросы. Например: "Зачем нужен виртуальный деструктор?", "В какой момент инициализируется указатель на виртуальную таблицу?", "Какой конкретно метод вызовется, если позвать виртуальный метод из констуруктора базового класса?". Без четкого понимания базы создания объектов, на эти вопросы конечно можно заучить ответы, но понимания никакого не будет. А программирование - это только про понимание, как и любая другая техническая дисциплина.

Поэтому

Understand essence of basic concepts. Stay cool.

#OOP #cppcore
Виртуальный деструктор
#новичкам

Возможно САМЫЙ популярный вопрос на собеседованиях на джунов и мидлов. Оно и справедливо в принципе: очень простой и понятный вопрос, но ответ на него требует хорошего уровня понимания ООП в принципе и как оно конкретно работает в плюсах. Динамический полиморфизм, наследование, порядок вызовов конструкторов и деструкторов - да, это база. Но именно ее и нужно проверить у начинающих и продолжающих разработчиков.

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

struct Resource {
Resource() { std::cout << "Resourse has been acquired\n";}
~Resource() { std::cout << "Resource has been released\n";}
};

struct Base {
Base() { std::cout << "Base Constructor Called\n";}
~Base() { std::cout << "Base Destructor called\n";}
};

struct Derived1: Base {
Derived1() {
ptr = std::make_unique<Resource>();
std::cout << "Derived constructor called\n";
}
~Derived1() {std::cout << "Derived destructor called\n";}
private:
std::unique_ptr<Resource> ptr;
};

int main() {
Base *b = new Derived1();
delete b;
}


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

В коде вроде все хорошо написано и невнимательный кандидат может выдать вот это:

Base Constructor Called
Resourse has been acquired
Derived constructor called
Derived destructor called
Resource has been released
Base Destructor called


Вот тут-то его и подловили! На самом деле никакого деструктора наследника вызвано не будет и соответственно ресурсы не освободятся. Интервьюер дает наводку посмотреть на деструктор базового класса. И кандидат с красным лицом кричит: "Деструктор - невиртуальный! По указателю на базовый класс вызовется сразу деструктор базового класса, а деструктор дочернего не вызовется. Будет утечка памяти". Его так на курсах научили говорить. И дальше он выдает правильный вывод программы.

И тут интервьюер говорит: "А что будет, если наследник не будет содержать никаких полей? Какие проблемы будут у этого кода?".

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

Естественно, это неправда.

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

if the static type of the object to be deleted 
is different from its dynamic type, the static type
shall be a base class of the dynamic type of
the object to be deleted and the static type
shall have a virtual destructor or
the behavior is undefined.


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

Для корректного поведения полиморфных объектов и вызова деструктора дочернего класса вам обязательно нужен виртуальный деструктор базового класса.

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

Stay armed. Stay cool.

#cppcore #interview
Default member initializer
#новичкам

Представьте себе большой класс, определенный целиком в одном файле. Этак строк на 300-400. Обычно принято в таком порядке описывать класс: конструкторы, деструктор, методы и только потом поля. Вариации могут быть разными, но из моей практики одно остается неизменным: объявления конструктора и полей находятся в разных концах тела класса. И вот бывают случаи, когда при создании объекта какие-то поля получают свое значение не из внешних параметров, а какие-то заранее заданные. Дефолтовые.

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

Такие удобства появились у нас в C++11 - default member initializer. Это именно то, что и хотелось иметь в описанных выше ситуациях. Пример

template<typename T>
struct Stack {
// rule of 5
void push(const T& elem) {...}
void push(T&& elem) {...}
T& front() {...}
T& front() const {...}
void pop() {}
T GetMinElem() {...}
private:
std::deque<T> container;
T min_elem{Limit<T>::max_value};
}

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

Пример здесь сильно укороченный. Если реализовывать все по чесноку, то реализация такого шаблонного класса займет приличное количество места. Вариантов методов и конструкторов может быть миллион. И я не очень хочу в них возвращаться, чтобы узнать, какое изначальное состояние имеет поле min_elem. А здесь мы сразу видим: у пустого стека примем значение минимального элемента, как максимально возможное значение этого типа. Тогда при добавлении в стек первого элемента для обновления минимума мы можем пользоваться тем же условием, что и для добавления остальных элементов
if (new_elem <= min_elem)
min_elem = new_elem;

Limit<T> - шаблонный класс, который хранит максимальное и минимальное значение для заданного шаблонного типа. Это может быть реализовано как угодно: через явные специализации, через if constexpr и так далее. Шаблонная магия в общем. Кто хочет, опять же, может в комментах попрактиковаться в реализации этого класса.

Кто не знал - пользуйтесь, вещь полезная.

Stay useful. Stay cool.

#cpp11 #cppcore
​​Member initialization. Best practices
#новичкам

Пост по запросу подписчика. Вот его вопрос.

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

Здесь я буду приводить какое-то общие и распространенные принципы. К каждому можно придраться и сказать "а у нас в проекте по-другому!". Исключения и другие подходы есть везде. Если хотите высказать свои варианты - комменты открыты.

Начну с того, что нужно предпочитать инициализировать поля либо с помощью списка инициализации конструктора, либо с помощью default member initializer. Дело в том, что все поля на самом деле инициализируются до входа в конструктор! Если списком инициализации или default member initializer'ом не установлено, как поле должно инициализироваться, то в конструктор оно попадет инициализированным по умолчанию. Именно поэтому, например, не можете в конструкторе инициализировать объект класса, у которого нет конструктора по умолчанию. Будет ошибка компиляции и у вас потребуют дефолтный конструктор. Запомните: конструктор нужен для нетривиальных вещей. С простой иницализацией справятся ctor initialization list и инициализатор по умолчанию.

Далее. Остается 2 способа, как инициализировать. Какой из них выбрать и в какой пропорции смешивать?

CppCoreGuidelies говорят нам: "Prefer default member initializers to member initializers in constructors for constant initializers".

То есть, если инициализатор константный, то используйте default member initializer.

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

class X { // BAD 
int i;
string s;
int j;
public:
X() :i{666}, s{"qqq"} { } // j is uninitialized
X(int ii) :i{ii} {} // s is "" and j is uninitialized
// ...
};


Как в этом случае читатель кода поймет, была ли инициализация j специально пропущена(что скорее всего не очень гуд) или было ли для s намеренным выставление его значения в "qqq" в первом случае и в пустую строку во втором случае(почти стопроцентный баг)? Все эти ошибки могут появиться при добавлении новых полей в класс. По классике: добавили новое поле, использовали его в методах, но вот в одном месте упустили инициализацию. Кейс настолько жизненный, что мое почтение.

Более адекватный вариант:
class X2 { 
int i {666};
string s {"qqq"};
int j {0};
public:
X2() = default; // all members are initialized to their defaults
X2(int ii) :i{ii} {} // s and j initialized to their defaults
// ...
};


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

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

Используйте default member initializer и будет вам счастье!

Stay happy. Stay cool.

#cpp11 #cppcore #goodpractice
Директивы ifdef, ifndef, if
#новичкам

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

Этот способ - использование директив препроцессора #ifdef, #ifndef, #if. Все три - условные конструкции. Первая смотрит, определен ли в коде какой-то макрос. Если да, то делаем одни действия, если нет - другие. Второй наоборот, входит в первую ветку условия, если макрос не определен, и входит во вторую, если определен. Директива #if проверяет какое-то условие, ничего необычного. Все три директивы могут иметь как полные формы(с веткой в случае если условие ложно), так и неполные(без "else").

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

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

int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
int result = 0;
#if CPU_TYPE == 0
// mmx|sse|avx code
#elif CPU_TYPE == 1
// arm neon code
#else
static_assert(0, "NO CPU_TYPE IS SPECIFIED");
#endif
return result;
}


Если каждое значение CPU_TYPE включает нужную ветку кода и убирает из текста программы все остальные.

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

int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
int result = 0;
#ifdef OPTIMIZATION_ON
// mmx|sse|avx code
#else
for (int i = 0; i < vec1.size(); ++i)
result += vec1[i] * vec2[i];
#endif
return result;
}


(Все примеры - учебные, все совпадения с реальным кодом - случайны, не повторяйте код в домашних условиях). Здесь мы проверяем директивой ifdef, определен ли макрос OPTIMIZATION_ON, сигнализирующий что нужно использовать векторные инструкции. Если да, то ключаем в текст программы оптимизированный код. Если нет - обычный.

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

Широко известно, что такой способ не только устарел, но еще и опасен. Завтра посмотрим, чем конкретно.

Choose the right path. Stay cool.

#compiler
​​Правила константности
#новичкам

Константность - важное свойство сущности в коде. Оно не только позволяет обезопасить объекты от изменения, но еще и говорит программисту о гарантиях, которые дает та или иная функция. Допустим, принимая параметр по константной ссылке, функция говорит программисту: "расслабься, ничего я не сделаю с твоим объектом". Это повышает читаемость кода.

В С++ много чего можно сделать константным. Объекты, указатели, ссылки, параметры функции, методы класса и тд. И зачастую новичкам сложно разобраться в правилах присваивания константности. Сегодня разберемся в этом.

Не будем долго задерживаться над константными методами. Константные объекты могут вызвать только константные методы. Все. Синтаксис такой:

void Class::Method(Type1 param1, Type2 param2) const {}

Теперь и константные, и неконстантные объекты могут вызывать метод Method.

Дальше все так или иначе сводится к правилам в объявлении переменных. Что в качестве поля класса, параметра функции, что объявлении обычной переменной - разницы нет. Правила одни. Поехали.

Константный объект можно объявить двумя способами:

const T obj;
// Либо
T const obj;


Эти записи абсолютно эквивалентны! Это очень важно запомнить, потому что при разговоре о ссылках и указателях это играет большую роль.

Собственно также есть 2 нотации объявления массивов констант:

const T arr[5];
// либо
T const arr[5];


И 2 нотации определения ссылок:

const T& ref;
// либо
T const & ref;


Помните, что при создании ссылки в скоупе функции вы обязаны ее инициализировать.
При объявлении поля класса этого делать не обязательно, потому что вы не создаете объект прямо сейчас. Но вы обязаны инициализировать ссылку до входа в конструктор либо через список инициализации конструктора, либо через default member initializer, так как базового поля класса иницализируются до входа в конструктор.
При объявлении параметра функции тоже не нужно сразу инициализировать ссылку, потому что функция принимает уже существующую и инициализированую ссылку на вход.

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

struct Type {
Type& operator=(const Type& other) {
std::cout << "copy assign" << std::endl;
return *this;
}
};

Type a{};
Type& b = a;
b = Type{};
// OUTPUT:
// copy assign


Когда говорят "константная ссылка" имеют ввиду ссылку на константу. И при любом виде объявления const T& ref или T const & ref она также будет ссылкой на константу.

Теперь указатели. Наверное самое сложное из всего перечисленного. Указатели, в отличии от ссылок, сами могут быть константными, да еще и указывать на константные объекты. А еще могут быть многоуровненые указатели. В общем сложно. Но есть правило: при объявлении указателя каждое появление ключевого слова const относится к тому уровню вложенности, который находится слева от этого слова. Вы просто читаете объявление справа налево и получаете правильное понимание объявления. Примеры:

// Читаем справа налево
int * const ptr; // ptr - это константный указатель на инт
int * * const ptr; // ptr - это константный указатель на указатель на инт
int * const * const ptr; // ptr - это константный указатель на константный указатель на инт

// Самый низкий уровень, который относится к самому объекту,
// можно писать двумя способами, о которых мы говорили выше

int const * const * * const ptr; // ptr - это константный указатель на указатель
// на константный указатель на интовую константу
const int * const * * const ptr; // Тоже самое


ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Rely on fixed thing in your life. Stay cool.

#cppcore

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ