Бьерн Страуструп.
Язык программирования С++
326
String(const
char*
q);
~String();
//...
};
Объект типа String можно изобразить так:
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;
Бьерн Страуструп.
Язык программирования С++
327
}
char& String::operator[](int i)
{
check();
// проверка на входе
if (i<0 || i
//
действует
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
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//...
}
Шаблон типа Assert() подражает макрокоманде assert() языка С. Если i не находится в требуемом
диапазоне, возникает особая ситуация Bad_f_arg.
С помощью отдельной константы или константы из класса проверить подобные утверждения или
инварианты - пустяковое дело. Если же необходимо проверить инварианты с помощью объекта, можно
определить производный класс, в котором проверяются операциями из класса, где нет проверки, см.
упр.8 в $$13.11.
Для классов с более сложными операциями расходы на проверки могут быть значительны, поэтому
проверки можно оставить только для "поимки" трудно обнаруживаемых ошибок. Обычно полезно
оставлять по крайней мере несколько проверок даже в очень хорошо отлаженной программе. При всех
условиях сам факт определения инвариантов и использования их при отладке дает неоценимую
помощь для получения правильной программы и, что более важно, делает понятия, представленные
классами, более регулярными и строго определенными. Дело в том, что когда вы создаете инварианты,
то рассматриваете класс с другой точки зрения и вносите определенную избыточность в программу. То
и другое увеличивает вероятность обнаружения ошибок, противоречий и недосмотров. Мы указали в
$$11.3.3.5, что две самые общие формы преобразования иерархии классов состоят в разбиении класса
на два и в выделении общей части двух классов в базовый класс. В обоих случаях хорошо
продуманный инвариант может подсказать возможность такого преобразования. Если, сравнивая
Бьерн Страуструп.
Язык программирования С++
328
инвариант с
программами операций, можно обнаружить, что большинство проверок инварианта
излишни, то значит класс созрел для разбиения. В этом случае подмножество операций имеет доступ
только к подмножеству состояний объекта. Обратно, классы созрели для слияния, если у них сходные
инварианты, даже при некотором различии в их реализации.
0>0>0>
Достарыңызбен бөлісу: