【问题标题】:Why return by shared_ptr instead of by value when RVO could apply?当 RVO 可以应用时,为什么要按 shared_ptr 而不是按值返回?
【发布时间】:2020-06-06 19:49:42
【问题描述】:

我最近开始阅读“C++ Concurrency in Action”,我惊讶于我对完全独立于并发 API 的 C++ 特性的了解之多。不过,有一个例子我想不通:当 RVO 可以应用时,书中的几个并发数据结构示例返回 shared_ptr 而不是 value。 (不过,我绝对是 C++ 新手,所以也许真正的问题是我对 RVO 的理解。)

这本书对它的线程安全队列有这样的说法:“将std::shared_ptr<> 复制出内部std::queue<> 然后就不能抛出异常,所以wait_and_pop() 又是安全的”。该书似乎暗示,如果仅在修改队列后将弹出的值返回给调用者,则拥有数据值的内部队列而不是 shared_ptrs 的内部队列可能会导致问题。如果复制数据返回给调用者的过程中抛出异常,则刚刚弹出的数据会丢失,但堆栈仍然会被修改。

如果我误解了他们的意思,请让我直接引用教科书的内容:

“对于那些不知道这个问题的人,请考虑stack<vector<int>>。现在,向量是一个动态大小的容器,因此当您复制向量时,库必须从堆中分配更多内存才能复制内容。如果系统负载很重,或者存在严重的资源限制,则此内存分配可能会失败,因此 vector 的复制构造函数可能会抛出 std::bad_alloc 异常。如果向量包含很多元素,这种情况尤其可能发生。如果 pop() 函数被定义为返回弹出的值,以及将其从堆栈中移除,那么您就有一个潜在的问题:被弹出的值只有在堆栈被修改后才返回给调用者,但复制数据以返回给调用者可能会引发异常。如果发生这种情况,刚刚弹出的数据就会丢失;它已从堆栈中删除,但复制不成功! std::stack 接口的设计者很有帮助地将操作一分为二:获取顶部元素 (top()),然后将其从堆栈中移除 (pop()),这样如果您无法安全地复制数据,它留在堆栈上。如果问题是堆内存不足,也许应用程序可以释放一些内存并重试。”

pop() 函数所采取的步骤是:

  1. 获取最高值并将其存储为局部变量

  2. 从堆栈中弹出值

  3. 返回值

我的问题是:返回值优化怎么会出现这种异常情况?如果满足复制省略的条件,但编译器选择不执行复制省略,则必须处理返回的对象作为右值。实际上,标准要求当 RVO 被允许时,要么发生复制省略,要么将std::move() 隐式应用于返回的本地对象。上述情况绝对符合 RVO 的条件。因此,移动对象的成本要么是单次移动(没有 RVO,对象被视为右值),要么根本不移动(RVO)。 那么将值移出 shared_ptr 真的有必要吗?

【问题讨论】:

  • 这本书是否可能早于将移动语义引入 C++ 的时间?对stack<vector<int>> 的担忧对我来说似乎是错误的——确实可以而且相当容易地从这样的堆栈中弹出一个向量而不复制任何东西,而不需要任何内存分配。 std::vector的移动构造函数和移动赋值绝对不会抛出。
  • 比这更简单...如果类型的移动构造函数是 noexceptstd::vector 是从 C++17 开始的)那么你可以 auto val = std::move(the_stack.top()); the_stack.pop(); return val; 并且没有t 任何例外的可能性。 (请注意,stack::pop() 不是 noexcept,但你会遇到与 shared_ptrs 堆栈相同的潜在问题,所以它没有实际意义。)
  • @IgorTandetnik shared_ptr 是在 C++11 中与移动语义一起添加的,所以你要么两者都有,要么都没有。
  • 那本书现在已经很老了,不是吗?
  • 太棒了,谢谢。我才刚刚开始学习 C++,所以我不确定我是否完全误解了 RVO。这本书应该是最新的,因为我使用的是第二版(针对 C++17 更新)。不过,更新本书的人绝对有可能忘记更改移动语义的示例。

标签: c++ c++11 concurrency


【解决方案1】:

简短的回答是因为您的案例不是RVO
返回值优化发生在在 return 语句中创建对象时。例如:

T foo() { return T(); }

虽然你的情况是这样的:

T pop() {
    T temp = stack_storage.front();
    stack_storage.erase(stack_storage.begin());
    return temp;
}

如您所见,return 对象不是在 return 语句中创建的。
这种情况称为命名返回值优化(NRVO)。

根据标准,RVO 是编译器必须的,而NRVO 不是。

更多关于copy elision

【讨论】:

  • 很抱歉给您带来了困惑!当我说“RVO”时,我并没有区分 C++ 的 RVO/NRVO。我的意思是“返回值优化”作为一种通用的、与语言无关的编译器技术。这与我的最后一段有关。如果您查看答案中链接的“复制省略”页面上的示例,您会看到它引用了这样一个事实:如果禁用返回值优化,则会调用移动构造函数。这就是我的问题所在。 :)
猜你喜欢
  • 2017-11-10
  • 2014-08-26
  • 1970-01-01
  • 1970-01-01
  • 2012-11-13
  • 1970-01-01
  • 2016-07-22
  • 2013-06-08
  • 2011-02-04
相关资源
最近更新 更多