【问题标题】:Is the inconsistency of C++'s istream::eof() a bug in the spec or a bug in the implementation?C++ 的 istream::eof() 的不一致是规范中的错误还是实现中的错误?
【发布时间】:2012-10-23 15:37:44
【问题描述】:

以下程序演示了 std::istream(特别是在我的测试代码中,std::istringstream)设置 eof() 的方式不一致。

#include <sstream>
#include <cassert>

int main(int argc, const char * argv[])
{
    // EXHIBIT A:
    {
        // An empty stream doesn't recognize that it's empty...
        std::istringstream stream( "" );
        assert( !stream.eof() );        // (Not yet EOF. Maybe should be.)
        // ...until I read from it:
        const int c = stream.get();
        assert( c < 0 );                // (We received garbage.)
        assert( stream.eof() );         // (Now we're EOF.)
    }
    // THE MORAL: EOF only happens when actually attempting to read PAST the end of the stream.

    // EXHIBIT B:
    {
        // A stream that still has data beyond the current read position...
        std::istringstream stream( "c" );
        assert( !stream.eof() );        // (Clearly not yet EOF.)
        // ... clearly isn't eof(). But when I read the last character...
        const int c = stream.get();
        assert( c == 'c' );             // (We received something legit.)
        assert( !stream.eof() );        // (But we're already EOF?! THIS ASSERT FAILS.)
    }
    // THE MORAL: EOF happens when reading the character BEFORE the end of the stream.

    // Conclusion: MADNESS.
    return 0;
}

因此,当您在实际文件结束符之前读取字符 时,eof() 会“触发”。但如果流为空,它只会在您实际尝试读取字符时触发。 eof() 的意思是“你只是想读完结尾吗?”或者“如果你再读一遍,你会读到最后吗?”答案不一致。

此外,断言是否触发取决于编译器。例如,Apple Clang 4.1 触发断言(在读取前面的字符时引发 eof())。例如,GCC 4.7.2 没有。

这种不一致性使得编写明智的循环来读取流但同时处理空流和非空流变得很困难。

选项 1:

while( stream && !stream.eof() )
{
    const int c = stream.get();    // BUG: Wrong if stream was empty before the loop.
    // ...
}

选项 2:

while( stream )
{
    const int c = stream.get();
    if( stream.eof() )
    {
        // BUG: Wrong when c in fact got the last character of the stream.
        break;
    }
    // ...
}

那么,朋友们,我该如何编写一个循环来解析流,依次处理每个字符,处理每个字符,但在遇到 EOF 或流为空的情况下会毫不费力地停止开始,从未开始?

好吧,更深层次的问题:我有直觉认为使用 peek() 可能会以某种方式解决这个 eo​​f() 不一致问题,但是......该死的废话!为什么不一致?

【问题讨论】:

  • 你能指定什么编译器吗?您的EXHIBIT B: 行为对我来说似乎是一个错误。
  • @JesseGood:对于大多数实现来说,这似乎是正确的行为:流还没有尝试读取过去的 EOF。只有当它试图读取最后一个字符时,才需要设置eof()。不过可以提前设置eof()
  • @DietmarKühl:It is allowed to set eof() earlier, though. 嗯,你确定吗?
  • 使用多个编译器为我工作(运行完成)。
  • @DietmarKühl:你在哪里看到它可以更早地设置 eof() ?请记住,OP 使用 get(),它只读取一个字符,因此没有歧义。

标签: c++ iostream


【解决方案1】:

eof() 标志仅用于确定您是否在某些操作之后到达文件末尾。主要用途是在读取合理失败时避免出现错误消息,因为没有更多内容要读取。尝试使用eof() 控制循环或其他东西肯定会失败。在所有情况下,您都需要检查您尝试读取后是否读取成功。在尝试之前,流无法知道您要阅读的内容。

eof() 的语义被彻底定义为“读取流时设置此标志导致流缓冲区返回失败”。如果我没记错的话,找到这个陈述并不容易,但这就是结果。在某些时候,该标准还说,在某些情况下,允许流读取的内容比它必须的要多,这可能会导致在您不一定期望它时设置eof()。一个这样的例子是读取一个字符:流可能最终检测到该字符后面没有任何内容并设置eof()

如果你想处理一个空流,这很简单:从流中查看一些东西,只有在你知道它不为空时才继续:

if (stream.peek() != std::char_traits<char>::eof()) {
    do_what_needs_to_be_done_for_a_non_empty_stream();
}
else {
    do_something_else();
}

