Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 190
Текст из файла (страница 190)
Язык С++ не поддерживает идиому делегирования напрямую, но ее можно смоделировать в коде на С++ (то есть запрограммировать) следующим образом: с!азз В ( (( ... »оиру'( ); еоЫе(); юЫ й(); )' с!азз А ( В*р; /(... ооЫу'( ); юй(Я'( ); юЫ е () (р->д (); ) У делегируем е() »оЫй() (р->йО; ) Уделегируем ЬВ )' У делегируе ~ через указатель р Программисту ясно, что здесь класс А делегирует работу классу В через указатель А::р, но для языка программирования нет никакой разницы, как именно используется указатель типа В*. Соответственно, такие «программируемые отношения» между классами плохо распознаются автоматизированными инструментами. 876 Глава 24. Проектирование и программирование Взаимооднозначные соответствия между концепциями проекта и средствами языка программирования следует использовать всегда, когда только возможно. Они обеспечивают простоту и гарантируют, что проект точно отражен в коде, а это помогает в работе и программистам, и автоматизированным инструментам.
Операции преобразования обеспечивают языковый механизм для выражения программируемых отношений. Например, операция преобразования Х:: орега<ог У( ) утверждает, что если допустим У, то можно использовать и Х Я1!.4.1). Конструктор У:: У(Х) выражает то же самое отношение. Отметим, что операция преобразования (и конструктор) производят новый объект, а не изменяют тип существующего. Объявляя операцию преобразования к У, мы обеспечиваем способ неявного вызова функции, возвращающей У.
Поскольку неявное применение преобразований, программируемых с помощью соответствующих операций и конструкторов, может быть опасным, их следует тщательно и по отдельности проанализировать в проекте. Важно также гарантировать, что графы преобразования типов не зацикливаются. В противном случае возникнут ошибки для некоторых комбинаций типов. Например: с1авв Кайопа1; с<авв В1д !и< ( риЫ<с: Тг(епд В<я Ы< арета<ого (В/е <п<, В!д 1пп орега<ог К<«юпа1 ( ) < // ... )) с1авв Кайо«а< ( риЫ<с: кепд Кайопа! орега<ог+ (Кайопа!, Кайопа1) орега<ог В<я и«() < // ...
)< Типы Кайо«а! и В!» !л< взаимодействуют не так гладко, как кто-то надеялся: го!«)(Кайо«а< г, В(а !п«) д(гН); //еггог, неоднознонностш орего<ог+(гланопо!<В) или орего<ог+(В<к <п<(г),<)? д (г+Кайопа! (1) ); // одно явное разрешение д (В(д <п< (г) +<); // другое явное разреи<ение ) Можно избежать таких взаимных преобразований, сделав некоторые из них явными. Например, преобразование В)я 1«< а Капала! можно определить (вместо операцйи) функцией «<а!<е КаИола1(), и тогда сложение разрешилось бы как д(В1В 1«<(г), <) . Там, где не избежать взаимных операций преобразования, следует разрешать конфликты при помощи явных преобразований или предоставляя несколько версий таких операций, как е.
877 24.3 Классы 24.3.7. Отношения внутри класса Класс может скрывать любое число деталей реализации и некоторое количество «грязи» Иногда ему это приходится делать. В то же время, объекты класса имеют вполне регулярную структуру и ими можно легко манипулировать. Объект класса— это набор некоторых подобъектов (часто называемых членами объекта), многие из которых являются указателями или ссылками на другие объекты. Таким образом, объект можно представить себе в виде корня дерева других объектов, в совокупности составляющих иерархию объектов, дополнительную к иерархии классов (й24.3.2.1).
Например, рассмотрим очень простой класс о(г!пд: с(аз( Я(г(пя ( йи з(( сйаг* р( рий((: 51((пя (солса сйаг* «); -Б(г(пд ( ); ~У ... ): Объект класса Я(г!пя можно представить графически следующим образом; 24.3.7.1. Инварианты Значения членов объекта и значения объектов, на которые могут указывать члены, в совокупности называются состоянием объекта или его значением (ой!сс(((а(е ог та!ие). Главной заботой при проектировании класса является необходимость привести объект в точно определенное состояние (инициализация/конструирование), поддерживать такое состояние в процессе выполнения операций с объектом и, наконец, уничтожить объект корректным и изящным образом. Свойство, делающее состояние объекта четко определенным, называется ипвариаптом (Игаг!ап(). Итак, целью проектирования является приведение объекта в состояние с выполняющимся инвариантом.
Как правило, это делается конструктором. Любая операция над классом ожидает истинный инвариант на входе и обязана оставить инвариант в этом состоянии на выходе. В конце концов, деструктор отменяет инвариант, уничтожая обьект. Например, конструктор Я(г!пя:: Б(г(ай (сапа( айат*) гарантирует, что р указывает на массив по крайней мере из лег 1 элементов, где зх имеет разумное значение и р(зх) ==О. Каждая строковая операция должна оставлять это утверждение истинным. Искусство проектирования состоит в том, чтобы сделать класс простым и чтобы его реализация имела четко выраженный инвариант.
Заявить это просто, но не так-то просто реально сформулировать полезный инвариант, который бы легко воспринимался и не налагал неприемлемых ограничений на эффективность операций класса. Под инвариантом мы будем здесь понимать фрагмент кода, который 878 Глава 24. Проектирование и программирование с1азз з!г!п0 тл! ззт сйаг* р; риЬКс: с!азз Каляе ( ); сйазз Тниаг!алт ( ); д ктассы исключений епит ( ТОО ЕАКОЕ = 16000 ); II предел длины го!И елеей(); й проверка инворианта Я~г!п0 (соиз! айаг* 0); Б!ггл0 (соне! Б!г!ляй ); -Я!г(лл ( ); слака орегагог [] (!л! !); !и! з!зе () (ге!игл згн ) У... )' иоЫ 5Ылл:: елеей ( ) ( 0 (р==0()зз<0( [ТОО ТАКОЕ<=зз) (р[зз] ) загоы Тпгаг!апг() ) с!така К г!п0:: прего!ог [ ] (!пз !) ( елеей (): Ц (!<О[ ) ее<=1) !агою Калле () ) слева ( ); ге!нгп рУ] ' !! проверка на входе ,(г работаеи // проверка на выходе Это хорошо работает и не представляет какой-либо сложности для программиста.
В то же время, для такого простого класса, как Ягг!ня, проверки займут бсль- можно выполнить для проверки состояния объекта. Можно также использовать и более строгое, математическое определение инварианта, и оно бывает уместно в некоторых случаях. Но мы будем придерживаться более простого понятия инварианта, как практической (пусть и логически неполной) проверки состояния объекта.
Понятие инварианта ведет свое начало от работ Флойда, Наура и Хоара (г]оуб, ]ч]апг, Ноаге) о предусловиях и постусловиях и присутствует практически во всех работах по абстрактным типам данных и верификации программ за последние 30 лет. Оно также широко применяется в процессе отладки программ, написанных на языке С.
Как правило, инвариант не поддерживается в процессе выполнения функций-членов. Функции, которые могут вызываться в этот момент, не должны быть частью открытого интерфейса класса (здесь лучше применять зашишенные и закрытые функции). Как выразить понятие инварианта в коде на С++? Простейший путь — определить функцию проверки инварианта и вставлять ее вызов в открытые операции. Например: 879 24.3. Классы шую часть времени работы методов и, возможно, большую часть объема кода. Поэтому программисты часто выполняют проверку инварианта только во время отлад- «пгй«е гои( Б«гтя «: сйесй ( ) ( №с/п««е)' )«(РЕВГ)С ««(р==0) )вс<0) ) 700 ЕАВОЕ<=вг) )р(вс] ) «йгон«!пваг«ап«() « №еп«(«у" ) Здесь макрос А(РЕВ№)б используется так же, как в стандартном макросе аввегг( ) языка С.
Принято устанавливать )«)РЕВ«)б в знак того, что отладка не производится. Столь простая работа с инвариантами во время отладки оказывает бесценную помощь в получении правильно работающего кода и, что даже еще важнее, в четком отражении классами концепций проекта. Дело в том, что при определении инвариантов приходится рассматривать классы с иной точки зрения, в результате чего код получает полезную избыточность, увеличивая вероятность выявления ошибок, несовместимостей и оплошностей. во«0)"(«пг* р) алвес«(р ! =О); «)« ... дутверо«с«)ается, что р/=0; ойог«() есяи р равно нулю Перед тем, как прекратить работу программы, аиегг() выводит имя своего исходного файла и номер строки, на которой это произошло. Это делает макрос амегг() полезным отладочным средством. Обычно )«(РЕВ66 устанавливается с помощью опций компилятора отдельно для каждой единицы трансляции.
Отсюда следует, что не стоит использовать атвегг() во встраиваемых и шаблонных функциях, поскольку их определения включаются в разные единицы трансляции, и нужно тщательно следить, чтобы ))(РЕВЮ6 был всюду установлен одинаково (59.2.3). Как и все, что относится к магии макросов, применение )«(РЕВЕРС следует признать весьма низкоуровневым приемом, довольно запуганным и подверженным ошибкам. Кроме того, иногда нужно оставлять диагностические средства и в хорошо работающей программе, а вызов айогг() не подходит для готового кода промьшгленного уровня.