从函数返回unique_ptr

时间:2010-11-30 17:44:37

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

6 个答案:

答案 0 :(得分:192)

  

语言规范中是否还有一些其他条款可供使用?

是的,见12.8§34和§35:

  

当满足某些条件时,允许实现省略类对象的复制/移动构造[...]   复制/移动操作的省略,称为复制省略,是允许的[...]   在具有类返回类型的函数的return语句中,当表达式是其名称时   非易失性自动对象,具有与函数返回类型相同的cv非限定类型[...]

     

当满足复制操作的省略标准并且由左值指定要复制的对象时,   选择复制构造函数的重载决策首先执行,好像该对象是由右值指定的。


只是想再增加一点,按值返回应该是默认选择,因为在最坏的情况下返回语句中的命名值,即在C ++ 11,C ++ 14和C ++中没有elisions 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)。

答案 1 :(得分:89)

这绝不是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;
}

答案 2 :(得分:35)

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

unique_ptr::unique_ptr(unique_ptr && src);

右值引用(双号&符号)仅绑定到右值。这就是当您尝试将左值unique_ptr传递给函数时出现错误的原因。另一方面,从函数返回的值被视为右值,因此自动调用移动构造函数。

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

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

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

答案 3 :(得分:9)

我认为在Scott Meyers的第25项中完美地解释了这一点。 Effective Modern C++。这是一个摘录:

  

标准祝福RVO的部分接着说,如果满足RVO的条件,但编译器选择不执行复制省略,则返回的对象必须被视为右值。实际上,标准要求在允许RVO时,要么进行复制省略,要么将std::move隐式应用于要返回的本地对象。

此处, RVO 指的是返回值优化,如果满足RVO的条件,则 表示返回在其中声明的本地对象您期望执行 RVO 的功能,通过引用标准(这里的本地对象包括创建的临时对象)在本书的第25项中也很好地解释了这一功能通过退货声明)。摘录中最大的一点是复制省略或std::move隐式应用于返回的本地对象。 Scott在第25项中提到,当编译器选择不删除副本并且程序员不应该明确地这样做时,隐式应用std::move

在您的情况下,代码显然是 RVO 的候选者,因为它返回本地对象p并且p的类型与返回类型相同,这导致复制省略。如果编译器选择不删除副本,无论出于何种原因,std::move都会进入行1

答案 4 :(得分:2)

我在其他答案中没有看到的一件事是澄清another answers返回已在函数中创建的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));

答案 5 :(得分: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