Порядок вычислений в С++



Рассмотрим простой вызов функции:



foo(std::unique_ptr{new A}, std::unique_ptr{new B});



Что может пойти здесь не так?



Есть 2 противоположных ответа на этот вопрос, но выбор каждого из них определяется версией плюсов.



Что было до С++17?



Был бардак, одним словом. Нормальный человек подумает, что в начале вычисляем new A, потом конструктор unique_ptr, потом new B и второй конструктор. Но это же С++. Компилятор на самом деле мог сгенерировать код, который вычисляет это выражение в любом рандомном порядке. Именно поэтому существует эта известная проблема, что компилятор так переупорядочит вызовы, что первым будет вызов new A, а вторым будет вызов new B и дальше конструкторы. Прикол в том, что если new B бросит std::bad_alloc, то мы получим утечку памяти. Успешно отработавший new выдаст указатель на память, которая никогда не будет возвращена обратно системе. Это мог бы сделать unique_ptr, но его конструктор так и не был вызван.



Да и вообще там много приколов неприятных было. Самые знаменитые примеры:



i = i++ + i++;



std::string s = "but I have heard it works even if you don't believe in it";

s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don't"), 6, "");



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



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



Именно поэтому std::make_unique и std::make_shared были незаменимы до 17-х плюсов. Потому что содержали внутри себя аллокацию памяти и давали базовую гарантию безопасности исключений.



Что стало после С++17?



Порядок вычислений стал более понятен. Там ввели кучу ужесточающих правил, не буду их всех приводить, потому что они скучные и скорее всего вы с ними не пересечетесь в своей работе. Главное, что в самом первом примере с функций foo и умными указателями теперь не сможет случиться утечка памяти. Новый закон таков, что не указано, в каком порядке должны быть вычислены аргументы функций, но каждый из них должен быть полностью вычислен до того, как начнет вычисляться следующий. Если второй new бросит bad_alloc, то первый указатель уже будет обернут в unique_ptr и при разворачивании стека будет вызван его деструктор и память освободится.



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



Stay safe. Stay cool.



#cpp17 #memory