从函数返回 unique_ptr

Posted

技术标签:

【中文标题】从函数返回 unique_ptr【英文标题】:Returning unique_ptr from functions 【发布时间】:2011-05-18 00:48:36 【问题描述】:

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 出现这个异常,因为返回值是一个临时对象,一旦函数退出就会被销毁,从而保证返回指针的唯一性。我很好奇这是如何实现的,是编译器中的特殊情况还是语言规范中的其他子句可以利用?

【问题讨论】:

假设,如果你正在实现一个 factory 方法,你希望 1 还是 2 来返回工厂的输出?我认为这将是 1 最常见的用法,因为通过适当的工厂,您实际上希望将构造事物的所有权传递给调用者。 @Xharlie ?他们都传递了unique_ptr 的所有权。整个问题是关于 1 和 2 是实现同一目标的两种不同方式。 在这种情况下,RVO 也发生在 c++0x 中,unique_ptr 对象的销毁将在 main 函数退出后执行一次,但不会在 foo 时执行退出。 【参考方案1】:

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

是的,请参阅 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 int2; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr


...

auto int_uptr = get_unique(); // <- 3

在编译时设置了标志,此函数中发生了两次移动(1 和 2),然后在(3)中发生了一次移动。

【讨论】:

@juanchopanza 你的意思是说foo() 确实也即将被销毁(如果它没有被分配给任何东西),就像函数中的返回值一样,因此它是有道理的C++ 在做unique_ptr&lt;int&gt; p = foo();? 时使用移动构造函数 这个答案说实现允许做某事......它没有说它必须,所以如果这是唯一相关的部分,那就意味着依赖这种行为是不可移植的。但我认为这是不对的。我倾向于认为正确的答案更多地与移动构造函数有关,如 Nikola Smiljanic 和 Bartosz Milewski 的回答中所述。 @DonHatch 它说在这些情况下“允许”执行复制/移动省略,但我们在这里不是在谈论复制省略。这是此处适用的第二个引用段落,它搭载了复制省略规则,但不是复制省略本身。第二段没有任何不确定性——它是完全可移植的。 @juanchopanza 我意识到现在已经是 2 年之后了,但你还觉得这是错的吗?正如我在之前的评论中提到的,这与复制省略无关。碰巧在可能应用复制省略的情况下(即使它不能与std::unique_ptr 一起应用),有一个特殊规则首先将对象视为右值。我认为这与 Nikola 的回答完全一致。 那么当我以与本示例完全相同的方式返回它时,为什么我的只移动类型(已删除的复制构造函数)仍然收到错误“尝试引用已删除的函数”?【参考方案2】:

这绝不是特定于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;

【讨论】:

@Fred - 好吧,不是真的。虽然p 不是临时的,但foo() 的结果,返回的是;因此它是一个右值并且可以移动,这使得main 中的赋值成为可能。我会说你错了,除了 Nikola 似乎将此规则应用于 p 本身,这是错误的。 正是我想说的,但找不到单词。我已经删除了答案的那部分,因为它不是很清楚。 我有一个问题:在原来的问题中,Line 1 和 Line 2 之间有什么实质性区别吗?在我看来是一样的,因为在main中构造p时,它只关心foo的返回类型,对吧? @HongxuChen 在该示例中绝对没有区别,请参阅已接受答案中标准的引用。 其实后面可以用p,只要给它赋值即可。在此之前,您不能尝试引用内容。【参考方案3】:

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

unique_ptr::unique_ptr(unique_ptr && src);

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

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

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

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

【讨论】:

我认为重点更多,为什么p - “显然”是一个 lvalue - 在 return 语句中被视为 rvalue return p;foo 的定义中。我认为函数本身的返回值可以“移动”这一事实没有任何问题。 把函数返回的值包装在std::move中是否意味着会被移动两次? @RodrigoSalazar std::move 只是从左值引用 (&) 到右值引用 (&&) 的一个花哨的转换。在右值引用上额外使用 std::move 将只是一个 noop【参考方案4】:

我认为 Scott Meyers 的Effective Modern C++ 的 item 25 完美地解释了这一点。摘录如下:

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

这里,RVO是指返回值优化如果满足RVO的条件表示返回内部声明的本地对象您希望执行 RVO 的函数,这在他的书的第 25 项中通过引用标准也得到了很好的解释(这里 local object 包括创建的临时对象通过 return 语句)。摘录中最大的收获是复制省略发生或std::move 隐式应用于返回的本地对象。 Scott 在第 25 项中提到,std::move 是在编译器选择不删除副本并且程序员不应该明确这样做时隐式应用的。

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

【讨论】:

【参考方案5】:

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

【讨论】:

answer by fredoverflow 中提到了 - 明确突出显示“自动对象”。引用(包括右值引用)不是自动对象。 @TobySpeight 好的,抱歉。我想我的代码只是一个澄清。 感谢您的回答!几天来我一直在尝试调试由此引起的问题,阅读这个答案让我意识到出了什么问题。【参考方案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

【讨论】:

以上是关于从函数返回 unique_ptr的主要内容,如果未能解决你的问题,请参考以下文章

如何从静态函数返回函数接口?

C 从函数返回指针

C 从函数返回指针

从函数内部的函数返回值

如何从Objective-C中的异步内部函数内部返回外部函数

如何从Objective-C中的异步内部函数内部返回外部函数