为什么`basic_ostream`的`operator<<`中的rvalue重载返回一个左值引用?

26

§27.7.3.9 定义了以下重载形式的 operator<<

template <class charT, class traits, class T>
  basic_ostream<charT, traits>&
  operator<<(basic_ostream<charT, traits>&& os, const T& x);

效果: os << x
返回: os

(§27.7.2.6 定义了 operator>> 的右值重载函数。)
基本上,它只是转发到左值重载函数。我认为这个重载函数相当危险(事实上,istream 版本比 ostream 版本更加危险),请考虑以下内容:

#include <sstream>
#include <iostream>

int main(){
  auto& s = (std::stringstream() << "hi there!\n");
  std::cout << s.rdbuf(); // oops
}

Ideone上的实时示例(未定义行为的完美示例。在我的MSVC10上没有输出任何内容)。

上面的示例看起来可能是刻意构造的,但在通用代码或将 (std::stringstream() << "text") 传递给提供 lvalue 和 rvalue 重载的函数且根据重载以不同方式存储 std::ostreamstd::istream 时,很容易陷入这种情况。

现在,对于返回 basic_ostream<charT, traits>&& 并指定以下内容,是否有反对意见?

返回: move(os)

(对于 basic_istream 同样适用。)

我是否忽略了什么?在当前状态下,在我看来,它看起来危险而且像一个缺陷。我浏览了LWG问题列表并找到了这个建议(嗨@HowardHinnant!)。它确实返回一个 rvalue,但仅为了能够链接该特殊运算符,而不是专门解决我上面描述的安全问题(尽管它肯定解决了这个问题)。此外,它被标记为关闭状态,并重新考虑下一个标准。因此,我想在这里问:

为什么上述重载返回左值引用?有什么好理由吗?


出于某种原因,我见过的所有 MSVC 都似乎允许将 rvalue 绑定到非 const 引用,因此对于 MSVS 返回 rvalue 不会改变任何内容。 - Terenty Rezman
1个回答

16

这是一个缺陷,是我的错,对此感到抱歉。LWG 1203(非常感谢您为我找到它!:-))是我目前认为是“rvalue-stream-inserter”的正确修复方式。但请注意,您仍然可能会遇到问题并且需要小心处理:

auto&& s = (std::stringstream() << "hi there!\n");
std::cout << s.rdbuf(); // oops
尽管在上面的代码中至少有一个小细节是显而易见的(因为有 && ),表明你正在做一些不该做的事情。

3
那么,不返回引用而只返回一个被移动的 basic_ostream<charT,traits> 对象是否更好呢?这将禁用左值引用绑定,并通过 auto &&auto const& 实现正确的生命周期延长语义,同时仍能像您建议的那样链接该运算符。 - Xeo
那样会更安全。但我不确定性能损失是否值得。虽然移动流相对便宜,但我不确定在每个链接操作上执行此操作是否足够便宜。但这是需要进一步思考的事情... - Howard Hinnant
我认为优化应该在这里启动,省略几乎所有的移动(尽管依赖于此可能不好)。然而,我注意到另一个小缺点 - std::ostream 的用户定义派生类也需要可移动,并且可能会包含一个固定大小的缓冲区,需要 O(N) 时间来“移动”。再次强调,优化很可能会使其成为无操作,但如果它们没有启动,那么问题就更严重了。 - Xeo

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