C++的 istream::eof() 的不一致是规范中的错误还是实现中的错误?

Is the inconsistency of C++'s istream::eof() a bug in the spec or a bug in the implementation?

本文关键字:错误 范中 实现 不一致 eof istream C++      更新时间:2023-10-16

下面的程序演示了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() 可能会以某种方式解决这种 eof() 不一致的问题,但是......天哪!为什么不一致?

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();
}

永远不要单独检查eof

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

但是eofbit并不能表明操作是否成功。为此,请检查failbit|badbitrdstate().failbit表示"存在逻辑错误",badbit表示"存在 I/O 错误"。方便的是,有一个fail()函数可以准确返回rdstate() & (failbit|badbit)。更方便的是,有一个返回!fail()operator bool()函数。所以你可以做一些事情,比如while(stream.read(buffer)){ ....

如果操作失败,您可以分别检查eofbitbadbitfailbit,以找出失败的原因

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

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

这不是一个错误,从某种意义上说,这是预期的行为。 是的不是您在输入之前使用 test 进行eof()的意图 失败。 它的主要目的是在提取函数中使用,其中 在早期的实现中,事实上std::streambuf::sgetc()返回EOF并不意味着下次调用时会: 目的是每当sgetc()返回EOF(现在std::char_traits<>::eof(),这会被记住,然后流 不会再打电话给流。

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

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

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

当然,如果您没有实现提取器函数,您应该 切勿测试ios_base::eof(),直到您实际遇到输入失败。 这从来都不是要提供任何有用的信息 (这让人想知道为什么他们定义ios_base::good()- 事实上,如果eof()它返回false意味着它可以提供 NOR 可靠的信息fail()返回true,此时,我们 知道它会返回false,所以调用它没有意义)。

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

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

例如:

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

如果文件包含142<EOF>,则数字序列在文件末尾终止,因此eofbit设置并成功提取。 不会尝试提取j,因为已遇到文件末尾。

如果文件包含142 <EOF>,则数字序列以空格结尾(提取i成功)。eofbit尚未设置,因此将执行blah >> j,并且它将到达文件末尾而没有找到任何数字,因此它将失败。

请注意文件末尾看起来无害的空格如何更改行为。