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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Правильный swap двух объектов Ч2

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

Очень хочется написать просто типа такого:

struct my_type
{
friend void swap(my_type& first, my_type& second) noexcept {
std::swap(first.member_1, second.member_1);
std::swap(first.member_2, second.member_2);
}
Type1 member1;
Type2 member2;
};


И это работает для тривиальных типов Type1, Type2. Если у вас тривиальные члены - пожалуйста пользуйтесь.

Но если Type1, Type2 - пользовательские типы не из стандартной библиотеки? Понятное дело, что для стдшных типов есть специализации std::swap. Но вот для обычных смертных - нет. И если для этого смертного типа определена своя функция swap, которая лучше знает, как обменивать данные, то таким образом вы никогда ее не вызовете. А если какой-то из ваших мемберов не удовлетворяет условию std::is_move_constructible_v<T> && std::is_move_assignable_v<T>, то вы вообще не скомпилируете этот код.

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

Такая штука есть и называется она ADL или Argument Dependent Lookup. С ее помощью мы можем найти нужную функцию по типу аргумента. Можно написать:

struct my_type
{
friend void swap(my_type& first, my_type& second) noexcept {
swap(first.member_1, second.member_1);
swap(first.member_2, second.member_2);
}
Type1 member1;
Type2 member2;
};


И если для Type1 и Type2 определены свои функции swap, то они найдутся и все скомпилируется.

Однако выискивать для каждого типа своего мембера, есть ли у него своя собственная swap - задача неблагодарная. Хотелось бы и рыбку съесть и машку за ляшку не задумываться о выборе нужной функции и положить эту задачу на плечи компилятора. И это можно сделать!

struct my_type
{
friend void swap(my_type& first, my_type& second) noexcept {
using std::swap;
swap(first.member_1, second.member_1);
swap(first.member_2, second.member_2);
}
Type1 member1;
Type2 member2;
};


Какая тут магия произошла. Мы попрежнему используем ADL для поиска наиболее подходящей функции. Но если мы ее не нашли, то остается бэкап в виде std::swap, которая может вызваться благодаря using std::swap.

Примерно так и выглядит "каноничный" вид функции swap для ваших кастомных классов.

Sit on both chairs. Stay cool.

#cppcore
std::swap и ADL

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

Первая, довольно косвенная. Все алгоритмы стандартной библиотеки никогда не используют вызов std::swap напрямую для обмена элементов последовательности. Там все делается как в предыдущем посте с помощью using std::swap, чтобы как раз разрешить ADL найти самую подходящую перегрузку. Зачем это делать, если std::swap и так самостоятельно ищет через ADL?

Вторая - подобная реализация std::swap вгоняет в бесконечную рекурсию следующий код:

namespace ns
{
struct foo
{
foo() : i(0) {}
int i;
};
void swap(foo& lhs, foo& rhs)
{
std::swap(lhs, rhs);
}
}

template <typename T>
void do_swap(T& lhs, T& rhs)
{
std::swap(lhs, rhs);
}

int main()
{
ns::foo a, b;
do_swap(a, b);
}


Неважно хороший ли это код или нет, он может встречаться в проектах. Зачем определять свою функцию swap через std::swap - загадка, но это работает.

Почему вообще должно быть важно, что работает такой плохой код? Потому что обратная совместимость. Такой код вполне мог существовать и использование ADL внутри std::swap его бы сломало.

Тем не менее были предложения в стандарт, которые предлагали разрешить ADL внутри std::swap. И они вроде даже попали в драфт С++20, но в сам стандарт так и не вошли. Думаю, что в том числе и по причине обратной совместимости.

Так что для правильного свопа необходимо приписывать using std::swap. Или нет?

Я уже говорил, что в стандартной библиотеке алгоритмов не используется обмен элементов последовательности через std::swap. Но не сказал, как именно. А обменивают их через std::iter_swap(логично, там же итераторы используются). И вот его возможная имплементация:

template<class ForwardIt1, class ForwardIt2>
constexpr //< since C++20
void iter_swap(ForwardIt1 a, ForwardIt2 b)
{
using std::swap;
swap(*a, *b);
}


