mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
142 lines
7.7 KiB
Markdown
142 lines
7.7 KiB
Markdown
# (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 |