从函数返回unique_ptr

Pra*_*ian 328 c++ unique-ptr c++11

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;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码按预期编译和工作.那么该行如何1不调用复制构造函数并导致编译器错误呢?如果我必须使用line 2而不是它有意义(使用line 2也可以,但我们不需要这样做).

我知道C++ 0x允许此异常,unique_ptr因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证返回指针的唯一性.我很好奇这是如何实现的,它是在编译器中特殊的,还是在语言规范中有一些其他条款可以利用?

fre*_*low 204

这个漏洞是否在语言规范中有其他一些条款?

是的,见12.8§34和§35:

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

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


只是想再增加一点,按值返回应该是默认选择,因为在最坏的情况下,返回语句中的命名值,即C++ 11,C++ 14和C++ 17中没有elisions的处理作为一个左值.因此,例如,以下函数使用-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
Run Code Online (Sandbox Code Playgroud)

在编译时设置标志,在此函数中发生两个移动(1和2),然后稍后移动(3).

  • @DonHatch它表示在这些情况下"允许"执行复制/移动省略,但我们不是在谈论复制省略.这是适用于此的第二个引用段落,它背负着复制省略规则,但不是复制省略本身.第二段没有不确定性 - 它完全是便携式的. (6认同)
  • 那么,为什么当我以与此示例完全相同的方式返回仅移动类型(已删除的复制构造函数)时,仍然会收到错误“尝试引用已删除的函数”? (2认同)

Nik*_*nić 98

这绝不具体std::unique_ptr,但适用于任何可移动的类.由于您按价值返回,因此语言规则保证了这一点.编译器尝试删除副本,如果无法删除副本则调用移动构造函数,如果无法移动则调用复制构造函数,如果无法复制则无法编译.

如果你有一个接受std::unique_ptr作为参数的函数,你将无法将p传递给它.您必须显式调用move构造函数,但在这种情况下,您不应在调用后使用变量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;
}
Run Code Online (Sandbox Code Playgroud)

  • @Fred - 好吧,不是真的.虽然`p`不是临时的,但`foo()`的结果是,返回的是; 因此它是一个rvalue并且可以移动,这使得"main"中的赋值成为可能.我说你错了,只不过尼古拉似乎把这条规则应用于`p`本身,这是错误的. (3认同)

Bar*_*ski 37

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

unique_ptr::unique_ptr(unique_ptr && src);
Run Code Online (Sandbox Code Playgroud)

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

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

bar(unique_ptr<int>(new int(44));
Run Code Online (Sandbox Code Playgroud)

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

  • 我认为重点是,为什么`p` - "显然"_lvalue_ - 在`foo`的定义中的return语句`return p;`中被视为_rvalue_.我认为函数本身的返回值可以"移动"这一事实并不存在任何问题. (8认同)
  • @RodrigoSalazar std :: move只是从左值引用(&)到右值引用(&&)的精彩转换.rvalue引用上std :: move的无关用法只是一个noop (3认同)

Dav*_*Lee 12

我认为在Scott Meyers的Effective Modern C++第25项中对它进行了完美的解释.这是一段摘录:

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

这里,RVO指的是返回值优化,如果满足RVO的条件,则意味着返回在函数内声明的本地对象,你希望做RVO,这在他的书的第25项中也很好地解释了标准(这里本地对象包括return语句创建的临时对象).摘录的最大内容是复制省略或者std::move隐式应用于返回的本地对象.Scott在第25项中提到,std::move当编译器选择不删除副本并且程序员不应该明确地这样做时,会隐式应用.

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


Vic*_*pta 10

我想提一下一种情况,您必须使用 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
}
Run Code Online (Sandbox Code Playgroud)

参考: https: //www.chromium.org/developers/smart-pointer-guidelines


v01*_*dya 7

我在其他答案中没有看到的一件事是澄清另一个答案,即返回在函数中创建的 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));
Run Code Online (Sandbox Code Playgroud)