Возвращение unique_ptr из функций
unique_ptr<T>
не позволяет создавать копии, вместо этого он поддерживает семантику перемещения. Тем не менее, я могу вернуть unique_ptr<T>
из функции и присвойте возвращаемое значение переменной.
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> foo()
{
unique_ptr<int> p( new int(10) );
return p; // 1
//return move( p ); // 2
}
int main()
{
unique_ptr<int> p = foo();
cout << *p << endl;
return 0;
}
Приведенный выше код компилируется и работает как задумано. Так как же эта линия 1
не вызывает конструктор копирования и приводит к ошибкам компилятора? Если бы мне пришлось использовать линию 2
вместо этого будет иметь смысл (используя строку 2
работает, но мы не обязаны это делать).
Я знаю, что C++0x позволяет это исключение unique_ptr
поскольку возвращаемое значение является временным объектом, который будет уничтожен при выходе из функции, что гарантирует уникальность возвращаемого указателя. Мне любопытно, как это реализовано, это специальный случай в компиляторе или есть какое-то другое предложение в спецификации языка, которое это эксплуатирует?
7 ответов
есть ли еще какое-то предложение в спецификации языка, которое это эксплуатирует?
Да, см. 12.8 §34 и §35:
При соблюдении определенных критериев реализация может опустить конструкцию копирования / перемещения объекта класса [...] Это исключение операций копирования / перемещения, называемое копированием, разрешено [...] в операторе возврата в функция с возвращаемым типом класса, когда выражение является именем энергонезависимого автоматического объекта с тем же типом cv-unqualified, что и тип возвращаемого значения функции [...]
Когда критерии для исключения операции копирования выполнены, и объект, который должен быть скопирован, обозначен lvalue, разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен rvalue.
Просто хотел добавить еще один момент, что возвращение по значению должно быть выбором по умолчанию здесь, потому что именованное значение в операторе возврата в худшем случае, то есть без исключений в C++11, C++14 и C++17, обрабатывается как ценность. Так, например, следующая функция компилируется с -fno-elide-constructors
флаг
std::unique_ptr<int> get_unique() {
auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
return ptr; // <- 2, moved into the to be returned unique_ptr
}
...
auto int_uptr = get_unique(); // <- 3
При установленном флаге при компиляции в этой функции происходит два хода (1 и 2), а затем один ход (3).
Это никоим образом не относится к std::unique_ptr
, но относится к любому классу, который является подвижным. Это гарантируется правилами языка, так как вы возвращаетесь по значению. Компилятор пытается исключить копии, вызывает конструктор перемещения, если он не может удалить копии, вызывает конструктор копирования, если он не может перемещаться, и не может скомпилировать, если он не может копировать.
Если у вас была функция, которая принимает std::unique_ptr
в качестве аргумента вы не сможете передать ему p. Вам бы пришлось явно вызывать конструктор перемещения, но в этом случае вы не должны использовать переменную p после вызова bar()
,
void bar(std::unique_ptr<int> p)
{
// ...
}
int main()
{
unique_ptr<int> p = foo();
bar(p); // error, can't implicitly invoke move constructor on lvalue
bar(std::move(p)); // OK but don't use p afterwards
return 0;
}
unique_ptr не имеет традиционного конструктора копирования. Вместо этого он имеет "конструктор перемещения", который использует ссылки rvalue:
unique_ptr::unique_ptr(unique_ptr && src);
Ссылка rvalue (двойной амперсанд) будет связываться только с rvalue. Вот почему вы получаете ошибку, когда пытаетесь передать lvalue unique_ptr в функцию. С другой стороны, значение, которое возвращается из функции, обрабатывается как значение r, поэтому конструктор перемещения вызывается автоматически.
Кстати, это будет работать правильно:
bar(unique_ptr<int>(new int(44));
Временный unique_ptr здесь является значением.
Я думаю, что это прекрасно объясняется в пункте 25 " Эффективного современного С ++" Скотта Мейерса. Вот выдержка:
Далее в части стандарта, благословляющей RVO, говорится, что если условия для RVO выполнены, но компиляторы решают не выполнять копирование, возвращаемый объект должен рассматриваться как значение. По сути, стандарт требует, чтобы при разрешении RVO было либо выбрано копирование, либо
std::move
неявно применяется к возвращаемым локальным объектам.
Здесь RVO относится к оптимизации возвращаемого значения, и если условия для RVO выполняются, значит возвращать локальный объект, объявленный внутри функции, которую вы ожидаете выполнить в RVO, что также хорошо объясняется в пункте 25 его книги, ссылаясь на стандарт (здесь локальный объект включает временные объекты, созданные оператором return). Самым большим отрывом от выдержки является либо исключение копии, либо std::move
неявно применяется к возвращаемым локальным объектам. Скотт упоминает в пункте 25, что std::move
применяется неявно, когда компилятор решает не удалять копию, а программист не должен делать это явно.
В вашем случае, код явно является кандидатом на RVO, так как он возвращает локальный объект p
и тип p
совпадает с типом возвращаемого значения, что приводит к удалению копии. И если по какой-либо причине компилятор решит не удалять копию, std::move
бы пнул в линию 1
,
Одна вещь, которую я не видел в других ответах Чтобы уточнить другие ответы, существует разница между возвратом std::unique_ptr, который был создан внутри функции, и тем, который был передан этой функции.
Пример может быть таким:
class Test
{int i;};
std::unique_ptr<Test> foo1()
{
std::unique_ptr<Test> res(new Test);
return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
// return t; // this will produce an error!
return std::move(t);
}
//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));
Я хотел бы упомянуть один случай, когда вы должны использовать std::move(), иначе он выдаст ошибку. Случай: Если тип возвращаемого значения функции отличается от типа локальной переменной.
class Base { ... };
class Derived : public Base { ... };
...
std::unique_ptr<Base> Foo() {
std::unique_ptr<Derived> derived(new Derived());
return std::move(derived); //std::move() must
}
Ссылка: https://www.chromium.org/developers/smart-pointer-guidelines
Я знаю, что это старый вопрос, но я думаю, что здесь отсутствует важная и четкая ссылка.
Из https://en.cppreference.com/w/cpp/language/copy_elision :
(Начиная с C++11) В операторе return или выражении throw, если компилятор не может выполнить исключение копирования, но условия для исключения копирования выполнены или будут выполнены, за исключением того, что источник является параметром функции, компилятор попытается использовать конструктор перемещения, даже если объект обозначен lvalue; подробности см. в заявлении о возврате.