То, что нужно! Теперь можно писать так:

struct my_type
{
friend void swap(my_type& first, my_type& second) noexcept {
std::iter_swap(&first.member_1, &second.member_1);
std::iter_swap(&first.member_2, &second.member_2);
}
Type1 member1;
Type2 member2;
};


Немного корявенько, зато думать не надо.

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

Properly exchange values. Stay cool.

#cppcore
​​Зачем вообще нужен кастомный swap?

Коротко - незачем)

Но как всегда есть нюансы. Забайтились? Погнали разбираться.

Как всю историю человечества разделяет Рождество Христово, так и история С++ делится на две эпохи появлением стандарта С++11. Получается, что С++11 - Иисус в мире плюсов...

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

И вот в те времена кастомная функция swap была как нельзя кстати. Именно поэтому std::vector имеет отдельный метод swap. Рудимент архаичного прошлого...

С тех пор все стандартные алгоритмы в первую очередь ищут use-defined swap и уже на крайняк используют std::swap.

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

Но по сути-то, свап - это такой одновременный мув друг в друга(идейно). Ну и с появлением мув-семантики стандратная swap стала выглядеть именно так, как нам нужно идейно:

template <typename T>
void swap(T& x, T& y)
{
T temp(std::move(x));
x = std::move(y);
y = std::move(temp);
}


Эта версия свапа делает ровно то, что ожидали практически от всех кастомных swap'ов - эффективный обмен двух значений.

Она позволяет даже некопируемым объектам, типа стримов, мьютексов и прочих, обменяться местами. То есть она буквально отобрала весь хлеб у кастомной swap: теперь стандратная функция делает такой же эффективный обмен значениями, плюс может также обменять некопируемые объекты. Красота!

Но у самописной swap остается одно преимущество: Не происходит никаких вызовов конструкторов классов обмениваемых объектов. Мы напрямую обмениваем содержимое объектов. А std::swap все-таки вызывает один мув-конструктор и 2 мув присваивания. О производительности надо думать...

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

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

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

Use standard things. Stay cool.

#cppcore #cpp11
​​Вызываем оператор индексации у указателей

Есть суперважная проблема, с которой сталкиваются 100% разработчиков в каждой строке своего кода.

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

И вот теперь представим, что у нас есть класс с собственным перегруженным оператором[]. Например, std::deque. У нас каким-то образом в руках появился указатель на экземпляр этого контейнера. Как у него вызвать оператор взятия индекса объекта?

Если сделать так:

std::deque<int> * deque_inst = new std::deque<int>{1, 2, 3, 4, 5};
std::cout << deque_inst[4] << std::endl;


то получите огромную простыню нечитаемых ошибок компиляции.

Все потому что deque_inst - это указатель, а deque_inst[4] значит "сдвинь указатель на 4 вправо и разыменуй".

И шо делать?

Так же не напишешь:

deque_inst->[3]


Немного не очень опытных разработчиков знают, что можно вызывать операторы явно. Так вот в этом случае как раз можно вызвать оператор[] явно:

std::deque<int> * deque_inst = new std::deque<int>{1, 2, 3, 4, 5};
std::cout << deque_inst->operator[](3)<< std::endl;


Ну то есть все как обычно: есть указатель на объект, мы вызываем у него метод через оператор доступа к членам (для пролетариев - оператор стрелочка). Но этот метод - сам оператор и мы просто вызываем его согласно сигнатуре.

Такой вот прикол. Иногда может выручить.

Однако nobody knows зачем и как вы получили указатель на объект с переопределенным оператором[]. Возможно, вам стоит пересмотреть организацию вашего кода.

Express your thoughts explicitly. Stay cool.

#cppcore
​​Фактор загрузки std:unordered_map
#новичкам

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

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

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

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

У вектора есть поле - capacity, которое говорит о том, сколько элементов может вмещать внутренний буффер.

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

float load_factor() const;


