Порядок вычислений в С++
Рассмотрим простой вызов функции:
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
Рассмотрим простой вызов функции:
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