Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 236
Текст из файла (страница 236)
Например: гетргате<с1ат Т> Тв геетог<Т>::орегатг[] (з!се (1) ( ~Т(г) гетгп » [1); // улаживаем проблему ) Наличие возможности г==Р делает стоимость операции индексирования без проверки диапазона равной стоимости операции индексирования с проверкой диапазона индексов: гетр!а!в< сгат Т> Тв тес!ее< Т>::аг(з!ге ! !) ( «Т(!<ч.з!се() ) гетгп »[1] т твго«г оп< оГ голее("чеетог иЫех"); ) Главное здесь то, что допуская возможносты-=О, я усложнил базовый инвариант для гестог Ьам, а как следствие усложнился и таковой для весгог.
В конечном итоге, весь код вестог Ьазе и вес!ее усложнился в связи с этим, что является источником возможных ошибок, усложняет поддержку кода и увеличивает накладные расходы на этапе выполнения. Отметим, что условные операторы могут оказаться неожиданно дорогими на современных компьютерных архитектурах. Если производительность критически важна, то ключевые операции (вроде индексирования вектора) следует реализовывать без условных операторов. Интересно, что первоначальное определение вестог Ьазе уже содержало легко конструируемое действительное состояние: объект вестог Ьаее не мог сушествовать без успешного выделения памяти.
Из-за этого реализация гестог могла содержать следуюшую «функцию аварийного выхода»; тетр1ате<с(авз Т, с1ам А> гоЫ чеегог<Т,А>::етегеепеу ехй() ( зраее = ч; д устанавливаем размер»гь!з в О твгоп Тога! Та!!иге(); Это немного грубовато, так как при этом не удается вызвать деструкторы элементов и освободить в них память, отведенную под поля данных базового типа частое Ьазе. То есть не обеспечивается базовая гарантия (ВЕ.2). Если же мы намерены доверять значениям г и зрасе н деструкторам элементов, то мы можем избежать потенциальных утечек ресурсов: тегпр1ате<с!ат Т, с1ат А> чо!а иеетог<Т,А>::етегдепеу ех1! () Е 3.
Технологии реализации безопасности при исключениях 1097 ( ггеззгоу е!етепгз(); У очистка гйгогг тосе! ~а((иге (); Отметим, что стандартный застое спроектирован настолько аккуратно, что он минимизирует проблемы, вызываемые двухфазным конструированием. Функция злзг() приблизительно эквивалентна функции газ(хе(), и почти везде возможность и==О закрыта проверками лзхе () =-О. Отрицательные эффекты двухфазного конструирования становятся особо заметными в случае классов, захватывающих важные ресурсы, такие как сетевые соединения и файлы. Эти классы редко являются частью более общей среды, в рамках которой формулировались бы способы их реализации и использования, как это имеет место для контейнера гесгог в стандартной библиотеке языка С++.
Обычно проблемы усиливаются еще и из-за того, что понятия прикладных областей плохо соответствуют описанию реально захватываемых компьютерных ресурсов, необходимых для их реализации. Немного существует классов, концепции которых отображаются на системные ресурсы столь непосредственно, как это имеет место для контейнера гесгог. Идея «безопасного состояния» в принципе хорошая. Если мы не можем поместить объект в безопасное состояние без риска генерации исключения в процессе выполнения операции, у нас и в самом деле проблемы.
Однако «безопасное состояние» должно вытекать из семантики класса, а не быть артефактом реализации, усложняющим инвариант этого класса. Е.З.Б.З. Отложенное выделение ресурсов Подобно второму пункту (5Е.2), третий пункт (отложить выделение ресурса до момента, когда он на самом деле потребуется) неправильно применяет хорошую идею, так что издержки есть, а выгод нет.
Во многих случаях, особенно для контейнеров вроде гесгог, лучший способ отложить выделение ресурсов — это отложить создание объекта. Рассмотрим неоптимальное использование контейнера уесгож гоЫЗ(гн( и) ( гесгог<Х> г(п); ~У... г[З) = Х(99); У... ) У создаем и умолчательных значений типа Х и реальная "инициализация" ч(З( Создание Хс единственной целью присвоить ему позже новое значение, является расточительной идеей, особенно если присваивание для типа Х не дешево.
Поэтому двухфазное конструирование Х может показаться привлекательным. Например, если тип Х сам является вектором, то можно было бы рассмотреть двухфазное создание чесгог с целью эффективного создания пустых векторов. Однако создание умолчательных (пустых) векторов и так эффективно, поэтому усложнение реализации с учетом частного случая пустого вектора представляется бесполезным. Вообще, освобождение кода конструктора элементов от сложной инициализации редко когда является наилучшим решением для реализации фиктивной инициализации. Вместо этих ухищрений пользователь может создавать элементы строго по необходимости.
Например: Приложение Е Исключения и безопасность стандартной библиотеки 1098 иоЫ г2((и( и) ( иесгог<Х>»э У... «.ризй Ьасй (Х(99) ) ( ~У .. ) У создаем нустой нес(ог У конструируем элементы ио необходимости Подводим итог: двухфазное конструирование объектов приводит к более сложным инвариантам, и, как правило, к менее элегантному, чреватому ошибками коду, более сложному в сопровождении. Где это возможно, следует отдавать предпочтение коду, базирующемуся на конструкторах, а не на шюг() -функциях, поскольку первый подход поддерживается языковыми средствами. То есть ресурсы должны выделяться в конструкторах, если только отложенное вьщеление ресурсов не диктуется унаследованной семантикой класса. Е.4.
Гарантии стандартных контейнеров Если операция стандартной библиотеки генерирует исключение, она может быть уверена в том, что ее объекты-операнды остаются в действительном состоянии. например, аг(), генерирующая исключение оаг оУ галле для уесгог (916.3.3), не создает проблем с обеспечением безопасности исключений для контейнера еесгож Разработчик операции аг() не сомневается, что в момент генерации исключения уесгог находится в хорошо определенном состоянии. Трудности для разработчиков библиотеки, ее пользователей, а также людей, пытающихся разобраться в коле, начинаются тогда, когда исключение генерирует функция, предоставленная пользователем. Контейнеры стандартной библиотеки предоставляют базовую гарантию 5Е.2): соблюдаются основные инварианты библиотеки и нет утечек ресурсов до тех пор, пока пользовательский код ведет себя как надо, то есть пользовательские «операции» не оставляют элементы контейнеров в недействительных состояниях и не генерируют исключений из деструкторов.
Под «операциями» я здесь понимаю операции, используемые реализацией стандартной библиотеки, такие как конструкторы, операции присваивания, деструкторы и операции над итераторами (5Е.4.4). Программисту относительно легко добиться того, чтобы эти операции соответствовали ожиданиям библиотеки. Даже бесхитростно написанный код часто фактически соответствует требованиям библиотеки. Следующие типы определенно удовлетворяют требованиям стандартной библиотеки к типам контейнерных элементов: 1. Встроенные типы — включая указатели. 2. Типы без пользовательских операций.
3. Классы с операциями, не генерирующими исключений и не оставляющими операнды в недействительных состояниях. 4. Классы с деструкторами, не генерирукнцими исключений, и с операциями (которые использует стандартная библиотека — конструкторы, присваивания, <, ==, зтеар () и т.д.), для которых можно легко убедиться, что они не оставляют операнды в недействительных состояниях. Е4.
Гарантии стандартных контейнеров 1099 В любом случае нужно также убедиться, что нет утечек ресурсов. Например: иэЫГ(С!гс1е* рс, Тг!аде* рг, чесэог<ЯЬаре*>а ы2) ( иестог<5Ьаре*> ч(10); й'создает чес(ог или генерирует Ьаг! а!!ос ч(З) = рс! !нет генерации исключений ч.!пэвк!(ч.Ьед!п()+4,р!); Уили внедряет рг, или не иэменяет ч ч2.егаэе(ч2.Ьея1п () +3) ! Фили удаляет ч2(31, или не изменяет ч2 г= ! У копирует ч или не изменяет ч2 // ... Когда функция У( ) закончит работу, и будет надлежащим образом уничтожена, а о2 останется в действительном состоянии.
Из данного фрагмента не ясно, кто отвечает за удаление рс и рг. если это ответственность функции У(), то она может либо перехватывать исключения и выполнять необходимые удаления, либо присваивать указатели локальным переменным типа аиго ртг. Более интересный вопрос заключается в следующем: когда библиотечные операции предлагают сильную гарантию (то есть либо успешно завершаются, либо оставляют операнды нетронутыми)? Например: чо14 1( чесэог<Х> а чх) ( их.!пэегэ(чх.Ьед!и()+4,Х(7) ); ~У добавляем элемент ) Вообще говоря, операции Хи аллокатор у эесгог<Х> могут генерировать исключения. Что можно сказать об элементах ох, когдау() завершается по исключению? Базовая гарантия утверждает, что нет утечек памяти, и что элементы их находятся в действительных состояниях. Но остается вопрос — изменился ли при этом ох? Не добавилось ли умолчательных значений Х? Мог ли элемент быть удален из-за того, что для 1плегг() это единственный способ восстановиться с соблюдением базовой гарантии? Иногда не достаточно знать, что контейнер находится в хорошем состоянии; часто нужно знать, что это за состояние.
После перехвата исключения нам обычно интересно знать, являются ли значения элементов теми, что мы ожидаем, или же нужно приступать к восстановлению после ошибок. Е.4.1. Вставка и удаление элементов Вставка элемента в контейнер и удаление элемента из контейнера — это очевидные примеры операций, которые в случае возникновения исключений могут оставить контейнер в непредсказуемом состоянии. Причина заключается в том, что вставка или удаление активизируют множеспю операций, которые могут генерировать исключения: 1. Новое значение копируется в контейнер. 2.