Б. Страуструп - Язык программирования С++ (1119446), страница 92
Текст из файла (страница 92)
Но для такого простого классакак 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, что две самые общие формы преобразования иерархии классов состоят в разбиении классана два и в выделении общей части двух классов в базовый класс. В обоих случаях хорошопродуманный инвариант может подсказать возможность такого преобразования. Если, сравнивая327Бьерн Страуструп.Язык программирования С++инвариант с программами операций, можно обнаружить, что большинство проверок инвариантаизлишни, то значит класс созрел для разбиения. В этом случае подмножество операций имеет доступтолько к подмножеству состояний объекта.
Обратно, классы созрели для слияния, если у них сходныеинварианты, даже при некотором различии в их реализации.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) returnif (p == 0) return 0;return next->on(p);}1;Но теперь итерация превращается в рекурсию, что может сильно замедлить выполнение программы,если только транслятор не сумеет обратно преобразовать рекурсию в итерацию.12.2.8 Программируемые отношенияКонкретный язык программирования не может прямо поддерживать любое понятие любого методапроектирования.
Если язык программирования не способен прямо представить понятиепроектирования, следует установить удобное отображение конструкций, используемых в проекте, наязыковые конструкции. Например, метод проектирования может использовать понятие делегирования,означающее, что всякая операция, которая не определена для класса A, должна выполняться в нем спомощью указателя p на соответствующий член класса B, в котором она определена. На С++ нельзявыразить это прямо. Однако, реализация этого понятия настолько в духе С++, что легко представитьпрограмму реализации:class A {B* p;//...void f();void ff();};class B {//...328Бьерн Страуструп.Язык программирования С++void f();void g();void h();};Тот факт, что В делегирует A с помощью указателя A::p, выражается в следующей записи: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){//...g(r+i);// ошибка, неоднозначность:// operator+(r,Rational(i)) или// operator+(Big_int(r),i)329Бьерн Страуструп.g(r,Rational(i));g(Big_int(r),i);Язык программирования С++// явное разрешение неопределенности// еще одно}Можно было бы избежать таких "взаимных" преобразований, сделав некоторые из них явными.Например, преобразование Big_int к типу Rational можно было бы задать явно с помощью функцииmake_Rational() вместо операции преобразования, тогда сложение в приведенном примереразрешалось бы как g(BIg_int(r),i).
Если нельзя избежать "взаимных" операций преобразования типов,то нужно преодолевать возникающие столкновения или с помощью явных преобразований (как былопоказано), или с помощью определения нескольких различных версий бинарной операции (в нашемслучае +).12.3 КомпонентыВ языке С++ нет конструкций, которые могут выразить прямо в программе понятие компонента, т.е.множества связанных классов. Основная причина этого в том, что множество классов (возможно ссоответствующими глобальными функциями и т.п.) может соединяться в компонент по самым разнымпризнакам. Отсутствие явного представления понятия в языке затрудняет проведение границы междуинформацией (имена), используемой внутри компонента, и информацией (имена), передаваемой изкомпонента пользователям.
В идеале, компонент определяется множеством интерфейсов,используемых для его реализации, плюс множеством интерфейсов, представляемых пользователем, авсе прочее считается "спецификой реализации" и должно быть скрыто от остальных частей системы.Таково может быть в действительности представление о компоненте у разработчика. Программистдолжен смириться с тем фактом, что С++ не дает общего понятия пространства имен компонента, такчто его приходится "моделировать" с помощью понятий классов и единиц трансляции, т.е. тех средств,которые есть в С++ для ограничения области действия нелокальных имен.Рассмотрим два класса, которые должны совместно использовать функцию f() и переменную v. Прощевсего описать f и v как глобальные имена.