#опытным
У компилятора большая свобода в том, что и как он может делать с исходным кодом при компиляции.
Возьмем, например, вызов функции:
f( g(expr1, expr2), h(expr3) );
В каком порядке вызываются expr1, expr2, expr3, g, h и f?
Культурно западный человек интуитивно будет представлять обход в глубину слева направо. То есть порядок вычисления будет примерно такой: expr1 -> expr2 -> g -> expr3 -> h -> f.
Однако это абсолютно не совпадает с тем как поступает компилятор в соответствии со стандартом.
Что было до С++17?
Было единственное правило: все аргументы функции должны быть вычислены до вызова функции. Все!
То есть могло теоретически мог бы быть такой порядок: expr2 -> expr3 -> h -> expr1 -> g -> f.
Полный бардак! И это приводило на самом деле к неприятным последствиям.
Что если мы принимаем в функцию два умных указателя и попробуем вызвать ее так:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));Какие тут могут быть проблемы?
Итоговый порядок вычислений может быть следующий:
new SomeClass1{} -> new SomeClass2{} -> std::unique_ptr<SomeClass1> -> std::unique_ptr<SomeClass2>Что произойдет, если SomeClass2 выкинет исключение? Правильно, утечка памяти. Для объекта, созданного как new SomeClass1{}, не вызовется деструктор.
Эту проблему решали с помощью std::make_* фабрик умных указаателей:
bar(std::make_unique<SomeClass1>(), std::make_unique<SomeClass2>());
Нет сырого вызова new, а значит если из второго конструктора вылетит исключение, то первый объект будет уже обернут в unique_ptr и для него вызовется деструктор.
Это было одной из мощных мотиваций использования std::make_* функций для умных указателей.
Что стало с наступлением С++17?
f(e(), g(expr1, expr2), h(expr3));
До сих пор неопределено в каком порядке вычислятся e, f и h. Или expr1 и expr2.
Но четко прописано, что если компилятор выбрал вычислять expr1 первым, то он обязан полностью вычислить g прежде чем перейти у другим аргументам. Это уже примерно как обход в глубину, только порядок захода в ветки неопределен.
Теперь такой код не будет проблемой:
void bar(std::unique_ptr<SomeClass1> a, std::unique_ptr<SomeClass2> b) {}
bar(std::unique_ptr<SomeClass1>(new SomeClass1{}), std::unique_ptr<SomeClass1>(new SomeClass2{}));потому что на момент вызова конструктора второго параметра уже будет существовать полностью созданный объект уникального указателя, для которого вызовется деструктор при исключении.
Это немного обесценило использование std::make_* функций. Но их все равно предпочтительно использовать из-за отсутствия явного использования сырых указателей.
Fix problems. Stay cool.
#cppcore #memory #cpp17