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这个异常,因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证了返回指针的唯一性。我很好奇这是如何实现的,它在编译器中是特殊的,还是在语言规范中有一些其他的子句,这利用了?


这并不特定于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:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object [...] This elision of copy/move operations, called copy elision, is permitted [...] in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type [...] When the criteria for elision of a copy operation are met and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.


只是想再补充一点,按值返回应该是这里的默认选择,因为在最坏的情况下,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是在函数中创建的,和给该函数的是有区别的。

例子可以是这样的:

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的条件,但编译器选择不执行复制省略,则返回的对象必须被视为右值。实际上,标准要求当允许RVO时,要么发生复制省略,要么隐式应用std::move到返回的局部对象。

Here, RVO refers to return value optimization, and if the conditions for the RVO are met means returning the local object declared inside the function that you would expect to do the RVO, which is also nicely explained in item 25 of his book by referring to the standard (here the local object includes the temporary objects created by the return statement). The biggest take away from the excerpt is either copy elision takes place or std::move is implicitly applied to local objects being returned. Scott mentions in item 25 that std::move is implicitly applied when the compiler choose not to elide the copy and the programmer should not explicitly do so.

在您的例子中,代码显然是RVO的候选对象,因为它返回局部对象p,而且p的类型与返回类型相同,这将导致复制省略。如果编译器选择不省略副本,不管出于什么原因,std::move将被踢到第1行。


我想提到一种情况,你必须使用std::move(),否则它会给出一个错误。 Case:如果函数的返回类型与局部变量的类型不同。

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语句。