Этот метод мапы возвращает ее фактор загрузки, который равен среднему числу элементов в одном бакете aka size() / bucket_count(). Эта та характеристика, которая определяет, когда мапа будет расширяться. Точнее не только она. Нужно же еще пороговое значение, при достижении которого произойдет расширение. А вот и оно.

float max_load_factor() const;
void max_load_factor( float ml );


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

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

Stay balanced. Stay cool.

#STL #cppcore
​​Виртуальный конструктор
#новичкам

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

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

Ответ: нет, не можем. Логика тут довольно простая. Виртуальные функции подразумевают собой позднее связывание объекта и вызываемого метода в рантайме. То есть для них нужны объекты(точнее vptr, которых находится внутри них). А объекты создаются в рантайме. И для создания объектов нужны констукторы. Получается, если бы конструкторы были виртуальными, то собака постоянно гналась бы укусить себя за жёпу получился бы цикл и парадокс(фанатам Шарифова посвящается). Нет указателя на виртуальную таблицу - нет виртуальности.

Если более формально и официально, то вот комментарий самого Бъерна по этому вопросу:

A virtual call is a mechanism to get 
work done given partial information.
In particular, "virtual" allows us to
call a function knowing only an interfaces
and not the exact type of the object.
To create an object you need complete
information. In particular, you need to
know the exact type of what you want to
create. Consequently, a
"call to a constructor" cannot be virtual.


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

Однако, нам по сути этого и не нужно. Нам нужен механизм создания объекта, зависящий от типа полиморфного объекта. И у нас такой механизм есть! Называется он фабричным методом. В ту же степь идет и паттерн "метод clone()".

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

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

Выглядит это так:

class Shape {
public:
virtual ~Shape() { } // A virtual destructor
// ...
virtual std::unique_ptr<Shape> clone() const = 0; // Uses the copy constructor
virtual std::unique_ptr<Shape> create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
std::unique_ptr<Shape> clone() const override;
std::unique_ptr<Shape> create() const override;
// ...
};
std::unique_ptr<Shape> Circle::clone() const { return new Circle(*this); }
std::unique_ptr<Shape> Circle::create() const { return new Circle(); }


У класса Фигура есть метод clone, который позволяет скопировать текущий объект в новый объект. Метод create позволяет дефолтно создать объект того же класса.

В класса Circle эти методы переопределяются. Теперь можно не зная точного типа полиморфного объекта вызвать его конструктор по умолчанию и копирования.

std::unique_ptr<Shape> ptr = std::make_unique<Circle>();

auto new_obj = ptr->create();

auto copy_obj = ptr->copy();


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

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

Use well-known tools for your task. Stay cool.

#interview #cppcore #pattern
Ковариантные возвращаемые типы

Есть такое интересное понятие, о котором вы возможно ни разу не слышали. Пример из поста выше с методами clone и create можно было написать иначе:

class Shape {
public:
virtual ~Shape() { } // A virtual destructor
// ...
virtual Shape* clone() const = 0; // Uses the copy constructor
virtual Shape* create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
public:
Circle* clone() const override;
Circle* create() const override;
// ...
};
Circle* Circle::clone() const { return new Circle(this); }
Circle* Circle::create() const { return new Circle(); }


Вы скажете: "Сигнатуры не совпадают! Код не скомпилируется!".

А я скажу: "Shape и Circle - ковариантные типы". С++ разрешает наследнику переопределять методы с возвращаемым типом, который является наследником типа метода из базового класса. Говорят, что это даже называется идиомой С++.

Какие юзкейсы у этой идиомы? По факту всего один. Представьте, что все методы возвращают один тип Shape. Вы создали объект Circle в куче и присвоили указатель на него к указателю на Circle. Тогда при клонировании объекта Circle вам вернется указатель на объект базового класса. И по хорошему его надо динамик кастить к Circle, чтобы работать с конкретным типом наследника. А это оверхэд:

Circle *circle1 = new Circle();
Shape *shape = d1->clone();
Circle *circle2 = dynamic_cast<Circle *>(shape);
if(circle2) {
// Use circle2 here.
}


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

Circle *circle1 = new Circle();
Circle *circle2 = d1->clone();


