将 std::istream&& 作为参数是否合理?

Is it reasonable to take std::istream&& as a argument?

提问人:spraff 提问时间:1/16/2019 更新时间:1/17/2019 访问量:770

问:

我遇到了这样做的代码:

SomeObject parse (std::istream && input) {....

参数是右值引用,这通常意味着函数旨在获取参数的所有权。这并不完全是这里发生的事情。input

该函数将完全消耗输入流,并且它需要右值引用,因为调用代码将放弃 的所有权,因此这是一个信号,表明输入流将不可用。parseistream

我认为这没关系,因为由于该函数实际上不会移动对象,因此不存在切出子类型的危险。从的角度来看,这基本上是一个普通的引用,只是对调用函数有一种可编译的注释,你必须放弃流的所有权。parseparse

这段代码真的安全吗?还是有一些被忽视的微妙之处使这变得危险?

C++ IOstream 右值引用 iStream

评论

6赞 Robert Harvey 1/16/2019
毫无疑问,C++语言律师对此有几句话要说。同时,您能否在问题的上下文中定义“好”、“安全”、“合理”和“危险”这些术语?一个人的干邑白兰地是另一个人的毒药。
1赞 Fureeish 1/17/2019
@SamVarshavchik或强制程序员进入已处理的输入流,这将表明没有人应该在事后尝试从中提取。std::move
1赞 Fureeish 1/17/2019
@Caleth,在最初的“WTF”之后,你开始明白“哦,所以它会解析整个输入,因为我放弃了所有权,我不应该在之后使用它。明白了。整洁”。至少在我看来。还要考虑 s 和 s 的用法。std::cinstd::stringstreamstd::fstream
1赞 Michael Kenzel 1/17/2019
那么,它能做些什么来使它无法使用呢?考虑到流的概念,这方面对我来说尤其没有任何意义。流可以位于 EOF。这不会使该流对象不可用。流可能处于错误状态。这并不会使对象无法使用......std::cin
1赞 Michael Kenzel 1/17/2019
但是,当实现实际上无法做任何事情来“利用”它时,将这种任意限制引入接口有什么意义呢!?

答:

6赞 Lightness Races in Orbit 1/17/2019 #1

std::istream 不可移动,因此没有任何实际好处。

这已经表明该事物可能被“修改”,而不会暗示您正在转移对象的所有权(您没有这样做,也无法这样做)。std::istream

可以看出用它来说明流在逻辑上被移动背后的基本原理,但我认为你必须区分“这个东西的所有权正在转移”和“我保留这个东西的所有权,但我会让你消费它的所有服务”。所有权转让在 C++ 中被很好地理解为一种约定,但事实并非如此。当你的代码的用户必须编写代码时,他们会怎么想?parse(std::move(std::cin))

不过,你的方式并不“危险”;您将无法对该右值引用执行任何操作,而对左值引用则无法执行任何操作。

只采用左值引用会更加自我记录和传统。

评论

3赞 SergeyA 1/17/2019
参考和搬家不一定是彼此结婚。rvalue
0赞 Lightness Races in Orbit 1/17/2019
@SergeyA 如果您不打算通过移动转移所有权,则看不到右值引用的用途(并且您不希望出于晦涩的重载原因限制对右值表达式的调用)。您能举例说明这何时有用吗?
2赞 Fureeish 1/17/2019
"你能举个例子吗“,好吧,问题中的代码......至少在我看来,一个人将转移输入流的所有权(通过右值引用手段传递)这一事实将表明,没有人应该在事后尝试从中提取它,我认为这是一种使用语法来表达意图的巧妙方式。但这只是我的看法。C++
1赞 Lightness Races in Orbit 1/17/2019
转让所有权在 C++ 中是一个定义明确的上下文,它通常涉及移动。你根本无法用 .当然,你可以让你的函数只接受一个右值来表示它将从流中读取所有数据,但这不是转移所有权在 C++ 中的意思,所以这将是非常奇怪的 IMO!我的意思是我能看到它,但我看不出它是值得的。std::istream
1赞 Lightness Races in Orbit 1/17/2019
@templatetypedef 仅从派生类型内部 - 移动 ctor 受到保护。
6赞 François Andrieux 1/17/2019 #2

std::move只是从对象生成右值引用,仅此而已。右值的本质是这样的,你可以假设在你完成它之后,没有人会关心它的状态。 然后用于允许开发人员对具有其他值类别的对象做出承诺。换句话说,在有意义的上下文中调用等同于说“我保证我不再关心这个对象的状态”。std::movestd::move

由于您将使对象基本上不可用,并且您希望确保调用方不再使用该对象,因此使用右值引用在某种程度上强制执行此期望。它迫使调用方向你的函数做出该承诺。未能做出承诺将导致编译器错误(假设没有其他有效的重载)。你是否真的搬离了这个对象并不重要,重要的是原来的所有者已经同意放弃它的所有权。

评论

0赞 Lightness Races in Orbit 1/17/2019
基本上同意,但我仍然不明白你为什么要对流这样做。除非存在 OP 未提及的领域限制,否则这是不必要的限制。迈克尔的回答说得非常好。
2赞 Michael Kenzel 1/17/2019 #3

从某种意义上说,您在这里尝试做的事情并不“危险”,因为鉴于当前的接口,似乎没有任何情况,在这种情况下,此处采用右值引用必然会导致未定义的行为,而采用左值引用则不会。但是恕我直言,整个装置的语义充其量是非常值得怀疑的。调用代码“放弃所有权”但同时“不转让所有权”是什么意思?返回后谁“拥有”流!?究竟在什么方面使流“无法使用”?如果在整个流被“消耗”之前由于某些错误而解析失败怎么办?那么流“不可用”了吗!?没有人可以尝试阅读其余部分吗?在这种情况下,“所有权”是否以某种方式“归还”给调用代码?std::istreamparse()parse()

流是一个抽象的概念。流抽象的目的是充当一个接口,通过该接口,人们可以使用输入,而不必知道数据的来源、存在或如何访问和管理数据。如果目的是解析来自任意源的输入,那么它不应该关注源的性质。如果它涉及来源的性质,那么它应该请求特定类型的来源。恕我直言,这就是您的界面自相矛盾的地方。目前,采用任意来源。界面说:我接受你给我的任何流,我不在乎它是如何实现的。只要它是流,我就可以使用它。同时,它要求调用方放弃实际实现流的对象。接口要求调用方交出接口本身阻止接口后面的任何实现以任何方式了解、访问或使用的内容。例如,我将如何从?之后谁会关闭文件?如果不能成为解析器。它也不能是我,因为调用解析器迫使我交出对象。同时,我知道解析器甚至永远不知道它必须关闭我交出的文件......parse()parse()parse()std::ifstream

它最终仍然会做正确的事情,因为接口的实现不可能真正完成接口建议它会做的事情,所以我的析构函数将运行并关闭文件。但是,我们这样互相撒谎究竟得到了什么!?你答应过在你永远不会去的时候接管这个物体,我答应过再也不碰那个东西,当我知道我永远必须这样做时......std::ifstream

2赞 AnT stands with Russia 1/17/2019 #4

你认为右值参考参数意味着“获得所有权”的假设是完全不正确的。右值引用只是一种特定的引用,它自带初始化规则和重载解析规则。不多也不少。从形式上讲,它与引用对象的“移动”或“所有权”没有特别的亲和力。

诚然,对移动语义的支持被认为是右值引用的主要目的之一,但您仍然不应该认为这是它们的唯一目的,并且这些功能在某种程度上是不可分割的。就像任何其他语言功能一样,它可能允许大量完善的替代惯用用法。

与您刚才引用的类似示例引用实际上存在于标准库本身中。这是 C++11(和 C++17,取决于一些细微差别)中引入的额外重载

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os, 
                                            const T& value );

template< class CharT, class Traits, class T >
basic_istream<CharT,Traits>& operator>>( basic_istream<CharT,Traits>&& st, T&& value );

它们的主要目的是“弥合”成员和非成员重载之间行为的差异operator <<

#include <string>
#include <sstream>

int main() 
{
  std::string s;
  int a;

  std::istringstream("123 456") >> a >> s;
  std::istringstream("123 456") >> s >> a;
  // Despite the obvious similarity, the first line is well-formed in C++03
  // while the second isn't. Both lines are well-formed in C++11
}

它利用了这样一个事实,即右值引用可以绑定到临时对象,并且仍然将它们视为可修改的对象。在这种情况下,右值引用用于与移动语义无关的目的。这是完全正常的。

评论

0赞 Lightness Races in Orbit 1/17/2019
这些目的是什么?我认为这是这个问题的关键点。如果你能解释这个例子中的目的,以及它与OP的情况有什么关系,那可能就是答案。