Почему можно возвращать std::unique_ptr из функции без std::move?
Правило NRVO и автоматическое перемещение при возврате
Вы столкнулись с важной особенностью C++11 и более новых стандартов, которая называется автоматическое перемещение (implicit move) при возврате локальных переменных. Это не магия, а четко прописанное правило языка, которое делает код более эффективным и удобным.
Когда функция возвращает локальную переменную (например, std::unique_ptr ptr), компилятор пытается применить оптимизацию NRVO (Named Return Value Optimization) — это разновидность copy elision (пропуска копирования). Если NRVO не срабатывает (например, из-за сложной логики или множественных точек возврата), то вступает в силу правило из §12.8/32 стандарта C++11 (и последующих):
- Если возвращается локальная переменная, которая не является параметром функции, и она имеет автоматическую продолжительность хранения (не static, не global), то компилятор сначала пытается применить NRVO.
- Если NRVO невозможно, то компилятор рассматривает возвращаемую переменную как rvalue (правостороннее значение) и пытается вызвать конструктор перемещения (move constructor) вместо конструктора копирования.
В вашем примере с createInt() переменная ptr — это локальная переменная. Компилятор видит, что она будет уничтожена сразу после возврата, поэтому безопасно "переместить" ее содержимое в результат функции. Это работает даже без явного вызова std::move(). Вот как это выглядит под капотом:
// Компилятор преобразует return ptr; во что-то вроде:
return std::move(ptr); // Неявно!
Но почему это не работает в вашем втором примере с test()? Потому что там a — это lvalue (именованная переменная), и копирование запрещено. Правило неявного перемещения применяется только при возврате из функции (и в некоторых других контекстах, например, при инициализации catch-блока).
Важно: это правило работает только для:
- Локальных переменных (не параметров функции).
- Переменных с автоматическим хранением (не static, не thread_local).
- Переменных, которые не являются ссылками.
Если вы попробуете вернуть std::unique_ptr, который является членом класса или статической переменной, то компилятор потребует явного std::move():
std::unique_ptr<int> globalPtr;
std::unique_ptr<int> badReturn() {
return globalPtr; // Ошибка! Неявное перемещение не работает для глобальных переменных.
// Нужно писать: return std::move(globalPtr);
}
Также это правило не работает, если переменная была передана в функцию как параметр:
std::unique_ptr<int> wrapPtr(std::unique_ptr<int> p) {
return p; // Ошибка! p — параметр, а не локальная переменная.
// Нужно: return std::move(p);
}
В C++20 это правило было расширено: теперь неявное перемещение применяется и к параметрам функций, но только в определенных контекстах (например, при возврате параметра, который не является ссылкой). Однако для unique_ptr это все равно требует явного std::move() в старых стандартах.
Итог: при возврате локального std::unique_ptr из функции вы можете писать просто return ptr; — компилятор сам позаботится о перемещении. Это безопасно, эффективно и соответствует идиоматическому C++.
Практический совет: когда все же стоит использовать std::move()
Хотя компилятор автоматически перемещает локальные переменные при возврате, бывают ситуации, где явный std::move() улучшает читаемость или необходим для корректной работы:
- Условный возврат: если у вас несколько точек возврата и компилятор не может применить NRVO, явный move может помочь избежать копирования (хотя в C++17 и новее это уже не проблема).
- Возврат из тернарного оператора:
return condition ? std::move(ptr1) : std::move(ptr2);— здесь без move не обойтись, так как тернарный оператор создает lvalue. - Явная документация намерений: если вы хотите подчеркнуть, что переменная больше не используется, можно написать
return std::move(ptr);. Это не обязательно, но может помочь другим разработчикам понять ваш код.
Однако в большинстве случаев следуйте правилу: если переменная локальная — просто пишите return ptr;. Компилятор C++11 и новее сделает все правильно. Излишнее использование std::move() может даже помешать оптимизациям вроде NRVO, поэтому добавляйте его только когда это действительно необходимо.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги