Б. Страуструп - Язык программирования С++. Специальное издание, 3-изд. Бином. 2004 (1160791), страница 234
Текст из файла (страница 234)
Стандарт не требует, чтобы реализация в точности соответствовала одной из представленных выше. Что на самом гарантирует стандарт — тема у ДА. Д.З.5. Конструкторы и инварианты С точки зрения безопасности исключений, другие операции вектора или эквивалентны уже исследованным (потому что они захватывают и освобождают ресурсы подобными же способами), или тривиальны (потому что они не выполняют операций, требующих искусства обеспечения действительных состояний). Однако для большинства классов именно подобные <тривиальные» функции составляют львиную долю кода. Трудность написания таких функций зависит главным образом от среды, которую устанавливает для их деятельности конструктор. Иными словами, сложность «обычных функций-членов» весьма чувствительна к выбору хорошего инварианта класса (у 24.3.7,1).
Изучение «тривиальных» функпий векторов поможет нам вникнуть в суть интересного вопроса о том, что делает инвариант класса хорошим, и как следует писать конструкторы, чтобы они устанавливали хорошие инварианты, Операции типа индексирования пес1ог Я 16.З.З) писать просто, потому что они могут полагаться на инвариант, установленный конструкторами и поддерживаемый всеми функциями, которые захватывшот или освобождают ресурсы. В частности, оператор индексирования вправе полагаться нато, что о ссылается на массив элементов: 1етр(а1е<с(алл Т, с1акк А> Т<, иестаг< Т, А>сорега1аг[](к1ке 1уре 1] ( г«1игп о[1]; Фундаментально важно, чтобы конструкторы захватили ресурсы и установили простой инвариант. Разберемся почему, рассмотрев альтернативное определение оес1ог базе: 1етр1а1е<с!аал Т, с(аллА = а11оса1аг<Т' //неуклюл<ее испол»зоканае конструктора с1алл аесгог Ьаке( 1ОЗЗ д.3.
Безопасные при исключениях методы реализации риЬИс А аИос; Т о; Т" врасе // распределитель памяти // начало отведенной полтти // конец последовательности злел1ентов, начало свободного // пространства, отведенного для возможного расширения Т'1аз1; // конец отведенного пространсгпва оес1ог Ьазе(сопзгАо а, 1урепатеА:вьее гуре и); аИос(а), о(0), зрисе(0), 1аз1(0) о = иИос.аИоса1е(п); юрасе = 1ав1 = о+ и; -оесгог Ьаве() ( (/(о) адов йеаИоса1е(о, 1аз1 — о); ) ); 1етр1а1е<с(авз Т //архоичный (' пред-стандартный, .//до-исключительныи") стиль с1аев оес1ог Ьаве ( риЬ1их Т ог Т' враге; // начало отведенной памяти // конец последовательности элементов, начало свободного // пространство, отведенного для возможного расширения // конец отведенного пространства Т< 1аз1; Здесь я создаю оес1ог Ьаве в два хода: сначала задаю «безопасное состояние», где о, врасе и 1ав1 установлены в О.
И лишь вслед за этим я пробую отвести память. Такая тактика вызвана следующим неуместным опасением: если во время распределения памяти под элементы произойдет исключение, конструктор может оставить после себя частично созданный объект. Это опасение неуместно, поскольку невозможно «оставить после себя» частично созданный объект и позднее обратиться к нему. Правила для статических объектов, автоматических объектов, объектов-членов и элементов контейнеров стандартной библиотеки предотвращают подобное развитие событий.
С другой стороны, это могло/может случиться в «достандартных» библиотеках, которые использовали/используют выделение памяти через пеш (з 10.4.11) для создания объектов в контейнерах, разработанных без учета безопасности исключений. Трудно сломать старые привычки. Отметим, что такая попытка написать код «побезопасцее» усложняет инвариант класса: больше не гарантируется, что о указывает на отведенную память.
Теперь о может 'быть О, что немедленно приводит к издержкам. Требования стандартной библиотеки для распределителей памяти не гарантируют безопасности освобождения указателя со значением 0 (9 19А.1). Этим распределители памяти отличаются от Ие!е1е (5 6.2.6). Так что мне пришлось предусмотреть в деструкторе проверку.
Кроме того каждый элемент сначала инициализируется О, а уж затем ему присваивается значение. Стоимость выполнения дополнительной работы может быть существенна для тех типов, для которых присваивание нетривиально, вроде в1г(лИ и Йз1. Двухступенчатый конструктор — не такой уж необычный стиль. Иногда двухступенчатость даже делается явной, так что конструктор производит только некоторую «простую и безопасную» инициализацию, помещая объект в уничтожимое состояние.
Реальное же создание объекта оставлено функции (л11(), которую пользователь должен явно вызвать. Например; 1034 Приложение Д. Безопасность исключений и стандартная библиотека оес1ог базе(): о(О), крисе(О), 1азг(О) () -оесгог базе() ( / ее(о); ) // возвраи(ает 1гие, если иничиаяазаиия успешна Ьоо! т11(з!ее 1п) ( 1/(о = (т") та!!ос(згееоЯ ЯП)) ( ипгп111а11еед 31!(о, о е и, 1()); красе = !аз1 = о + и; ге1игп 1гие; ) ге1игп /а!зе; ); Кажущаяся ценность данного стиля состоит в том, что: 11) Конструктор не генерирует исключений, а успешность инициализации !п11() можно проверить «обычными» (то есть без исключений) средствами.
)2] Существует тривиальное действительное состояние. При наличии серьезной проблемы операция может придать объекту это сос гояние. [31 Выделение ресурсов отложено до момента, когда на самом деле понадобится полностью инициализированный объект. Эти пункты изучаются в следу!ощих подразделах, где показывается, почему двухступенчатая методика создания не приносит ожидаемых выгод. Она может также оказаться источником проблем. Д.3.5.1. Использование функций )п)1() Первый пункт (исполвзование функции (п!1() вместо полноценного конструктора)— липа.
Применение конструкторов и обработка исключений — более общий и систематический способ запроса ресурсов и работы с ощибками инициализации (в 14.1, в 14А). Рассматриваемый двухступенчатый стиль — пережиток С++ в варианте до введения механизма исключений. Тщательно написанные коды с использованием двух сравниваемых стгщей— приблизительно эквивалентны. Вот примеры: т1~1(1п1 и) ( оесгог<Х> о; //- (/(о.(п11(п)) ( // использование о как вектора из и элементов ) е/зе ( // обработка проб«пни ) т1У2(т1 п) 1гу ( оестог о«Х> о(п); 1035 Д.З. Безопасные при исключениях методы реализации //- // использование о как вектора из и элементов ) сагсЬ(...) ( // обработка проблеиы При этом наличие отдельной функции !о(!() — это возможность: (1) забыть вызвать (п(т() Я 10.2.3); (2) забыть проверить успешность |л!!(); [3) забыть, что (и!!() может сгенерировать исключение; и (4) использовать объект до вызова (и!!().
Определение оес!ог<Т>с/о!!() иллюстрирует (3). В хорошей реализации С++~2() чуть-чуть быстрее~1() потому что в общем случае избегает проверки. Д.З.5.2. Надежда на действительное состояние по умолчанию Второй пункт (налнчие просто конструируемого действительного состояния по умолчанию) в принципе правилен, но в случае оес!ог он ведет к ненужным расходам. Теперь оес!ог Ьаве может содержать о==О, так что реализация оес!ог должна повсеместно защищаться от этого. Например; !етр!а!е«с!авв Т> Тб оес!ог<Т»серега!ог()(з!ее ! !) ( з/(о) ге!игп о(!), // обработка оптбкн Наличие возможности о==О делает стоимость индексирования без проверки на соот- ветствие диапазону эквивалентной стоимости доступа с проверкой диапазона; гетр!а!е<с!авв Т> ТЙ оесгог<Т»эа!(в!ее ! 0 !/(! < о в!ее()) ге!иго о(!(; !беат ои! оУ галие("оесгог !пг!ек"! Главное здесь то, что вводя возможности о =О, я усложнил основной инвариант оес!ог Ьаве.
Соответственно усложнился и основной инвариант оес!ог, В результате, чтобы справиться с новыми инвариантами усложнился весь код в оестог и оес!ог Ьаве. Это — источник возможных ошибок, трудностей поддержки и накладных расходов этапа выполнения. Обратите внимание, что условные инструкции могут быть неожиданно дорогостоящими на машинах с современной архитектурой. Там где важна производительность, решающее значение может иметь реализация ключевых операций (типа индексирования вектора) без условных инструкций. Интересно что первоначальное определение оес!ог Ьаве уже содержало просто создаваемое действительное состояние. Объект оес!ог Ьаве не мог существовать, если начальное распределение памяти не было успешным.
Следовательно, разработчик оес!ог мог написать функцию «запасного выхода» подобную следующей; Приложение Д. Безопасность исключений и стандартная библиотека 1035 1етр1аге<с1изз Т, с!азз А> иаЫ иес1аг Т, А> етегуепсу ех/1() ( зрасе = и, 1Ьгаги Та!и! ~а1!иге(! // углпанавлеыие размера ' уиз в 0 Это излишне «круто»: действуя так, не удается вызвать деструкторы элементов и ос- вободить пространство элементов, удерживаемое иес1ог Ьазв.