从函数返回unique_ptr


367

unique_ptr<T>不允许复制构造,而是支持移动语义。但是,我可以unique_ptr<T>从函数返回a 并将返回的值分配给变量。

#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不调用复制构造函数并导致编译器错误呢?如果我不得不使用line,2那将会很有意义(使用line 2也可以,但是我们不需要这样做)。

我知道C ++ 0x允许发生此异常,unique_ptr因为返回值是一个临时对象,一旦函数退出,该对象将被销毁,从而保证了返回指针的唯一性。我很好奇这是如何实现的,它是在编译器中进行特殊处理还是在该语言规范中使用了其他条款?


假设地,如果要实现工厂方法,您是希望1还是2返回工厂的输出?我猜想这将是1的最常见用法,因为对于一个适当的工厂,您实际上希望将所构造的事物的所有权传递给调用方。
Xharlie 2015年

7
@Xharlie?他们都通过了所有权unique_ptr。整个问题是关于1和2是实现同一件事的两种不同方式。
Praetorian

在这种情况下,RVO也会在c ++ 0x中发生,对unique_ptr对象的销毁将是一次,该销毁在main函数退出后执行,而在foo退出时不会执行。
ampawd

Answers:


218

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

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

当满足一定的条件,实现允许省略类对象的复制/移动建设[...]复制/移动操作的这个省音,叫做复制省略,允许[...]在return语句当表达式是具有与函数返回类型相同的cv不合格类型的非易失性自动对象的名称时,具有类返回类型的函数[...]

当满足复制操作省略标准并由lvalue指定要复制的对象时,首先执行重载决议以选择要复制的构造函数,就好像该对象由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)发生了一次移动。


@juanchopanza实质上,您是说foo()确实也确实会被销毁(如果未分配给任何东西),就像函数中的返回值一样,因此C ++在执行操作时使用move构造函数有意义unique_ptr<int> p = foo();吗?
7cows 2013年

1
这个答案说一个实现被允许做某件事...没有说它必须做,所以如果这是唯一相关的部分,则意味着依靠这种行为是不可移植的。但是我认为那是不对的。我倾向于认为正确的答案与move构造函数有更多关系,如Nikola Smiljanic和Bartosz Milewski的答案所述。
唐·哈奇

6
@DonHatch它表示在这种情况下“允许”执行复制/移动省略,但是这里我们不谈论复制删除。这是引用的第二段,适用于复制省略规则,但不是复制省略本身。第二段没有不确定性-它是完全可移植的。
约瑟夫·曼斯菲尔德

@juanchopanza我知道现在已经2年了,但是您仍然觉得这是错误的吗?正如我在前面的评论中提到的,这与复制省略无关。碰巧的是,在可能应用复制省略(即使不能使用std::unique_ptr)的情况下,有一个特殊的规则首先将对象视为右值。我认为这完全符合Nikola的回答。
约瑟夫·曼斯菲尔德

1
那么,为什么以与本示例完全相同的方式返回仅移动类型(已删除副本构造函数)时仍出现错误“试图引用已删除的函数”错误?
DrumM

104

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

如果您有一个接受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;
}

3
@Fred-好吧,不是真的。尽管p不是临时的,但foo()返回的结果是;因此,它是一个右值并且可以移动,这使得赋值成为main可能。我会说你错了,除了尼古拉然后似乎将此规则应用于p错误的本身。
爱德华·斯特朗奇

正是我想说的,但找不到这些单词。由于答案不太清楚,因此我删除了该部分答案。
NikolaSmiljanić10年

我有一个问题:在原始问题中,“行” 1和“行” 之间是否有实质性区别2?在我看来,这是相同的,因为在构造pin时main,它仅关心的返回类型foo,对吗?
Hongxu Chen

1
@HongxuChen在该示例中,没有任何区别,请参阅已接受答案中来自标准的引用。
NikolaSmiljanić2014年

实际上,只要分配给p,便可以随后使用p。在此之前,您不能尝试引用其内容。
艾伦(Alan)

38

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

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


8
我认为重点还在于,为什么p- 可以“显然”将一个左值 - 在的定义中的return语句中视为右值。我认为函数本身的返回值可以被“移动”没有任何问题。return p;foo
CB Bailey 2010年

从函数返回的值包装在std :: move中是否意味着它将被移动两次?

3
@RodrigoSalazar std :: move只是从左值引用(&)到右值引用(&&)的幻想。在右值引用上对std :: move的多余使用将只是一个小问题
TiMoch 2013年

13

我认为这在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都会加入line 1


5

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

它在答案中由fredoverflow提及 -突出显示的“ 自动对象”。引用(包括右值引用)不是自动对象。
Toby Speight

@TobySpeight好的,抱歉。我想我的代码只是一个澄清。
v010dya
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.