ifstream:: unget() терпит неудачу. Ошибочна ли реализация MS или мой код ошибочен?


Вчера я обнаружил странную ошибку в довольно простом коде, который в основном получает текст из 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. могут ли другие люди, пожалуйста, проверить это? Я был бы очень признателен за активную форму сотрудничества.
  2. есть ли что-нибудь неправильное в коде, что может вызвать проблему (не говоря уже о том, имеет ли это смысл)
  3. или любые флаги компилятора, которые могут вызвать такое поведение?
  4. all parser код довольно критичен для приложения и тестируется тяжело, но, конечно, эта проблема не была обнаружена в тестовом коде. Должен ли я придумать экстремальные тестовые случаи, и если да, то как это сделать? Как я мог предугадать, что это может вызвать проблемы?
  5. если это действительно ошибка, где мне лучше всего сообщить об этом?
1 4

1 ответ:

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

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

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

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