# (N)RVO vs RAII C++ восхитительный язык. В нем столько идиом, концепций, и каждая со своей замечательной, иногда невыговариваемой, аббревиатурой! А самое замечательное в них то, что они иногда конфликтуют. И от их конфликта страдать придется разработчику. А иногда они вступают в симбиоз и страдать приходится еще больше. В C++ есть конструкторы, деструкторы и приходящая с ними концепция RAII: Захватывай и инициализируй ресурс в конструкторе, очищай и отпускай в деструкторе. И будет тебе счастье. Ну что ж, давайте попробуем! Сделаем какой-нибудь простенький класс, выполняющую буферизированную запись: ```C++ struct Writer { public: static const size_t BufferLimit = 10; // захватываем устройство, в которое будет писать Writer(std::string& dev) : device_(dev) { buffer_.reserve(BufferLimit); } // в деструкторе отпускаем, записывая все, что набуферизировали ~Writer() { Flush(); } void Dump(int x) { if (buffer_.size() == BufferLimit){ Flush(); } buffer_.push_back(x); } private: void Flush() { for (auto x : buffer_) { device_.append(std::to_string(x)); } buffer_.clear(); } std::string& device_; std::vector buffer_; }; ``` И попробуем им красиво воспользоваться: ```C++ const auto text = []{ std::string out; Writer writer(out); writer.Dump(1); writer.Dump(2); writer.Dump(3); return out; }(); std::cout << text; ``` [Работает!](https://godbolt.org/z/szhvbM). Печатает `123`. Все как мы и ожидали. Как похорошел язык! Ага. Только работает оно исключительно потому что нам повезло. Тут, начиная с C++17, гарантированные NRVO (_named return value optimization_) и copy elision. А программа написана вообще-то с очень злобной ошибкой. И если мы возьмем, например, MSVC, который последним стандартам частенько забывает полностью соответствовать. То результат внезапно будет [иной](https://rextester.com/OKK46123). Если мы чуть-чуть модифицируем программу: ```C++ int x = 0; std::cin >> x; const auto text = [x]{ if (x < 1000) { std::string out; Writer writer(out); writer.Dump(1); writer.Dump(2); writer.Dump(3); return out; } else { return std::string("hello\n"); } }(); std::cout << text; ``` то под clang все еще работает, а под gcc — [нет](https://godbolt.org/z/5GWba8) И самое замечательное во всем этом безобразии, что никакое это не неопределенное поведение! Помните, мы обсуждали [не работающее перемещение](../syntax/move.md)? И выясняли, что в C++ нет деструктивного перемещения. А оно все-таки есть. Иногда. Когда срабатывает оптимизация возвращаемого значения и удаление лишних вызовов конструкторов копий/перемещений. Программы выше все неправильные. Они предполагают, что деструктор `Writer` будет вызван до возврата значения из функции. Чего никак быть не может. Деструкторы объектов вызываются всегда после возврата из функции. Иначе эти самые значения бы просто умирали, и вызывающий код всегда получал мертвый объект. Но как же тогда оно иногда работает и скрывает такую печальную ошибку? А вот как: ```C++ const auto text = []{ std::string out; Writer writer(out); // (2) адреса out и text одинаковые. // по сути это один и тот же объект writer.Dump(1); writer.Dump(2); writer.Dump(3); return out; // (1) это единственная точка возврата из функции // NRVO позволяет в качестве адреса временной // переменной out подложить адрес переменной, // в которую мы запишем результат — text }(); // (3) деструктор Writer пишет напрямую в text ``` Без всех хитроумных оптимизаций же происходит следующее: ```C++ const auto text = []{ std::string out; // (0) строка пуста Writer writer(out); // (1) адреса out и text разные. Это разные объекты writer.Dump(1); writer.Dump(2); writer.Dump(3); // (2) записи не происходило — буфер не заполнился return out; // (3) возвращаем копию out — пустую строку }(); // (3) деструктор Writer пишет в out, она умирает и не достается никому // text пуст ``` Никакого неопределенного поведения тут, повторяю, нет. Просто всякий деструктор/конструктор с побочными эффектами как бы «сломан» из-за разрешенных и описанных в стандарте (и даже иногда гарантированных) оптимизаций. Ну а в каком-нибудь Rust нам такую ерунду написать [просто не дадут](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c5c9b4edbf891d469214eae29a3ca1af). Такие дела. Исправляется проблема либо вытаскиванием `Flush` наружу и явным вызовом его. Либо добавлением еще одной вложенной области видимости: ```C++ const auto text = []{ std::string out; { Writer writer(out); writer.Dump(1); writer.Dump(2); writer.Dump(3); } // деструктор Writer вызывается здесь return out; }(); std::cout << text; ``` Не забудьте только оставить комментарий, чтобы ваши коллеги случайно не удалили такие «лишние» скобочки. И проверьте, что ваш автоформаттер кода также их не удаляет. ## Полезные ссылки: 1. https://en.cppreference.com/w/cpp/language/copy_elision 2. http://eel.is/c++draft/class.copy.elision