2012-01-11 1 views
3

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

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

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

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

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? а не только основные вещи: полная обработка ошибок.

спасибо.

+0

Возможно, Boost.Qi облегчит вашу жизнь для этой задачи. –

+0

@KerrekSB: Boost.Qi то же самое, что Boost.Spirit? Если это так, я уже попробовал. Хотя я был поражен дизайном библиотеки, это значительно увеличило время моей компиляции до того, что я столкнулся со сбоем компилятора при некоторых условиях. Во всяком случае, мой вопрос больше связан с изучением передового опыта, чем с практическим решением. Однако, спасибо. – ereOn

+1

Да, я думаю, что это то же самое, хотя я действительно не вижу в джунглях духа/кармы/ци/феникса/фьюжн ... и вы определенно хотите прекомпилировать заголовки с Boost, это точно! Ци просто поразила меня как довольно элегантное решение чего-то, что требует много ручного переосмысления колес в противном случае. –

ответ

2

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

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

Я не назвал бы это ошибка но вы, вероятно, хотите, чтобы убедиться, что вы не должны отступать от того, что вы прочитали. То есть я бы выполнил три версии входных функций. В зависимости от того, насколько сложным является декодирование определенного типа, я, возможно, даже не разделяю код, потому что это может быть всего лишь небольшая часть. Если это больше, чем строка или две, вероятно, разделит код. То есть в вашем примере у меня будет экстрактор для 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(). В противном случае мы всегда терпим неудачу. Решила непосредственную проблему. On для распространения информации ...

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

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

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

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

если (STD :: GetLine (в, ул) & & in.eof()) {зЬй: : cout < < "файл, не заканчивающийся на новую строку \"; }

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

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

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

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() копирует все поля, связанные с потоком, связанные с форматированием. К ним относятся:

  • локаль
  • в fmtflags
  • iword хранения информации() и pword()
  • события ручья
  • Исключения
  • потоков государственная

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

В чем разница между неисправностью и неисправностью? Когда мы должны использовать одно или другое?

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

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

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

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

+0

Большое спасибо за этот замечательный и полный ответ :) Убирает почти все мои сомнения по теме! Я обязательно постараюсь получить одну из этих книг. – ereOn

0

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

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

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

2

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

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

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

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

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

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

Try:

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

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

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

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

Да, но это требует двух вызовов: see

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

Q: Какие различия они между failbit и badbit?

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

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

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

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

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

+0

Большое спасибо за этот очень полный ответ. Я принимаю * Dietmar Kühl * ответ, потому что он также очень хорош и дает некоторые ссылки на книгу по этой теме. Однако, несмотря на это, это справедливо. – ereOn

 Смежные вопросы

  • Нет связанных вопросов^_^