Шаг 506.
Библиотека STL. Ввод-вывод с использованием потоковых классов. Операторы ввода-вывода для пользовательских типов. Реализация операторов ввода

    На этом шаге мы рассмотрим особенности реализации операторов ввода.

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

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

#include <iostream>

inline
std::istream& operator >> (std::istream& strm, Fraction& f)
{
    int n, d;

    strm >> n;      // Ввод числителя
    strm.ignore();  // Пропуск '/'
    strm >> d;      // Ввод знаменателя

    f = Fraction(n,d);  // Присваивание всей дроби

    return strm;
}

    Во-первых, такая реализация подходит только для потоков данных с типом символов char. Во-вторых, она не проверяет, действительно ли два числа разделяются символом /.

    Другая проблема возникает при вводе неопределенных значений. Если знаменатель прочитанной дроби равен 0, ее поведение не определено. Проблема обнаруживается в конструкторе класса Fraction, вызываемом выражением Fraction(n,d). Но зто означает, что ошибки форматирования автоматически обрабатываются внутри класса Fraction. Так как на практике ошибки форматирования обычно регистрируются на уровне потоков данных, лучше установить в этом случае флаг ios_base::failbit.

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

    Ниже приведена усовершенствованная реализация программы, избавленная от этих недостатков. Кроме того, она более универсальна, поскольку благодаря параметризации подходит для любых типов потоков данных:

#include <iostream>

template <class charT, class traits>
inline
std::basic_istream<charT,traits>&
operator >> (std::basic_istream<charT,traits>& strm, Fraction& f)
{
    int n, d;

    // Ввод числителя
    strm >> n;

    // Если числитель прочитан успешно
    //  - прочитать '/' и знаменатель
    if (strm.peek() == '/') {
        strm.ignore();
        strm >> d;
    }
    else {
        d = 1;
    }

    // Если знаменатель равен нулю
    //  - установить failbit как признак ошибки форматирования ввода-вывода
    if (d == 0) {
        strm.setstate(std::ios::failbit);
        return strm;
    }

    // Если все прошло успешно,
    //  изменить значение дроби
    if (strm) {
        f = Fraction(n,d);
    }

    return strm;
}

    На этот раз знаменатель читается только в том случае, если за первым числом следует символ /; в противном случае по умолчанию используется знаменатель, равный 1, а целое число интерпретируется как дробь. Таким образом, для целых чисел знаменатель не обязателен.

    Реализация также проверяет, не равен ли прочитанный знаменатель нулю. В этом случае устанавливается флаг ios_base::failbit, что может привести к выдаче соответствующего исключения. Разумеется, при нулевом знаменателе возможны и другие действия. Например, реализация может сама сгенерировать исключение или вообще отказаться от проверки знаменателя, чтобы исключение было сгенерировано классом Fraction.

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

    Конечно, разумность чтения целых чисел как дробей можно поставить под сомнение. Существуют и другие нюансы, которые тоже можно исправить: например, символ \ должен следовать за числителем без разделяющих пробелов. С другой стороны, знаменателю может предшествовать произвольное количество пробелов, которые обычно игнорируются. Впрочем, это дает лишь отдаленное представление о сложностях, связанных с чтением нетривиальных структур данных.

    На следующем шаге мы рассмотрим ввод-вывод с использованием вспомогательных функций.




Предыдущий шаг Содержание Следующий шаг