昨天,我在相当简单的代码中发现了一个奇怪的错误,该代码基本上从 ifstream 获取文本并将其标记化。实际失败的代码执行了多次 get()/peek() 调用来查找标记“/*”。如果在流中找到令牌,则调用 unget(),以便下一个方法看到以令牌开头的流。

有时,似乎仅取决于文件的长度,unget() 调用会失败。它在内部调用 pbackfail(),然后返回 EOF。然而,清除流状态后,我可以很高兴地读取更多字符,所以它不完全是 EOF..

深入研究后,这是可以轻松重现问题的完整代码:

#include <iostream>
#include <fstream>
#include <string>

  //generate simplest string possible that triggers problem
void GenerateTestString( std::string& s, const size_t nSpacesToInsert )
{
  s.clear();
  for( size_t i = 0 ; i < nSpacesToInsert ; ++i )
    s += " ";
  s += "/*";
}

  //write string to file, then open same file again in ifs
bool WriteTestFileThenOpenIt( const char* sFile, const std::string& s, std::ifstream& ifs )
{
  {
    std::ofstream ofs( sFile );
    if( ( ofs << s ).fail() )
      return false;
  }
  ifs.open( sFile );
  return ifs.good();
}

  //find token, unget if found, report error, show extra data can be read even after error 
bool Run( std::istream& ifs )
{
  bool bSuccess = true;

  for( ; ; )
  {
    int x = ifs.get();
    if( ifs.fail() )
      break;
    if( x == '/' )
    {
      x = ifs.peek();
      if( x == '*' )
      {
        ifs.unget();
        if( ifs.fail() )
        {
          std::cout << "oops.. unget() failed" << std::endl;
          bSuccess = false;
        }
        else
        {
          x = ifs.get();
        }
      }
    }
  }

  if( !bSuccess )
  {
    ifs.clear();
    std::string sNext;
    ifs >> sNext;
    if( !sNext.empty() )
      std::cout << "remaining data after unget: '" << sNext << "'" << std::endl;
  }

  return bSuccess;
}

int main()
{
  std::string s;
  const char* testFile = "tmp.txt";
  for( size_t i = 0 ; i < 12290 ; ++i )
  {
    GenerateTestString( s, i );

    std::ifstream ifs;
    if( !WriteTestFileThenOpenIt( testFile, s, ifs ) )
    {
      std::cout << "file I/O error, aborting..";
      break;
    }

    if( !Run( ifs ) )
      std::cout << "** failed for string length = " << s.length() << std::endl;
  }
  return 0;
}

当字符串长度接近典型的 multiple=of-2 缓冲区大小 4096, 8192, 12288 时,程序失败,输出如下:

oops.. unget() failed
remaining data after unget: '*'
** failed for string length = 4097
oops.. unget() failed
remaining data after unget: '*'
** failed for string length = 8193
oops.. unget() failed
remaining data after unget: '*'
** failed for string length = 12289

在 Windows XP 和 7 上进行测试时会发生这种情况,两者都在调试/发布模式下编译,动态/静态运行时,32 位和 64 位系统/编译,全部使用 VS2008、默认编译器/链接器选项。在64位Debian系统上使用gcc4.4.5测试时没有发现问题。

问题:

  1. 其他人可以测试一下吗?我非常感谢 SO 的积极合作。
  2. 有没有 任何事物 这在可能导致问题的代码中不正确(不谈论它是否有意义)
  3. 或任何可能触发此行为的编译器标志?
  4. 所有解析器代码对于应用程序都相当关键并且经过了大量测试,但是当然在测试代码中没有发现这个问题。我应该提出极端的测试用例吗?如果是的话,我该怎么做?我怎么能预测这会导致问题呢?
  5. 如果这确实是一个错误,我应该在哪里最好地报告它?
有帮助吗?

解决方案

代码中是否有任何不正确的地方可能导致问题(不讨论是否有意义)

是的。标准流至少需要有 1 unget() 位置。所以你只能安全地做一件事 unget() 致电后 get(). 。你打电话时 peek() 并且输入缓冲区为空, underflow() 发生并且实现清除缓冲区并加载数据的新部分。注意 peek() 不会增加当前输入位置,因此它指向缓冲区的开头。当你尝试 unget() 该实现尝试减少当前输入位置,但它已经位于缓冲区的开头,因此失败。

当然这取决于实施。如果流缓冲区保存多个字符,则有时可能会失败,有时则不会。据我所知,微软的实现仅在 basic_filebuf 中存储一个字符(除非您显式指定更大的缓冲区)并依赖于 <cstdio> 内部缓冲(顺便说一句,这就是 MVS iostream 速度慢的原因之一)。质量实现可能会在以下情况下再次从文件加载缓冲区 unget() 失败。但并不要求这样做。

尝试修复您的代码,这样您就不需要多个代码 unget() 位置。如果您确实需要它,则使用保证 unget() 不会失败的流来包装该流(请参阅 Boost.Iostreams)。另外你贴的代码也是废话。它试图 unget() 进而 get() 再次。为什么?

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top