Выглядит намного лучше. Но вот вопрос: почему вы нигде не увидите в коде применения ковариантных типов?

Потому что этот подход не работает с умными указателями, которые де факто являются стандартом при возвращении объектов из фабрик. std::unique_ptr<Circle> не является наследником std::unique_ptr<Shape>, поэтому они и не ковариантные типы и сигнатуры методов будут несовместимы.

Возвращение сырых указателей - супер bad practice, один только этот факт заставляет отказаться от такого подхода.

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

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

Только что вы прочитали очередную статью про совсем ненужную хрень. Ставьте 🗿, если ваше лицо сейчас на него похоже)

Stay poker-faced. Stay cool.

#fun #cppcore
​​Приватный деструктор

Все мы с вами знаем, что можно делать конструкторы приватными. Например, для синглтон паттерна такое используется. Или для запрета создания объекта класса никаким другим образом, кроме как вызовом статический метода Create. Раньше, до появления возможности удаления функций в С++11 с помощью =delete, конструктор копирования делали приватным, чтобы запретить внешнему коду возможность копирования объекта.

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

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

struct CreationTest {
private:
~CreationTest() {};
}

CreationTest global_obj;

int main() {
CreationTest auto_obj;
CreationTest * dynamic_obj = new CreationTest;
// delete dynamic_obj;
}


Пойдем по порядку. global_obj. Его конструктор вызывается в статической области памяти до вызова main. А деструктор по идее должен вызваться после завершения main в функции std::exit. Однако проблема: std::exit - внешний код для класса CreationTest, поэтому она не имеет право вызвать деструктор. Значит, на этой строчке будет ошибка компиляции. Вы не можете создавать объекты с приватным деструктором в статической области.

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

Теперь dynamic_obj. Конструктор здесь вызывается самим оператором new, который в начале аллоцирует память и потом на этой памяти вызывает конструктор. С этим все хорошо. Но здесь намеренно допущена утечка, потому что если бы мы вызвали оператор delete, то и на этой строчке была бы ошибка.

То есть динамическая область - единственное место, где мы нормально можем создавать объекты. Но без удаления этих объектов жить будет как-то грустновато. Утечки памяти, ее фрагментация. В общем ничего хорошего. Нужно решать проблему гениально! Я подключаюсь к Галилео

Кто может получить доступ к приватным полям класса? Либо его методы, либо его кореша. То есть друзья. И это единственные сущности, которые помогут решить нам проблему. Покажу сразу оба варианта.

struct CreationTest {
static void Destroy(CreationTest * obj) {
delete obj;
}
friend void DestroyFunc(CreationTest * obj);
private:
~CreationTest() {};
}

void DestroyFunc(CreationTest * obj) {
delete obj;
}

int main() {
CreationTest * dynamic_obj = new CreationTest;
CreationTest::Destroy(dynamic_obj);
CreationTest * dynamic_obj1 = new CreationTest;
DestroyFunc(dynamic_obj1);
}


Теперь все компилируется без проблем.

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

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

auto deleter = [](CreationTest * obj) {DestroyFunc(obj);};
std::unique_ptr<CreationTest, decltype(deleter)> smart_obj(new CreationTest, deleter);


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

Protect your private life. Stay cool.

#cppcore #cpp11
​​nullptr
#новичкам

Вероятно, каждый, кто писал код на C++03, имел удовольствие использовать NULL и постоянно ударяться мизинцем ноги об этот острый уголок тумбочки. Дело в том, что NULL использовался, как обозначение нулевого указателя, который никуда не указывает. Но если он для этого использовался - это не значит, что он таковым и являлся. Это макрос, который мог быть определен как 0 aka int zero или 0L aka zero long int, но всегда это вариация интегрального нуля. И уже эти чиселки могли быть приведены к типу указателя.

Вот в этом-то и вся проблема. NULL очень явно хочет себя видеть в виде указателя, но по факту в зеркале видит число. Допустим, у нас есть 2 перегрузки одной функции: одна для инта, вторая для указателя:

class Spell { ... };

void castSpell(Spell * theSpell);
void castSpell(int spellID);

