ifstream :: unget () завершается ошибкой.Реализация MS ошибочна или мой код ошибочен? - PullRequest
5 голосов
/ 29 сентября 2010

Вчера я обнаружил странную ошибку в довольно простом коде, который в основном получает текст из 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;
}

Программа завершается сбоем, когда длина строки приближается к типичному кратному = 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, опциями компилятора / компоновщика по умолчанию. При тестировании с gcc4.4.5 в 64-битной системе Debian проблем не обнаружено.

Вопросы:

  1. могут ли другие люди проверить это? Я был бы очень признателен за активную форму сотрудничества с SO.
  2. есть ли что-нибудь , которое является неправильным в коде, которое может вызвать проблему (не говоря о том, имеет ли это смысл)
  3. или какие-либо флаги компилятора, которые могут вызвать такое поведение?
  4. весь код синтаксического анализатора довольно критичен для приложения и тщательно тестируется, но, конечно, эта проблема не была обнаружена в тестовом коде. Должен ли я придумать экстремальные тестовые случаи, и если да, то как мне это сделать? Как я мог предсказать, что это может вызвать проблемы?
  5. если это действительно ошибка, куда мне лучше сообщить об этом?

1 Ответ

6 голосов
/ 29 сентября 2010

есть ли в коде что-то неправильное, что могло бы вызвать проблему (не говоря уже о том, имеет ли это смысл)

Да. Стандартные потоки должны иметь как минимум 1 unget() позицию. Таким образом, вы можете безопасно сделать только один unget() после звонка на get(). Когда вы вызываете peek() и входной буфер пуст, происходит underflow(), и реализация очищает буфер и загружает новую порцию данных. Обратите внимание, что peek() не увеличивает текущее местоположение ввода, поэтому оно указывает на начало буфера. Когда вы пытаетесь unget(), реализация пытается уменьшить текущую позицию ввода, но она уже находится в начале буфера, поэтому она терпит неудачу.

Конечно, это зависит от реализации. Если буфер потока содержит более одного символа, он может иногда не работать, а иногда нет. Насколько я знаю, реализация microsoft хранит только один символ в basic_filebuf (если вы явно не укажете больший буфер) и использует внутреннюю буферизацию <cstdio> (кстати, это одна из причин, почему iostreams MVS работают медленно). Реализация качества может снова загрузить буфер из файла, если unget() завершится неудачно. Но это не обязательно.

Попробуйте исправить свой код, чтобы вам не требовалось более одной unget() позиции. Если вам это действительно нужно, оберните поток потоком, который гарантирует, что unget () не потерпит неудачу (посмотрите на Boost.Iostreams). Кроме того, код, который вы разместили, это ерунда. Он пытается unget(), а затем get() снова. Почему?

...