从函数返回unique_ptr

unique_ptr<T>不允许复制构造,而是支持移动语义。 然而,我可以从一个函数返回一个unique_ptr<T> ,并将返回的值赋给一个variables。

 #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行如何不调用复制构造函数并导致编译器错误呢? 如果我不得不使用第二行,那么它是有道理的(使用第二行也是如此,但我们并不需要这样做)。

我知道C ++ 0x允许这个exception为unique_ptr因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证了返回指针的唯一性。 我很好奇这是如何实现的,在编译器中是特殊的,还是在语言规范中有一些其他的子句呢?

这个漏洞在语言规范里还有其他的一些条款吗?

是的,请参阅12.8§34和§35:

当符合某些标准时,允许实现省略类对象的复制/移动构造[…] […]复制/移动操作的复制,称为复制省略 ,允许在返回语句中具有类返回types的函数, 当expression式是具有与函数返回types相同的cv不合格types的非易失性自动对象的名称 […]

当符合复制操作的条件被满足并且要被复制的对象被左值指定时,首先执行用于select副本的构造函数的重载parsing, 就好像对象被右值指定一样


只是想再补充一点,那就是按值返回应该是默认的select,因为在最坏的情况下,在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)之后移动一个动作。

这不是unique_ptr特有的,而是适用于任何可移动的类。 这是由语言规则保证,因为你正在返回的价值。 编译器会尝试去除副本,如果无法移除副本,则调用移动构造函数;如果无法移动,则调用复制构造函数;如果无法移动,则无法编译。

如果你有一个接受unique_ptr作为参数的函数,你将无法将p传递给它。 您将不得不显式调用移动构造函数,但在这种情况下,您不应该在调用bar()之后使用variablesp。

 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 } 

unique_ptr没有传统的拷贝构造函数。 相反,它有一个使用右值引用的“移动构造函数”:

 unique_ptr::unique_ptr(unique_ptr && src); 

右值引用(双和号)只会绑定到右值。 这就是为什么当你尝试将一个左值unique_ptr传递给一个函数时,你会得到一个错误。 另一方面,从函数返回的值被视为右值,因此移动构造函数被自动调用。

顺便说一句,这将正常工作:

 bar(unique_ptr<int>(new int(44)); 

这里的临时unique_ptr是一个右值。

有一件事我没有看到其他的答案是 为了澄清另一个答案 ,即在函数中创build的返回std :: unique_ptr与已经赋予该函数的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)); 

我认为Scott Meyers的Effective Modern C ++第 25项完全解释了这个问题。 这是一个摘录:

标准的祝福RVO的部分继续说,如果符合RVO的条件,但编译器select不执行复制省略,被返回的对象必须被视为一个右值。 实际上,标准要求当RVO被允许时,复制elision发生或者std::move被隐式地应用到返回的本地对象上。

在这里, RVO是指返回值优化如果符合RVO的条件意味着返回在你期望执行RVO的函数内声明的本地对象,这也在他的书的第25项中很好地解释标准(这里的本地对象包含由return语句创build的临时对象)。 摘录最大的缺点是要么复制elision发生或std::move隐式应用于返回的本地对象 。 Scott在第25项中提到,当编译器select不删除拷贝时, std::move是隐式应用的,程序员不应该明确地这样做。

在你的情况下,代码显然是RVO的候选者,因为它返回本地对象pp的types与返回types相同,这导致了复制的省略。 如果编译器select不删除副本,无论出于何种原因, std::move将会被踢入第1行。