int main() {
castSpell(NULL);
}


Намерения ясны: мы хотим вызвать перегрузку для указателя. Но это гарантировано не произойдет! В произойдет один из двух сценариев: если NULL определен как 0, то просто без объявления войны в 4 часа утра вызовется вторая перегрузка. Если как 0L, то компилятор поругается на неоднозначный вызов: 0L может быть одинаково хорошо сконвертирован и в инт, и в указатель.

Проблему можно решить енамами и передавать для нулевого spellID что-то типа NoSpell. Но надо опять городить огород. Почему все не работает из коробки?!

С приходом С++11 начало работать из коробки. Надо только забыть про NULL и использовать nullptr.

Ключевое слово nullptr обозначает литерал указателя. Это prvalue типа std::nullptr_t. И nullptr неявно приводится к нулевому значению указателя для любого типа указателя. Это объект отдельного типа, который теперь к простому инту не приводится.

Поэтому сейчас этот код отработает как надо:

class Spell { ... };

void castSpell(Spell* theSpell);
void castSpell(int spellID);

int main() {
castSpell(nullptr);
}



Так как nullptr - значение конкретного типа std::nullptr_t, то мы может принимать в функции непосредственно этот тип, а не общий тип указателя. Такая штука используется, например, в реализации std::function, конструктор которого имеет перегрузку для std::nullptr_t и делает тоже самое, что и конструктор без аргументов.

/**
* @brief Default construct creates an empty function call wrapper.
* @post !(bool)this
/
function() noexcept
: _Function_base() { }

/
* @brief Creates an empty function call wrapper.
* @post @c !(bool)*this
/
function(nullptr_t) noexcept
: _Function_base() { }


По той же причине nullptr даже при возврате через функцию может быть приведен к типу указателя. А вот обычные null pointer константны не могут похвастаться таким свойством. Они могут приводиться к указателям только в виде литералов.

template<class T>
constexpr T clone(const T& t)
{
return t;
}
 
void g(int *)
{
std::cout << "Function g called\n";
}
 
int main()
{
g(nullptr); // Fine
g(NULL) // Fine
g(0); // Fine
 
g(clone(nullptr)); // Fine
// g(clone(NULL)); // ERROR: non-literal zero cannot be a null pointer constant
// g(clone(0)); // ERROR: non-literal zero cannot be a null pointer constant
}


clone(nullptr) вернет тот же nullptr и все будет работать гладко. А для 0 и NULL функция вернет просто int, который сам по себе неявно не конвертится в указатель.

Думаю, что вы все и так пользуете nullptr, но этот пост обязан быть на канале.

Как говорит одна древняя мудрость: "Use nullptr instead of NULL, 0 or any other null pointer constant, wherever you need a generic null pointer."

Be a separate subject. Stay cool.

#cppcore #cpp11
​​Что на самом деле представляют собой short circuit операторы?

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

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

Если подумать, то логика тут очень похожа на вложенные условия. Если первое выражение правдиво, переходим в вычислению второго, если нет, то выходим из условия(это для &&). И если еще подумать, то у нас и нет никаких других средств это сделать, кроме джампов(условных переходов к метке). Покажу, во что примерно компиляторы С/С++ преобразуют выражение содержащее оператор &&. Не настаиваю на достоверность и точность. Объяснение больше для понимание происходящих процессов.

Вот есть у нас такой код


if (expr1 && expr2 && expr3) {  
// cool operation
} else { 
// even cooler operation
}
// the coolest operation


Он преобразуется примерно вот в такое:


if (!expr1) goto do_even_cooler_operation; 
if (!expr2) goto do_even_cooler_operation; 
if (!expr3) goto do_even_cooler_operation; 

{
// cool operation
goto do_the_coolest_operation;


do_even_cooler_operation: 

// even cooler operation


do_the_coolest_operation:
// the coolest operation

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

То есть встроенные операторы && и || разворачиваются вот с такую гармошку условий. Надеюсь, для кого-то открыл глаза, как это работает)

See what's under the hood. Stay cool.

#compiler #cppcore