【讨论】:

  • @Vlad:我不这么认为。流需要让开,看看是否有另一个字符来提供结果,当您稍后实际尝试读取它时,该结果可能会改变,因为文件可能已经增长。它也总是归结为同样的问题:在您尝试提取某些内容之前,流无法再次猜测您将尝试提取什么。可能很难看的是提供一些灵活性,并且何时设置 eof() 但语义上在读取后检查时并不重要,它可能迎合了不同的实现以保持在规范内。
  • 该函数回答的不是“流的实际状态是什么”,而是“系统对流的了解是其他调用的副作用”。这暴露了实现细节:流在读取 eof 状态期间必须记住。这就是为什么我个人仍然认为它不优雅。
  • 另一个不优雅的迹象是 OP 需要一个额外的 if :)
  • @Vlad:流是两个实体:流缓冲区和流。流缓冲区可能知道序列的结尾在哪里(但可能不知道,因为底层序列可能会增长)。流仅向底层流缓冲区询问它认为需要的字符。
  • 是的,我知道这一点,并且我理解“你不为你没有要求的东西付费”哲学是如何暗示的——但我宁愿不需要知道也不需要关心。
【解决方案2】:

永远不要单独检查eof

eof 标志(与rdstate() 返回的值中的eofbit 位标志相同)在提取操作期间到达文件结尾时设置。如果没有提取操作,则永远不会设置 eofbit,这就是您的第一次检查返回 false 的原因。

但是eofbit 没有指示操作是否成功。为此,请检查rdstate() 中的failbit|badbitfailbit 表示“出现逻辑错误”,badbit 表示“出现 I/O 错误”。方便的是,有一个 fail() 函数可以准确返回 rdstate() &amp; (failbit|badbit)。更方便的是,有一个返回!fail()operator bool() 函数。所以你可以做while(stream.read(buffer)){ ...之类的事情。

如果操作失败,您可以分别检查eofbitbadbitfailbit 找出为什么它失败了。

【讨论】:

    【解决方案3】:

    您使用的是什么编译器/标准 c++ 库?我用 gcc 4.6.3/4.7.2 和 clang 3.1 尝试过,它们都工作得很好(即断言没有触发)。

    我认为您应该将此作为工具链中的错误报告,因为我对标准的阅读符合您的直觉,即只要 get() 能够返回字符,就不应该设置 eof()。

    【讨论】:

      【解决方案4】:

      这不是错误,因为它是预期的行为。它是 不是您在输入后使用测试 eof() 的意图 失败的。它的主要目的是用于内部提取函数,其中 在早期的实现中,std::streambuf::sgetc() 返回EOF 并不意味着它会在下一次被调用时: 目的是随时sgetc() 返回EOF(现在 std::char_traits&lt;&gt;::eof(),这会被记住,流 不会再调用 streambuf。

      实际上:我们确实需要两个eof():一个供内部使用, 如上所述,另一个可以可靠地说明故障是由于 已到达文件末尾。事实上,给定如下内容:

      std::istringstream s( "1.23e+" );
      s >> aDouble;
      

      无法检测到错误是由格式错误引起的, 而不是流没有更多数据。在这种情况下, 内部 eof 应该返回 true (因为我们已经看到文件结尾,当 展望未来,我们想压制所有进一步的呼吁 streambuf 提取器函数),但外部的应该是假的, 因为存在数据(即使在跳过初始空白之后)。

      如果您没有实现提取器功能,当然,您应该 永远不要测试ios_base::eof(),直到你真正遇到输入故障。 从来没有打算提供任何有用的信息 (这让人想知道他们为什么定义ios_base::good()—— 事实上它返回false 如果eof() 意味着它不能提供也不 可靠信息直到fail() 返回true,此时,我们 知道它会返回false,所以调用它没有意义)。

      我不确定你的问题是什么。因为流无法知道 提前你的下一个输入是什么(例如它是否会跳过 空格与否),它无法提前知道您的下一个输入是否 是否会因为文件结束而失败。采用的成语很明确: 尝试输入,then 测试是否成功。没有 其他方式,因为无法实施其他替代方案。帕斯卡做到了 它不同,但是输入了一个 Pascal 文件——你只能读取 一种类型,因此它总是可以提前读取下一个元素 如果预读失败,则返回文件结尾。没有 previsional end of file 是我们为能够阅读更多内容而付出的代价 比文件中的一种类型。

      【讨论】:

        【解决方案5】:

        这种行为有些微妙。 eofbit 在尝试读取文件末尾之后设置,但这可能会也可能不会导致当前提取操作失败。

        例如:

        ifstream blah;
        // assume the file got opened
        int i, j;
        blah >> i;
        if (!blah.eof())
            blah >> j;
        

        如果文件包含142&lt;EOF&gt;,则数字序列以文件结尾终止,因此设置eofbit 并且提取成功。不会尝试提取j,因为已经遇到文件结尾。

        如果文件包含142 &lt;EOF&gt;,则数字序列以空格结尾(i 提取成功)。 eofbit还没有设置,所以blah &gt;&gt; j会被执行,它会到达文件末尾没有找到任何数字,所以它会失败。

        注意文件末尾看似无害的空格如何改变行为。

        【讨论】:

          猜你喜欢
          • 2011-01-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-04
          • 1970-01-01
          • 2011-05-21
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多