Б. Страуструп - Язык программирования С++. Специальное издание, 3-изд. Бином. 2004 (1160791), страница 190
Текст из файла (страница 190)
Свойство, делающее состояние объекта четко определенным, называется инвариантом. Таким образом, целью инициализации является приведение объект в состояние, для которого выполняется инвариант. Как правило, это делается конструктором, Каждая операция над классом может полагаться на то, что она найдет инвариант истинным при входе, и она должна оставить его истинным при выходе. Деструктор окончательно отменяет инвариант, уничтожая объект.
Например, конструктор Ыг(пйсЫг1пд(сопв1 сйаг*) гарантирует, что р указывает на массив по крайней мере из за+1 элементов, где зя имеет осмысленное значение, ир)зя) ==О. Каждая строковая операция должна оставлятьь это утверждение истинным. Искусство проектирования классов включает в себя умспис сделать класс достаточно простым, чтобы при реализации он имел полезный и просто выражаемый инвариант. Просто заявить, что каждому классу нужен инвариант. Труднее получить полезный инвариант, который легко понять, и который не подразумевает неприемлемых ограничений на реализацию или эффективность операций.
Отметим, что «инвариант» здесь можно понилзать также как фрагмент кода, который можно выполнить для проверки состояния объекта. Вполне возможно более строгое, математическое понятие, и в некоторых контекстах оно более уместно. Инвариант, который обсуждается здесь— это практическая — н поэтому, как обычно, экономная и логически незавершенная— проверка состояния объекта. Понятие инварпанта ведет свое происхождение от работы Флойда, Наура и Хоара (В)оус(, Хацг, апд Ноаге) о предусловиях и постусловиях, и в последние 30 лет оно присутствует практически во всех работах по абстрактным типам данных и верификации программ. Оно также является основой отладки в С.
Как правило, инвариант не сохраняется во время выполнения функции-члена. Функции, которые кто-то может вызвать в то время, когда инвариант не выполняется, не должны быть частью открытого интерфейса, Для этой цели могут скужить закрытые и защищенные функции. Как нам выразить понятие инварианта в программе на С++? Простой путь — определить функцию проверки инварианта и в открытые операции вставить ее вызов. Например: Глава 24.
Проектирование и программирование 32б сЬагйорегагог() ((л1К ш1 кнее () ( ге1игп кк; ) 0- оо!л1 Баглае".:сйеск () ( ([ (о==О)) хе<0)) кОО ЕЛ1(бЕ<=кк) р[кк-1)) 11лгош!поапап1 (л сдаг8 б1ппасорегатог[) (лп1 1) л'л'проверка на входе Оработаекл О проверка на выходе сЬесп 'В1 (лс (1<0)) ее<=1)1Ьгош йоль (); сйесл (), ге1игп р[1), Это будет неплохо работать н вряд ли представляет какую-либо сложность для про- граммиста. Однако для простого класса вроде 31г(лп проверка инварианта займет болыпую часть времени выполнения и, может быть, даже большую часть кода. Поэто- му программисты часто выполняют проверку инварианта только во время отладки: лл Иле оо1л( МппвхсйесЬ () ( «(Гплтет пОЕВОб лу (р==д ) кк<д () ОО ЕА1(бЕ<=кк)) р[кк)) 1Ьгош 1лоапап1 (); «елд(т ) 24.3.7.2. Утверждения Инвариант — это особая форма утверждения.
Утверждение — это просто высказывание о том, по некий .югичсский критерий должен выполняться. Вопрос в том, что делать, когда он не выполняется. Стандартная библиотека С вЂ” и стало быть, стандартная библиотека С++ — предоставляет в заголовочных файлах <саккег1> или <аккег1.6> макрос аккег1 (). Макрос аккег1 () вычисляет свой аргумент, и если результат равен нулю Ца(ке), вызывает абог1 (), Например: ооЫЯ(п~' р) ' Здесь макрос ЛгОЕВПб используется аналогично тому, как он применяется в стандартном макросе С аккег1 ().
Принято устанавливать ЛИЕЙ/б в знак того, что отладка не производится. Простое действлле по определению инвариантов и использование их во время отладки оказывает неоценимую помощь в получении правильного кода и — что более важно — в том, чтобы выраженные классами понятия были четко определены и регулярны.
Дело в том, что когда вы будете проектировать инварианты, класс будет рассматриваться с другой точки зрения, и код будет содержать избыточность. И то, и другое увеличивает вероятность выявления ошибок, несовместимостей и оплошностей. 827 24.3. Классы ( аяяег1 'ра В)1 // прекращение програласвс если р равно нуле //.- ) гетр(оге<с(аяяХ, с1аяяА> 1пйпе иоЫ Аяяег1 (А аяяегтгоп) ( 1/ (1аяяегпоп) гд гав Х Ц; Аяяег1 () ~ енерирует исключение Х (), если аяяегйоп ложно.
Например: с1аяяВас( аог(); ооЫ/(1п~ р) ( Аяяегг<Вас( агХ> (рг=п)' //- ) // верки р1=0; генеРаЦиЯ Во<1 а~к, еслиР ~=0 При этом стиле утверждений условия записываются явно, поэтому, если мы хотим проверять только во время отладки, мы должны так и сказать. Например: соЫ /2(Ы1' р) Аяяегг<Вас1 ага> (Ж(<ЕВ(16)) рй=0), 0-. ) Олибонет отладки,либор!=О Употребление в утверждении )) в отличие от Ь$ может показаться удивительны м.
Однако Аяяег1 <Е>(а))Ь) проверяет!(а))Ь), что эквивалентно (аб с )Ь). Использование МЕЕВ(/6 в этом случае требует, чтобы мы определили /яВЕВбг6 со значением, говорящим, находимся мы в режиме отладки или нет. Реализация С++ не делает этого для нас по умолчанию, поэтому лучше пользоваться значением. Например: Перед прекрапдением программы, аяяег1 () выводит имя исходного файла и номер строки, где прекратилось выполнение. Этим аяяег( () оказывает большую помощь при отладке. /яллЕВ</6 обгячно устанавливается опциями компилятора для каждой единицы компиляции отдельно. Это подразумевает, что аяяег(() не следует использовать во встроенных функциях и шаблонах функций, которые включаются в несколько единиц трансляции, разве что тщательно следя за тем, что ЖВЕВбг6 везде установлен одинаково (2 9 2 3). Как и вся остальная <макросомагия>, М)ЕВЕ16 относится к слишком низкому уровню, слишком запутан и провоцирует ошибки.
Кроме того, бывает полезно оставить активными по крайней мере некоторые средства проверки даже в отлично проверенной программе, аЛЮЕВ(/6 не очень хорошо для этого подходит. Ьолее того, вызов аЬог( () редко приемлем в промьппленпом коде. Однако часто полезно оставить хотя бы некоторые проверки активными даже в прекрасно проверенной программе, а й1ОЕВ(/6 не очень хорошо для этого подходит. К тому жс вызов абог1 () редко приемлем для готовых программ.
Альтернатива заключается в использовании шаблона Аеяег1 (), который генерирует исключение, а не прекращает программу, так что утверждения можно оставить в готовой программе, если это желательно. К сожалению, стандартная библиотека не предоставляет Аяяег( (). Однако он очень просто определяется: Глава 24. Проектирование и программирование 828 С!с!<1еЗ' ЬГОЕВУБ СопзсЬоо!АВБ СНЕСК=(а!зе; ае!зе сопз1Ьоо1 АВБ СНЕСК= 1гие; ссепс1!! пои!УЗ (сп1 *р) О не отлаживиелс — запретить проверка О отложив аелс Алвес!<Вас! ага> (~АКБ СНЕСК)) р!=О); ))либонет отладки,либо рс=б ) Если исключение, связанное с утверждением, не перехвачено, неудавшийся Аввег! () заканчивает программу (функцией 1егт(па1е ()) почти так же, как макрос аввег! () вызывает абог! ().
Однако обработчик исключений может предпринять не такие радикальные действия. В л собой программе осмысленных разме)юв для тестирования я вюсючаю и выключаю утверждения в группах. Функция аввег! () языка С вЂ” это просто самая грубая форма данного приема. На ранних этапах разработки болыпипство утверждений включены, однако после сдачи программы включеннымн остаются только основные проверки. Такой стиль использования лссне всего управляется, если действительное утвер>кденьсе делится на две части, где первая является разрешающим условием (переменной, разрешающей ту или иную проверку, такой как АРБ СНЕСК), а вторая — собственно утверждением. Если разрешающее условие — это константное выражение, то когда оно не включено, все утверждение просто не будет скомпилировано.
Однако разрешающее условие может быть и переменным, так что утверзкдение можно включать и выключать во время выполнения программы, как того требует отладка. Например: Ьоо! згапд сЬесЬ = сгие; т!те оо1с)51г1пя; сЬесЬ () ( Авяегг<!поапап1> (сзгг!па свесЬ (( (р гяГя 0<=ее аГ> вг<ТОО РЯБЕ М р(вг) ==0)й оо!с! !'() ( 51г!пуз; гслс здесь строки проверяюпмя всг1пя сЬесЬ=Га!зе; лссс здесь строки не проверяются Естественно, в таких случаях код будет генерироваться, поэтому если мы активно пользуемся утверждениями, то должны следить, не начнется ли разбухание кода.
Инструкция Аззегг<Е> (а); это просто другой способ сказать сУ (!а)сйгош Е (); Так зачем же связываться с Аввег1 (), а не написать прямо эту инструкцию? Дело в том, что применение Аввег! () делает намерение проектировщика явным. Оно говорит, что это утверлсдение о чем-то таком, что всегда предполагается истинным.
Это не обычная часть программпои логики. И, тем самым, ценная информация для тех, 829 24.3. Классы кто будет читать программу. Более практическое преимущество состоит в том, что в тексте проще искать аккег( () или Аккег( () нежели осуществлять нетривиальный поиск условных выражений, генерирующих исключения. Аккег( () можно обобщить для генерации исключений с аргументами и переменных исключений: гетр(ась<с(акк А, с(акк Е> ьп1гпе ооЫАккегГ (А акзепоп, Е ехсерй ( (( (!аккегпоп) ехсер1; кггис1Вас( у агу( 1п1 р; Вас(у агу (1п1' рр): р (рр) () Ьоо1 у с(ьесп= 1гие; гагу тах=100; ио1ь(у ((п1' р, ехсер11оп е) ( О указатель допуспьиль 1/значение допустило Аккег( ((у с(ьеси'1 р(=0, е)1 Аккег1 (.'у с(ьесй)( (О<'рйй'р<=а тах),Вал( у агу(р)); Во многих программах критически важно, чтобы там, где утверждение можно вычислить во время компиляции, никакой код для Аккег( () не генерировался.
К сожалению, некоторые компиляторы не могут добиться этого для обобщенного Аккег( (). Поэтому двухаргументный шаблон Аккег( () следует применять, только когда исключение представлено не в форме Е (), и когда допустимо, чтобы некоторый код генерировался независимо от значения утверждения. В 9 23АЗ.5 упоминалось, что две самые распространенные формы реорганизации иерархии классов — это расщепление класса ца два и выделение обшей части двух классов в базовый класс. В обоих случаях ключ к потенциальной реорганизации могут дать четко определенные инварианты. В созревшем для расщепления классе сравнение инварианта с кодом операций выявит болыпую часть избыточных проверок инвариаита.
В таких случаях подмноькества операций будут иметь доступ только к соответствующим подмножествам состояния обьекта. И наоборот — классы, созревшие для объединения, будут иметь схожие инварианты, даже если детали их реализации различаются. Аккегг<Вас( кедиепсе> 11 плакал последовательность? ("(Егк1 1акг) — допусьпильал последовательность' ), 11 псеваокод 24.3.7.3.