Каковы правила разбора с помощью iostreams? - PullRequest
4 голосов
/ 11 января 2012

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

Для улучшения возможности повторного использования я решил основывать свои функции синтаксического анализа на потоках ввода / вывода, чтобы их можно было использовать с такими вещами, как boost::lexical_cast<>.

Однако я понял, что никогда нигде не читал о том, как это сделать правильно.

Чтобы проиллюстрировать мой вопрос, давайте рассмотрим, что у меня есть три класса Foo, Bar и FooBar:

A Foo представлен данными в следующем формате: string(<number>, <number>).

A Bar представлен данными в следующем формате: string[<number>].

A FooBar является разновидностью типа, которая может содержать Foo или Bar.

Теперь предположим, что я написал operator>>() для моего Foo типа:

istream& operator>>(istream& is, Foo& foo)
{
    char c1, c2, c3;
    is >> foo.m_string >> c1 >> foo.m_x >> c2 >> std::ws >> foo.m_y >> c3;

    if ((c1 != '(') || (c2 != ',') || (c3 != ')'))
    {
      is.setstate(std::ios_base::failbit);
    }

    return is;
}

Синтаксический анализ выполняется для правильных данных. Но если данные неверны:

  • foo может быть частично изменено;
  • Некоторые данные во входном потоке были прочитаны и, следовательно, больше не доступны для дальнейших вызовов is.

Кроме того, я написал еще один operator>>() для моего FooBar типа:

istream& operator>>(istream& is, FooBar foobar)
{
  Foo foo;

  if (is >> foo)
  {
    foobar = foo;
  }
  else
  {
    is.clear();

    Bar bar;

    if (is >> bar)
    {
      foobar = bar;
    }
  }

  return is; 
}

Но очевидно, что это не работает, потому что если is >> foo не удается, некоторые данные уже прочитаны и больше не доступны для вызова is >> bar.

Так вот мои вопросы:

  • Где здесь моя ошибка?
  • Должен ли кто-нибудь написать свои звонки на operator>>, чтобы оставить исходные данные все еще доступными после сбоя? Если так, как я могу сделать это эффективно?
  • Если нет, есть ли способ «сохранить» (и восстановить) полный статус входного потока: состояние и данные?
  • Какая разница между failbit и badbit? Когда мы должны использовать один или другой?
  • Существует ли какая-либо онлайн-справка (или книга), которая глубоко объясняет, как бороться с iostreams? не только основные вещи: полная обработка ошибок.

Большое спасибо.

Ответы [ 3 ]

3 голосов
/ 12 января 2012

Лично я думаю, что это разумные вопросы, и я очень хорошо помню, что сам боролся с ними.Итак, начнем:

Где здесь моя ошибка?

Я бы не назвал это ошибкой , но вы, вероятно, хотите убедиться,Вы не должны отступать от того, что вы прочитали.То есть я бы реализовал три версии функций ввода.В зависимости от того, насколько сложным является декодирование определенного типа, я мог бы даже не делиться кодом, потому что в любом случае он может быть просто небольшим фрагментом.Если это больше, чем строка или две, вероятно, поделится кодом.То есть в вашем примере у меня был бы экстрактор для FooBar, который по существу читает элементы Foo или Bar и соответственно инициализирует объекты.В качестве альтернативы я прочитал бы ведущую часть и затем вызвал бы общую реализацию, извлекающую общие данные.

Давайте сделаем это упражнение, потому что есть несколько вещей, которые могут быть осложнены.Из вашего описания формата мне не ясно, если «строка» и то, что следует за строкой, разделены, например, пробелом (пробел, табуляция и т. Д.).Если нет, вы не можете просто прочитать std::string: поведение по умолчанию для них - читать до следующего пробела.Есть способы настроить поток, чтобы рассматривать символы как пробел (используя std::ctype<char>), но я просто предположу, что есть место.В этом случае экстрактор для Foo может выглядеть следующим образом (обратите внимание, что весь код полностью не проверен):

std::istream& read_data(std::istream& is, Foo& foo, std::string& s) {
    Foo tmp(s);
    if (is >> get_char<'('> >> tmp.m_x >> get_char<','> >> tmp.m_y >> get_char<')'>)
        std::swap(tmp, foo);
    return is;
}
std::istream& operator>>(std::istream& is, Foo& foo)
{
    std::string s;
    return read_data(is >> s, foo, s);
}

Идея состоит в том, что read_data() читает частьFoo, который отличается от Bar при чтении FooBar.Аналогичный подход будет использоваться для Bar, но я опускаю это.Более интересным является использование этого забавного get_char() шаблона функции.Это то, что называется манипулятором , и это просто функция, принимающая ссылку на поток в качестве аргумента и возвращающая ссылку на поток.Поскольку у нас есть разные символы, которые мы хотим прочитать и сравнить, я сделал это шаблоном, но у вас также может быть одна функция для каждого символа.Мне просто лень его печатать:

