|
Программирование >> Поддержка объектно-ориентированного программирования
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, что две самые общие формы преобразования иерархии классов состоят в разбиении класса на два и в выделении общей части двух классов в базовый класс. В обоих случаях хорошо продуманный инвариант может подсказать возможность такого преобразования. Если, сравнивая инвариант с программами операций, можно обнаружить, что большинство проверок инварианта излишни, то значит класс созрел для разбиения. В этом случае подмножество операций имеет доступ только к подмножеству состояний объекта. Обратно, классы созрели для слияния, если у них сходные инварианты, даже при некотором различии в их реализации. 12.2.7.2 Инкапсуляция Отметим, что в С++ класс, а не отдельный объект, является той единицей, которая должна быть инкапсулирована (заключена в оболочку). Например: class list { list* next; public: int on(list*); int list::on(list* p) list* q = this; for(;;) { if (p == q) return 1; if (q == 0) return 0; q = q->next; Здесь обращение к частному указателю list::next допустимо, поскольку list::on() имеет доступ ко всякому объекту класса list, на который у него есть ссылка. Если это неудобно, ситуацию можно упростить, отказавшись от возможности доступа через функцию-член к представлениям других объектов, например: int list::on(list* p) if (p == this) return 1; if (p == 0) return 0; return next->on(p); Но теперь итерация превращается в рекурсию, что может сильно замедлить выполнение программы, если только транслятор не сумеет обратно преобразовать рекурсию в итерацию. 12.2.8 Программируемые отношения Конкретный язык программирования не может прямо поддерживать любое понятие любого метода проектирования. Если язык программирования не способен прямо представить понятие проектирования, следует установить удобное отображение конструкций, используемых в проекте, на языковые конструкции. Например, метод проектирования может использовать понятие делегирования, означающее, что всякая операция, которая не определена для класса A, должна выполняться в нем с помощью указателя p на соответствующий член класса B, в котором она определена. На С++ нельзя выразить это прямо. Однако, реализация этого понятия настолько в духе С++, что легко представить программу реализации: class A { B* p; ... void f(); void ff(); class B { ... class A { B* p; ... void f(); void ff(); void g() { p->g(); } void h() { p->h(); } делегирование с помощью p делегирование q() делегирование h() Для программиста совершенно очевидно, что здесь происходит, однако здесь явно нарушается принцип взаимнооднозначного соответствия. Такие программируемые отношения трудно выразить на языках программирования, и поэтому к ним трудно применять различные вспомогательные средства. Например, такое средство может не отличить делегирование от B к A с помощью A::p от любого другого использования B*. Все-таки следует всюду, где это возможно, добиваться взаимнооднозначного соответствия между понятиями проекта и понятиями языка программирования. Оно дает определенную простоту и гарантирует, что проект адекватно отображается в программе, что упрощает работу программиста и вспомогательных средств. Операции преобразований типа являются механизмом, с помощью которого можно представить в языке класс программируемых отношений, а именно: операция преобразования X::operator Y() гарантирует, что всюду, где допустимо использование Y, можно применять и X. Такое же отношение задает конструктор Y::Y(X). Отметим, что операция преобразования типа (как и конструктор) скорее создает новый объект, чем изменяет тип существующего объекта. Задать операцию преобразования к функции Y - означает просто потребовать неявного применения функции, возвращающей Y. Поскольку неявные применения операций преобразования типа и операций, определяемых конструкторами, могут привести к неприятностям, полезно проанализировать их в отдельности еще в проекте. Важно убедиться, что граф применений операций преобразования типа не содержит циклов. Если они есть, возникает двусмысленная ситуация, при которой типы, участвующие в циклах, становятся несовместимыми в комбинации. Например: class Big int { ... friend Big int operator+(Big int,Big int); ... operator Rational(); ... class Rational { ... friend Rational operator+(Rational,Rational); ... operator Big int(); Типы Rational и Big int не так гладко взаимодействуют, как можно было бы подумать: void f(Rational r, Big int i) ошибка, неоднозначность: operator+(r,Rational(i)) operator+(Big int(r),i) void f(); void g(); void h(); Тот факт, что В делегирует A с помощью указателя A::p, выражается в следующей записи: ... g(r+i);
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |