Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Cpp_Страуструп.doc
Скачиваний:
17
Добавлен:
03.05.2015
Размер:
3.2 Mб
Скачать

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, что две самые общие формы преобразования

иерархии классов состоят в разбиении класса на два и в выделении

общей части двух классов в базовый класс. В обоих случаях хорошо

продуманный инвариант может подсказать возможность такого

преобразования. Если, сравнивая инвариант с программами операций,

можно обнаружить, что большинство проверок инварианта излишни,

то значит класс созрел для разбиения. В этом случае подмножество операций

имеет доступ только к подмножеству состояний объекта. Обратно,

классы созрели для слияния, если у них сходные инварианты, даже

при некотором различии в их реализации.