Бьерн Страуструп - Язык программирования С++. Главы 11-13
Страница 37. Инварианты



12.2.7.1 Инварианты

Значение членов или объектов, доступных с помощью членов класса,
называется состоянием объекта (или просто значением объекта).
Главное при построении класса - это: привести объект в полностью
определенное состояние (инициализация), сохранять полностью определенное
состояние обЪекта в процессе выполнения над ним различных операций,
и в конце работы уничтожить объект без всяких последствий. Свойство,
которое делает состояние объекта полностью определенным, называется
инвариантом.
     Поэтому назначение инициализации - задать конкретные значения,
при которых выполняется инвариант объекта. Для каждой операции класса
предполагается, что инвариант должен иметь место перед выполнением
операции и должен сохраниться после операции. В конце работы
деструктор нарушает инвариант, уничтожая объект. Например,
конструктор String::String(const char*) гарантирует,
что p указывает на массив из, по крайней мере, sz элементов, причем
sz имеет осмысленное значение и v[sz-1]==0. Любая строковая операция
не должна нарушать это утверждение.
    При проектировании класса требуется большое искусство, чтобы
сделать реализацию класса достаточно простой и допускающей
наличие полезных инвариантов, которые несложно задать. Легко
требовать, чтобы класс имел инвариант, труднее предложить полезный
инвариант, который понятен и не накладывает жестких ограничений
на действия разработчика класса или на эффективность реализации.
Здесь "инвариант" понимается как программный фрагмент,
выполнив который, можно проверить состояние объекта. Вполне возможно
дать более строгое и даже математическое определение инварианта, и в
некоторых ситуациях оно может оказаться более подходящим. Здесь же
под инвариантом понимается практическая, а значит, обычно экономная,
но неполная проверка состояния объекта.
    Понятие инварианта появилось в работах Флойда, Наура и Хора,
посвященных пред- и пост-условиям, оно встречается во всех важных
статьях по абстрактным типам данных и верификации программ за
последние 20 лет. Оно же является основным предметом отладки в C++.
    Обычно, в течение работы функции-члена инвариант не сохраняется.
Поэтому функции, которые могут вызываться в те моменты, когда
инвариант не действует, не должны входить в общий интерфейс класса.
Такие функции должны быть частными или защищенными.
   Как можно выразить инвариант в программе на С++? Простое решение -
определить функцию, проверяющую инвариант, и вставить вызовы этой
функции в общие операции. Например:

     class String {
         int sz;
         int* p;
     public:
         class Range {};
         class Invariant {};

         void check();

         String(const char* q);
         ~String();
         char& operator[](int i);
         int size() { return sz; }
         //...
     };

     void String::check()
     {
         if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz-1])
            throw Invariant;
     }

     char& String::operator[](int i)
     {
         check();                        // проверка на входе
         if (i<0 || i<sz) throw Range;   // действует
         check();                        // проверка на выходе
         return v[i];
     }

Этот вариант прекрасно работает и не осложняет жизнь программиста.
Но для такого простого класса как String проверка инварианта будет
занимать большую часть времени счета. Поэтому программисты обычно
выполняют проверку инварианта только при отладке:

     inline void String::check()
     {
         if (!NDEBUG)
             if (p==0 || sz<0 || TOO_LARGE<=sz || p[sz])
                 throw Invariant;
     }

Мы выбрали имя NDEBUG, поскольку это макроопределение, которое
используется для аналогичных целей в стандартном макроопределении
С assert(). Традиционно NDEBUG устанавливается с целью указать,
что отладки нет. Указав, что check() является подстановкой, мы
гарантировали, что никакая программа не будет создана, пока константа
NDEBUG не будет установлена в значение, обозначающее отладку.
С помощью шаблона типа Assert() можно задать менее регулярные
утверждения, например:

     template<class T, class X> inline void Assert(T expr,X x)
     {
         if (!NDEBUG)
             if (!expr) throw x;
     }

вызовет особую ситуацию x, если expr ложно, и мы не отключили
проверку с помощью NDEBUG. Использовать Assert() можно так:

     class Bad_f_arg { };

     void f(String& s, int i)
     {
         Assert(0<=i && i<s.size(),Bad_f_arg());
         //...
     }

Шаблон типа Assert() подражает макрокоманде assert() языка С.
Если i не находится в требуемом диапазоне, возникает особая
ситуация Bad_f_arg.
    С помощью отдельной константы или константы из класса проверить
подобные утверждения или инварианты - пустяковое дело. Если же
необходимо проверить инварианты с помощью объекта, можно определить
производный класс, в котором проверяются операциями из класса, где нет
проверки, см. упр.8 в $$13.11.
   Для классов с более сложными операциями расходы на проверки могут
быть значительны, поэтому проверки можно оставить только для "поимки"
трудно обнаруживаемых ошибок. Обычно полезно оставлять по крайней
мере несколько проверок даже в очень хорошо отлаженной программе.
При всех условиях сам факт определения инвариантов и использования
их при отладке дает неоценимую помощь для получения правильной
программы и, что более важно, делает понятия, представленные
классами, более регулярными и строго определенными. Дело в том, что
когда вы создаете инварианты, то рассматриваете класс с другой
точки зрения и вносите определенную избыточность в программу.
То и другое увеличивает вероятность обнаружения ошибок, противоречий
и недосмотров.
Мы указали в $$11.3.3.5, что две самые общие формы преобразования
иерархии классов состоят в разбиении класса на два и в выделении
общей части двух классов в базовый класс. В обоих случаях хорошо
продуманный инвариант может подсказать возможность такого
преобразования. Если, сравнивая инвариант с программами операций,
можно обнаружить, что большинство проверок инварианта излишни,
то значит класс созрел для разбиения. В этом случае подмножество операций
имеет доступ только к подмножеству состояний объекта. Обратно,
классы созрели для слияния, если у них сходные инварианты, даже
при некотором различии в их реализации.

 
« Предыдущая статья   Следующая статья »