从函数返回 only_ptr

unique_ptr<T>不允许复制构造,而是支持 move 语义。然而,我可以从函数返回一个 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异常,因为返回值是一个临时对象,一旦函数退出就会销毁它,从而保证了返回指针的唯一性。我很好奇这是如何实现的,它是在编译器中特殊处理的,还是在语言规范中有其他的子句可以利用?

235144 次浏览

这并不特定于std::unique_ptr,而是适用于任何可移动的类。这是由语言规则保证的,因为你是通过值返回的。编译器尝试省略副本,如果不能删除副本,则调用移动构造函数,如果不能移动则调用复制构造函数,如果不能复制则编译失败。

如果你有一个接受std::unique_ptr作为参数的函数,你就不能将p传递给它。你必须显式地调用move构造函数,但在这种情况下,你不应该在调用bar()之后使用变量p。

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;
}

在语言规范中是否有其他子句可以利用?

是的,见12.8§34和§35:

当满足某些条件时,实现允许省略类对象的复制/移动结构[…] 这种复制/移动操作的省略,称为复制省略,是允许的[…] 在具有类返回类型的函数的返回语句中,当表达式为的名称时,则 一个非易失性自动对象,与函数返回类型[…]相同的cv- qualified类型

当复制操作的省略条件满足并且复制对象是由左值指定时, 重载解析首先执行就好像对象是由右值指定的以选择复制的构造函数


只是想再补充一点,按值返回应该是这里的默认选择,因为在最坏的情况下,return语句中的命名值,即在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没有传统的复制构造函数。相反,它有一个使用右值引用的“move构造函数”:

unique_ptr::unique_ptr(unique_ptr && src);

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

顺便说一下,这将正确工作:

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

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

我在其他答案中没有看到的一件事是为了澄清另一个答案返回在函数中创建的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的有效的现代c++25项中得到了完美的解释。以下是节选:

标准支持RVO的部分继续说,如果满足RVO的条件,但编译器选择不执行复制省略,则返回的对象必须被视为右值。实际上,标准要求当RVO被允许时,要么发生复制省略,要么隐式应用std::move到被返回的局部对象。

这里,RVO指的是返回值优化,而是否满足RVO的条件指的是返回你期望执行RVO的函数内部声明的局部对象,这在他的书的第25项中也通过引用标准很好地解释了(这里本地对象包括由return语句创建的临时对象)。这段摘录中最大的内容是要么发生复制省略,要么将std::move隐式应用于返回的局部对象。Scott在第25项中提到,当编译器选择不省略副本,而程序员不应该显式地这样做时,std::move是隐式应用的。

在你的例子中,代码显然是RVO的候选对象,因为它返回本地对象p,并且p的类型与返回类型相同,这将导致复制省略。如果编译器选择不省略副本,无论出于何种原因,std::move将被踢到1行。

我想提一个你必须使用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表达式中,如果编译器不能执行复制省略,但满足或将满足复制省略的条件(除非源是函数形参),即使对象由左值指定,编译器也将尝试使用move构造函数;详细信息请参见return语句。