Files
ubbook/runtime/rvo_vs_raii.md
T
2022-03-20 12:49:46 +03:00

142 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# (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<int> 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