从函数返回unique_ptr

烂漫一生 提交于 2020-03-09 21:47:21

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


#1楼

这绝不是std::unique_ptr特有的,而是适用于任何可移动的类。 语言规则保证了这一点,因为您按价值返回。 编译器尝试取消副本,如果无法删除副本,则调用move构造函数,如果无法移动,则调用副本构造函数;如果无法复制,则编译失败。

如果您有一个函数接受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楼

语言规范中还有其他条款可利用吗?

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

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

当满足复制操作省略标准并由左值指定要复制的对象时,首先执行重载分辨率以选择要复制的构造函数, 就好像该对象由右值指定一样


只是想补充一点,按值返回应该是此处的默认选择,因为在最坏的情况下,即在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)发生了一次移动。


#3楼

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

unique_ptr::unique_ptr(unique_ptr && src);

右值引用(双“&”号)将仅绑定到右值。 这就是为什么在尝试将左值unique_ptr传递给函数时会出错的原因。 另一方面,将从函数返回的值视为右值,因此将自动调用move构造函数。

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

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

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


#4楼

我在其他答案中没有看到的一件事是 为了阐明另一个答案 ,返回在一个函数中创建的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));

#5楼

我认为这在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行。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!