template <char Expect>
std::istream& get_char(std::istream& in) {
    char c;
    if (in >> c && c != 'e') {
        in.set_state(std::ios_base::failbit);
    }
    return in;
}

В моем коде немного странно то, что проверок на работоспособность мало.Это потому, что поток просто установит std::ios_base::failbit, когда чтение участника не удастся, и мне не нужно беспокоиться.Единственный случай, когда на самом деле добавлена ​​специальная логика, это get_char(), чтобы иметь дело с ожиданием определенного символа.Аналогично, пропуски пропусков символов (т. Е. Использование std::ws) не выполняются: все входные функции являются formatted input функциями, и по умолчанию они пропускают пробел (вы можете отключить это, используя, например, in >> std::noskipws), но затемвещей не будет работать.

При аналогичной реализации для чтения Bar чтение FooBar будет выглядеть примерно так:

std::istream& operator>> (std::istream& in, FooBar& foobar) {
    std::string s;
    if (in >> s) {
         switch ((in >> std::ws).peek()) {
         case '(': { Foo foo; read_data(in, foo, s); foobar = foo; break; }
         case '[': { Bar bar; read_data(in, bar, s); foobar = bar; break; }
         default: in.set_state(std::ios_base::failbit);
         }
    }
    return in;
 }

Этот код использует Неформатированная функция ввода , peek(), которая просто смотрит на следующий символ.Он либо возвращает следующий символ, либо возвращает std::char_traits<char>::eof() в случае неудачи.Таким образом, если есть открывающая скобка или открывающая скобка, мы имеем read_data().В противном случае мы всегда терпим неудачу.Решил ближайшую проблему.О распространении информации ...

Должен ли кто-нибудь написать свои звонки оператору >>, чтобы оставить исходные данные все еще доступными после сбоя?

Общий ответ:нет.Если вам не удалось прочитать что-то пошло не так, и вы сдаетесь.Это может означать, что вам нужно работать больше, чтобы не потерпеть неудачу.Если вам действительно нужно отступить от позиции, в которой вы находились, для анализа ваших данных, вы можете сначала прочитать данные в std::string, используя std::getline(), а затем проанализировать эту строку.Использование std::getline() предполагает наличие отдельного символа, на котором можно остановиться.По умолчанию используется перевод строки (отсюда и название), но вы также можете использовать и другие символы:

std::getline(in, str, '!');

Это остановится на следующем восклицательном знаке и сохранит все символы до него в str. Это также извлечет символ завершения, но не сохранит его. Это иногда делает интересным, когда вы читаете последнюю строку файла, который может не иметь новой строки: std::getline() успешно, если он может прочитать хотя бы один символ. Если вам нужно узнать, является ли последний символ в файле новой строкой, вы можете проверить, достиг ли поток:

