为什么可以在不使用std::move的情况下返回std::unique_ptr?

490
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还是2返回工厂的输出?我认为这将是1最常见的用法,因为使用适当的工厂,您实际上希望构造的物品所有权传递给调用者。 - Xharlie
7
@Xharlie?它们都传递了unique_ptr的所有权。整个问题是关于1和2是实现相同目标的两种不同方式。 - Praetorian
1
在这种情况下,RVO 在 c++0x 中也会发生,unique_ptr 对象的销毁将在 main 函数退出后进行一次,而不是在 foo 退出时进行。 - ampawd
7个回答

283

这里使用了语言规范中的另一个条款,你可以参考12.8 §34和§35:

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

当符合省略复制操作的条件并且要复制的对象由左值指定时,会首先进行重载解析以选择用于复制的构造函数,就像对象是由右值指定一样


还想补充一点,返回值应该默认选择按值返回,因为在 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)。


1
这个答案说一个实现允许做某事...它并没有说它必须这样做,所以如果这是唯一相关的部分,那就意味着依赖这种行为是不可移植的。但我认为这不对。我倾向于认为正确的答案与移动构造函数有更多关系,就像Nikola Smiljanic和Bartosz Milewski的答案中描述的那样。 - Don Hatch
7
@DonHatch说,它说在那些情况下执行复制/移动省略是“允许”的,但我们这里不是在谈论复制省略。适用于此处的是第二个引用段落,它依附于复制省略规则,但并不是复制省略本身。第二段没有任何不确定性- 它完全可移植。 - Joseph Mansfield
@juanchopanza 我知道现在已经过去了2年,但是你是否仍然认为这是错误的?正如我在之前的评论中提到的那样,这不是关于复制省略的问题。恰好在可能适用复制省略的情况下(即使不能使用std::unique_ptr),有一个特殊规则首先将对象视为右值。我认为这完全符合Nikola所回答的内容。 - Joseph Mansfield
1
这种复制省略被称为RVO或返回值优化。在C++11之前就已经存在了。请参阅https://en.wikipedia.org/wiki/Copy_elision#Return_value_optimization - jaques-sam
2
为什么我在返回移动语义类型(已删除复制构造函数)时,按照这个例子的方式完全相同,仍然会收到“尝试引用已删除的函数”的错误? - jaques-sam
显示剩余5条评论

141
这并不是特定于std::unique_ptr,而是适用于任何可移动的类。由于您是按值返回,语言规则保证了它。编译器尝试消除副本,如果无法消除,则调用移动构造函数,如果无法移动,则调用复制构造函数,如果无法复制,则编译失败。
如果您有一个接受std::unique_ptr作为参数的函数,则无法将p传递给它。您必须显式调用移动构造函数,但在此情况下,在调用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;
}

3
@Fred - 其实不完全是这样。尽管 p 不是一个临时变量,但 foo() 的返回结果是一个临时值(rvalue),因此可以被移动,这使得在 main 中的赋值成为可能。我想说你的观点是错误的,但 Nikola 似乎将这个规则应用于 p 本身,这一点是错误的。 - Edward Strange
正是我想说的,但找不到措辞。由于那部分回答不太清晰,我已将其删除。 - Nikola Smiljanić
我有一个问题:在原始问题中,第1行和第2行之间是否存在实质性的区别?在我的看法中,它们是相同的,因为在main函数中构造p时,只关心foo的返回类型,对吗? - Hongxu Chen
1
@HongxuChen 在这个例子中完全没有区别,可以看一下被接受的答案中标准的引用。 - Nikola Smiljanić
1
实际上,只要你给它赋值,之后就可以使用p了。在此之前,你不能尝试引用其内容。 - Alan
即使RVO仍然需要std::unique_ptr的拷贝构造函数,但它已被删除以成为移动语义,不是吗? - jaques-sam

46

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

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


9
我认为重点在于为什么p(显然是一个_lvalue_)可以在foo函数定义中的返回语句return p;中被视为_rvalue_。我不认为该函数自身的返回值有任何问题可以“移动”。 - CB Bailey
将函数返回值用std::move包装,这意味着它会被移动两次吗? - user712850
4
@RodrigoSalazar:std::move 只是将左值引用(&)转换成右值引用(&&)的一种高级类型转换。在右值引用上过度使用 std::move 将只是一个空操作。 - TiMoch

27

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

标准中赞成RVO的部分继续说,如果满足了RVO的条件,但编译器选择不执行复制省略,则必须将返回的对象视为rvalue。实际上,标准要求在允许RVO时,要么执行复制省略,要么对返回的局部对象隐式应用std::move

这里,RVO指的是返回值优化,而如果满足RVO的条件意味着返回函数内声明的局部对象,你期望做RVO,这也是他在书中通过参照标准(这里的局部对象包括返回语句创建的临时对象)很好地解释的。从摘录中最重要的收获是要么执行复制省略,要么对返回的局部对象隐式应用std::move。Scott在第25项中提到,当编译器选择不省略复制时,会隐式应用std::move,程序员不应该显式这样做。

在您的情况下,代码显然是 RVO 的候选对象,因为它返回局部对象 p,并且 p 的类型与返回类型相同,这导致了副本省略。如果编译器选择不省略副本,出于任何原因,std::move 将会进入第一行。

这是RVO吗?在《C++程序设计语言第四版》的第113页上显示了一行代码return unique_ptr<X>{ new X{i} };,我认为这是纯粹的移动语义,而不是RVO。如果将这行代码改成以下两行,是否会变成RVO呢?unique_ptr<X> mypX{ new X{i} }; return mypX; - Will

10

其他回答中没有提到的一点是为了澄清其他回答,在返回在函数内创建的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));

1
fredoverflow 的回答 中提到了 "自动对象",明确指出引用(包括右值引用)不是自动对象。 - Toby Speight
@TobySpeight 好的,抱歉。我想我的代码只是一个澄清。 - v010dya
谢谢您的回答!我已经试图调试由此引起的问题好几天了,阅读这个答案让我意识到了问题所在。 - Nick Alger
我可以问一下为什么在foo2中需要使用std::move()吗? - Hua
@Hua unique_ptr 无法被复制,只能被移动。 - v010dya

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
}

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

(提示:该链接为英文页面,可能需要使用翻译工具)

1
但是如果我删除std::move,编译时就不会出现错误。如果复制构造函数无法调用,编译器将尝试调用移动构造函数。因此,在这里使用std::move可能是多余的。 - Jllobvemy
1
似乎您所引用的文档并未提到在此情况下应使用std::move()。我认为恰恰相反。 - shargors

3

我知道这是一个老问题,但我认为这里缺少了一个重要而清晰的参考。

来自https://en.cppreference.com/w/cpp/language/copy_elision

(自C++11以来)在return语句或throw表达式中,如果编译器无法执行复制省略,但满足或将满足复制省略的条件,除了源是函数参数之外,编译器将尝试使用移动构造函数,即使对象由左值指定;有关详细信息,请参见return语句。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接