【问题标题】:When should I return by value, as opposed to returning a unique pointer我什么时候应该按值返回,而不是返回唯一指针
【发布时间】:2017-11-10 20:26:37
【问题描述】:

我想知道的是,按值返回 Cat 与返回 std::unique_ptr<Cat> 在传递、内存管理和实际使用方面有何不同。

内存管理明智,它们不一样吗?因为值返回的对象和包裹在 unique_ptr 中的对象都会在超出范围后触发其析构函数?

那么,你将如何比较两段代码:

Cat catFactory(string catName) {
    return Cat(catName);
}

std::unique_ptr<Cat> catFactory(string catName) {
    return std::unique_ptr(new Cat(catName));
}

【问题讨论】:

  • 值语义更容易理解和阅读。如果Cat 可以移动构造,则没有理由制作指针。另外,unique_ptr 不能被复制。
  • 补充一点,unique_ptr 值可以从指针中释放出来,用作动态分配的对象。正如@HenriMenke 指出的那样,价值通常是他们要走的路,但是使用unique_ptr 允许您提取对象以供在ptr 范围之外使用。
  • 感谢您的意见,伙计们!是的,不实现移动构造函数和移动赋值可能会导致不必要地复制对象的大量开销。 uniq_ptr 的发布 api 似乎也是一个合法的差异化因素

标签: c++ c++11 memory-management unique-ptr return-by-value


【解决方案1】:

按值返回应被视为默认值。 (*) 偏离默认做法,返回std::unique_ptr&lt;Cat&gt;,需要说明理由。

返回指针的三个主要原因:

  1. 多态性。这是返回std::unique_ptr&lt;Cat&gt; 而不是Cat 的最佳理由:您实际上可能正在创建一个派生自Cat 的类型的对象。如果您需要这种多态性,您绝对需要返回某种指针。这就是工厂函数通常返回指针的原因。

  2. Cat 无法廉价移动或根本无法移动。 “固有地”不可移动的类型很少见;您通常应该尝试修复Cat,使其可以廉价移动。但当然Cat 可能是其他人拥有的类型,您不能向其添加移动构造函数(甚至可能是复制构造函数)。在这种情况下,您只能使用unique_ptr(并向所有者投诉)。

  3. 该函数可能会失败并且无法构造任何有效的Cat。在这种情况下,一种可能性是无论如何按值返回,但如果无法构造 Cat 则抛出异常;另一种是在 C++11/C++14 中,使函数返回 std::unique_ptr&lt;Cat&gt;,并在无法构造 Cat 时返回空指针。但是,在 C++17 中,您应该在这种情况下开始返回 std::optional&lt;Cat&gt; 而不是 std::unique_ptr&lt;Cat&gt;,以避免不必要的堆分配。

(*) 当被调用的函数需要它自己的值副本时,这也适用于 传递 对象,例如, 一个构造函数,该构造函数将从它的论据之一。按值接受对象并移动。

【讨论】:

  • 请注意,“按值传递和返回都应该被视为默认值”并不是真的。通过 const 引用传递通常比按值传递更便宜、更安全。
  • @mascoj 对不起,你是对的,我的意思是在函数需要自己的值副本的情况下按值传递。我会编辑。
  • @Brian 我认为这很好地回答了我的问题(据我所知,鉴于我不知道我不知道什么:D)。所以总的来说,在为程序员团队维护的项目设计某种 API 时,鉴于你提到的原因,你认为我应该坚持使用 std::unique_ptr 吗?
  • @user1113314:这听起来与这篇文章所说的相反。一般来说,在设计某种 API 时,应该使用通用机制:按值返回,除非按值返回是一个非常糟糕的主意。
【解决方案2】:

默认情况下,按值返回。

此规则的例外情况:

  1. Cat 需要存在于堆中,以便比触发其创建的代码更持久......但在这种情况下,它可能不应该是真正返回的 unique_ptr,而是 shared_ptr
  2. 您实际上并没有构建 Cat,而是访问可以被解释为 Cat 的东西;同样,在这种情况下,您可能不需要唯一指针,而是常规指针(或带有自定义删除器的唯一指针)。
  3. 多态性 - 如果它是一家工厂,并且 Cat 是它的产品之一,那么您可能还可以制作 Dog 和 Horse,它们都是 Animals,因此您将返回指向 Animal 的指针。这绝对是您会使用唯一指针的情况。
  4. 您的副本、赋值和/或移动构造函数中的黑暗巫术,这使得始终确保您只从远处戳您的猫非常重要。

我不同意@Brian 关于他建议的两个例外的回答:

  • 我建议不要使用指针返回类型,以便能够通过返回nullptr 来指示失败。未能返回有效值是异常的原因,即使您想避免它们 - 我建议返回 std::optional
  • 通常不需要移动构造函数来进行返回值优化 - 因此缺少移动构造函数不应成为返回指针的理由。

【讨论】:

  • 感谢您的观点。您能否举例说明第 2 点的含义?而且我也很想知道编译器移动优化,如果我们不需要为优化实现移动语义,为什么首先要有它们?也许@Brian 可以填补我们的空缺?
  • @user1113314:移动语义对于传递参数和使用函数的返回值非常有用,除了返回值优化。返回值优化不需要移动构造函数,因为返回值优化绕过了移动。
  • @user1113314:有时你甚至在你的工厂里也不能说Cat result{};。也许你,我不知道,只是反序列化一些字节流,你只知道在运行时这构成了一只猫。
【解决方案3】:

在内存管理方面它们完全不同。

当然,现在这些简单示例之间的实际功能差异非常小,假设移动语义可以使按值返回便宜(在第二个示例中,它们负责移动指针)。而且,当然,如果您立即让所有内容超出范围,这两个对象将同时被销毁。

但是动态分配的代码远不那么简单,并且添加了“为什么?”因素。

如果不检查函数返回后 将如何使用结果,您将无法真正进一步合理化差异。然后,所有关于自动与动态内存分配的典型考虑因素都会重新发挥作用。

总之,实际上并没有通用的、包罗万象的方法来告诉您工厂是应该动态分配还是按值返回。但是,为了简单起见,我个人更喜欢后者(除非您知道不能),特别是如果您的对象类型通常是可移动的(由于 RVO,这可能不会对函数本身产生太大影响,但可能会对您有所帮助在呼叫站点)。

【讨论】:

  • 感谢您为编写答案所付出的努力,但同时我认为您实际上并没有说太多。如果您能提供一些有助于以某种方式区分这两个选项的示例,我将不胜感激。
  • @user1113314:向您展示动态分配和非动态分配之间的区别超出了堆栈溢出问题的范围。请转至 C++ 书籍中的相关页面。
猜你喜欢
  • 2015-09-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-16
  • 2015-07-23
  • 2019-07-08
  • 2012-01-08
相关资源
最近更新 更多