ifstream :: unget()失败.是MS的实施错误还是我的代码错误?

sti*_*ijn 5 c++ ifstream visual-studio-2008 visual-studio

昨天我在相当简单的代码中发现了一个奇怪的错误,它基本上从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;
}
Run Code Online (Sandbox Code Playgroud)

当字符串长度接近典型的倍数= 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
Run Code Online (Sandbox Code Playgroud)

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

问题:

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

ybu*_*ill 6

是否有任何不正确的代码可能会导致问题(不是说它是否有意义)

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

当然这取决于实施.如果流缓冲区包含多个字符,则有时可能会失败,有时则不会.据我所知,microsoft的实现只在basic_filebuf中存储一个字符(除非你明确指定一个更大的缓冲区)并且依赖于<cstdio>内部缓冲(顺便说一下,这就是为什么MVS iostream缓慢的原因).质量实现可能会在unget()失败时再次从文件加载缓冲区.但并不要求这样做.

尝试修复您的代码,这样您就不需要多个unget()职位.如果你真的需要它,那么用一个流来包装流,以保证unget()不会失败(看看Boost.Iostreams).你发布的代码也是无稽之谈.它unget()get()一次又一次地尝试.为什么?