if (std :: getline (in, str) && in.eof ()) {std :: cout << "файл не заканчивается новой строкой \"; } </p>

Если так, как я могу сделать это эффективно?

Потоки по своей природе являются однопроходными: вы получаете каждый символ только один раз, а если пропустите один, вы его потребляете. Таким образом, вы обычно хотите структурировать свои данные таким образом, чтобы вам не приходилось возвращаться назад. Тем не менее, это не всегда возможно, и у большинства потоков есть буфер под капотом, два символа которого могут быть возвращены. Поскольку потоки могут быть реализованы пользователем, нет гарантии, что символы могут быть возвращены. Даже для стандартных потоков нет никакой гарантии.

Если вы хотите вернуть символ, вы должны вернуть именно тот символ, который вы извлекли:

char c;
if (in >> c && c != 'a')
    in.putback(c);
if (in >> c && c != 'b')
    in.unget();

Последняя функция имеет немного лучшую производительность, потому что она не должна проверять, что персонаж действительно тот, который был извлечен. У него также меньше шансов потерпеть неудачу. Теоретически, вы можете вернуть столько символов, сколько хотите, но большинство потоков не будут поддерживать больше, чем несколько во всех случаях: если есть буфер, стандартная библиотека заботится о том, чтобы «не получать» все символы до начала буфера достигнуто Если возвращается другой символ, он вызывает виртуальную функцию std::streambuf::pbackfail(), которая может или не может сделать больше доступного буферного пространства. В реализованных мною потоковых буферах он обычно просто не работает, т.е. я обычно не переопределяю эту функцию.

Если нет, есть ли способ «сохранить» (и восстановить) полный статус входного потока: состояние и данные?

Если вы хотите полностью восстановить состояние, в котором вы были, включая персонажей, ответ: конечно, есть. ... но нет легкий способ. Например, вы можете реализовать буфер потока фильтрации и вернуть символы обратно, как описано выше, чтобы восстановить читаемую последовательность (или поддержать поиск или явную установку метки в потоке). Для некоторых потоков вы можете использовать поиск, но не все потоки поддерживают это. Например, std::cin обычно не поддерживает поиск.

Восстановление персонажей - это только половина истории. Другие вещи, которые вы хотите восстановить, это флаги состояния и любые данные форматирования. На самом деле, если поток перешел в сбойное или даже плохое состояние, вам нужно очистить флаги состояния, прежде чем поток выполнит большинство операций (хотя я думаю, что форматирование может быть сброшено в любом случае):

std::istream fmt(0); // doesn't have a default constructor: create an invalid stream
fmt.copyfmt(in);     // safe the current format settings
// use in
in.copyfmt(fmt);     // restore the original format settings

Функция copyfmt() копирует все поля, связанные с потоком, которые связаны с форматированием. Это:

  • язык
  • флаги
  • хранилище информации iword () и pword ()
  • события потока
  • исключения
  • состояние потоков

Если вы не знаете о большинстве из них, не беспокойтесь: большинство вещей, о которых вы, вероятно, не заботитесь. Ну, пока вам это не нужно, но к тому времени вы, надеюсь, приобрели некоторую документацию и прочитали об этом (или спросили и получили хороший ответ).

Какая разница между сбойным битом и плохим битом? Когда мы должны использовать один или другой?

Наконец, короткий и простой:

  • failbit устанавливается при обнаружении ошибок форматирования, например, ожидается число, но символ 'T' найден.
  • badbit устанавливается, когда что-то идет не так в инфраструктуре потока. Например, когда буфер потока не установлен (как в потоке fmt выше), для потока установлен std::badbit. Другая причина в том, что выдается исключение (и перехватывается с помощью маски exceptions(); по умолчанию перехватываются все исключения).

Существует ли какая-либо онлайн-справка (или книга), которая глубоко объясняет, как бороться с iostreams? не только основные вещи: полная обработка ошибок.

Ах да, рад, что вы спросили. Вы, вероятно, хотите получить «Стандартную библиотеку C ++» Николая Йосуттиса. Я знаю, что эта книга описывает все детали, потому что я внесла свой вклад в ее написание. Если вы действительно хотите знать все о IOStreams и локалях, вам нужны Angelika Langer & Klaus Kreft "IOStreams and Locales". В случае, если вам интересно, откуда я взял информацию изначально: это были «IOStreams» Стива Тила, я не знаю, находится ли эта книга в печати, и в ней не хватает многих вещей, которые были представлены во время стандартизации. Поскольку я реализовал свою собственную версию IOStreams (и локалей), я знаю и о расширениях.

2 голосов
/ 11 января 2012

Итак, вот мои вопросы:

Q: Где здесь моя ошибка?

Я бы не назвал вашу технику ошибкой. Это абсолютно нормально.
Когда вы читаете данные из потока, вы, как правило, уже знаете объекты, выходящие из этого потока (если объекты имеют несколько интерпретаций, то это также необходимо либо кодировать в поток (или вам необходимо иметь возможность откатить поток).

Q: Должен ли кто-нибудь написать свои звонки оператору >>, чтобы оставить исходные данные все еще доступными после сбоя?

Состояние отказа должно быть там, только если что-то действительно плохое пошло не так.
В вашем случае, если вы ожидаете foobar (который имеет два представления), у вас есть выбор:

  1. Отметьте тип объекта, который поступает в поток, с некоторыми префиксными данными.
  2. В разделе синтаксического анализа foobar используйте ftell () и fseek () для восстановления позиции потока.

Попробуйте:

  std::streampos point = stream.tellg();
  if (is >> foo)
  {
    foobar = foo;
  }
  else
  {
    stream.seekg(point)
    is.clear();

Q: Если так, как я могу сделать это эффективно?

Я предпочитаю метод 1, когда вы знаете тип в потоке.
Второй метод может использоваться, когда это неизвестно.

Q: Если нет, есть ли способ «сохранить» (и восстановить) полный статус входного потока: состояние и данные?

Да, но для этого требуется два вызова: см.

std::iostate   state = stream.rdstate()
std::istream   holder;
holder.copyfmt(stream)

Q: Чем они отличаются между failbit и badbit?

Из документации до сбоя вызова ():

failbit: обычно устанавливается операцией ввода, когда ошибка связана с внутренней логикой самой операции, поэтому возможны другие операции в потоке.
badbit: обычно устанавливается, когда ошибка связана с потерей целостности потока, которая, вероятно, будет сохраняться, даже если в потоке выполняется другая операция. Badbit можно проверить независимо, вызвав функцию-член bad.

Q: Когда мы должны использовать один или другой?

Вы должны установить failbit.
Это означает, что ваша операция не удалась. Если вы знаете, как это не удалось, вы можете выполнить сброс и повторить попытку.

плохой бит - это когда вы случайно смешиваете внутренние элементы потока или делаете что-то настолько плохое, что сам объект потока полностью разветвляется.

0 голосов
/ 11 января 2012

Когда вы сериализуете свой FooBar, у вас должен быть флаг, указывающий, какой он является, который будет «заголовком» для вашей записи / чтения.

Когда вы читаете его обратно, вы читаете флаг, затем читаетев соответствующем типе данных.

И да, наиболее безопасно сначала прочитать во временный объект, а затем переместить данные.Иногда вы можете оптимизировать это с помощью функции swap ().

...