【问题标题】:Is it reasonable to take std::istream&& as a argument?将 std::istream&& 作为参数是否合理?
【发布时间】:2019-06-10 18:17:26
【问题描述】:

我遇到过这样的代码:

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

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

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

我认为这没问题,因为parse 函数实际上并没有移动对象,所以没有切出子类型的危险。从parse 的角度来看,这基本上表现为正常的引用,只是对调用函数有一种可编译的注释,您必须放弃对流的所有权。

这段代码真的安全吗?还是有一些被忽视的微妙之处导致了这种危险?

【问题讨论】:

  • 毫无疑问,C++ 语言的律师对此会有一些话要说。同时,请您在问题的上下文中定义“好的”、“安全的”、“合理的”和“危险的”这些术语吗?一个人的白兰地是另一个人的毒药。
  • @SamVarshavchik 或强制程序员std::move 正在处理的输入流,这将表明之后没有人应该尝试从中提取。
  • @Caleth 在最初的“WTF”之后,您开始理解“哦,所以它会解析整个输入,因为我放弃了std::cin 的所有权,我不应该使用它后来。明白了。整洁”。至少在我看来。还要考虑std::stringstreams 和std::fstreams 的用法。
  • 那么有什么可能使std::cin 无法使用?考虑到流的概念,这方面对我来说特别没有任何意义。流可以位于 EOF。这不会使该流对象不可用。流可能处于不良状态。这不会使对象无法使用……
  • 但是,当实现实际上无法“利用”它时,将这种任意限制引入接口有什么意义!?

标签: c++ iostream rvalue-reference istream


【解决方案1】:

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

由于您将使对象基本上无法使用,并且您希望确保调用者不再使用该对象,因此使用右值引用在某种程度上强制执行此期望。它迫使调用者对你的函数做出承诺。未能做出承诺将导致编译器错误(假设没有另一个有效的重载)。无论您是否真的从该对象搬走都没有关系,只要原所有者同意放弃它的所有权即可。

【讨论】:

  • 基本同意,但我仍然不明白您为什么要对流这样做。除非存在 OP 未提及的域限制,否则它是不必要的限制。迈克尔的回答说得很好。
【解决方案2】:

An std::istream isn't moveable,所以这并没有实际的好处。

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

我可以有点看到使用这个来表示流正在逻辑上移动的理由,但我认为你必须区分“正在转移这件事的所有权”,和“我保留这个东西的所有权,但我会让你使用它的所有服务”。所有权转移在 C++ 中被很好地理解为一种约定,但实际上并非如此。当你的代码的用户不得不写parse(std::move(std::cin))时他们会怎么想?

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

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

【讨论】:

  • rvalue参考搬家不一定是夫妻。
  • @SergeyA 如果您不打算通过移动转移所有权(并且由于神秘的重载原因,您不想限制对右值表达式的调用),则看不到右值引用的目的。你能举例说明这在什么时候有用吗?
  • 你能举个例子”,好吧,问题中的代码......至少在我看来,一个人会转让所有权 (通过右值引用方式传递)将表明以后没有人应该尝试从中提取,我认为这是使用C++ 语法来表达意图的一种巧妙方法。但这只是我的看法。
  • 转让所有权在 C++ 中是一个非常明确的上下文,它通常涉及到移动。 std::istream 根本无法做到这一点。你可以让你的函数只接受一个右值来表示它将从流中读取所有数据,当然,但这不是 C++ 中转移所有权的意思,所以这在 IMO 中会很奇怪!我的意思是我可以有点看到它,但我看不出它值得。
  • @templatetypedef 仅来自派生类型 - 移动 ctor 受到保护。
【解决方案3】:

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

流是一个抽象概念。流抽象的目的是作为一个接口,人们可以通过它来消费输入,而不必知道数据来自哪里、生活在哪里,或者如何访问和管理它。如果parse() 的目的是解析来自任意源的输入,那么它不应该关心源的性质。如果它关心来源的性质,那么它应该请求特定类型的来源。恕我直言,这就是您的界面自相矛盾的地方。目前,parse() 采用任意来源。界面说:我接受你给我的任何流,我不在乎它是如何实现的。只要它是一个流,我就可以使用它。同时,它要求调用者放弃实际实现流的对象。接口要求调用者交出接口本身阻止接口后面的任何实现以任何方式知道、访问或使用的东西。例如,我如何让parse()std::ifstream 中读取?之后谁关闭文件?如果不能成为解析器。也不能是我,因为调用解析器迫使我交出对象。同时,我知道解析器甚至都不知道它必须关闭我交出的文件……

它最终仍然会做正确的事情,因为接口的实现不可能实际完成接口建议的操作,因此我的 std::ifstream 析构函数只会运行并关闭文件.但是,我们这样互相欺骗到底得到了什么!?当你永远不会去的时候,你答应过要接管这件物品,当我知道我将永远不得不去的时候,我承诺永远不会再碰那个东西……

【讨论】:

    【解决方案4】:

    您认为右值引用参数暗示“取得所有权”的假设是完全错误的。右值引用只是一种特定的引用,它有自己的初始化规则和重载解析规则。不多也不少。形式上,它与引用对象的“移动”或“取得所有权”没有特殊的关联。

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

    与您刚刚引用的内容类似的示例引用实际上存在于标准库本身中。它是 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 &lt;&lt; 的成员和非成员重载之间的行为差​​异

    #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
    }
    

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

    【讨论】:

    • 这些目的是什么?我认为这是这个问题的关键点。如果你能解释这个例子的目的是什么,以及它与 OP 的情况有何关系,那可能就是答案。
    猜你喜欢
    • 2020-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-15
    • 2016-12-01
    • 1970-01-01
    • 2013-10-30
    • 2019-02-28
    相关资源
    最近更新 更多