434

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

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

4

6 回答 6

254

语言规范中是否还有其他条款可以利用?

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

当满足某些条件时,允许实现省略类对象的复制/移动构造 [...] 这种复制/移动操作的省略,称为复制省略,在返回语句中被允许 [...]具有类返回类型的函数,当表达式是具有与函数返回类型相同的 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)中发生了一次移动。

于 2010-11-30T18:06:56.860 回答
120

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

如果您有一个接受std::unique_ptr作为参数的函数,您将无法将 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;
}
于 2010-11-30T17:48:39.980 回答
42

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

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

于 2010-11-30T23:00:27.760 回答
19

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

于 2017-10-04T18:38:45.720 回答
9

我在其他答案中没有看到的一件事是为了澄清另一个答案,即返回已在函数中创建的 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));
于 2017-07-03T16:06:44.533 回答
6

我想提一个必须使用 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

于 2020-06-22T12:42:09.240 回答