Чтение онлайн

ЖАНРЫ

C++. Сборник рецептов

Когсуэлл Джефф

Шрифт:

return(buf_);

 }

private:

 void swapInternals(Messages msg) {

// Поскольку key_ не является встроенным типом данных, он может

// выбрасывать исключение, поэтому сначала выполняем действия с ним

swap(key_, msg.key_);

// Если предыдущий оператор не выбрасывает исключение, то выполняем

// действия со всеми переменными-членами, которые являются встроенными

// типами

swap(bufSize_, msg.bufSize_);

swap(initBufSize_, msg.initBufSize_);

swap(msgSize_, msg.msgSize_);

swap(buf_, msg.buf_);

 }

 int bufSize_;

 int initBufSize_;

 int msgSize_;

 char* buf;

 string key_;

}

Обсуждение

Вся

работа здесь делается конструктором копирования и закрытой функцией-членом
swapInternals
. Конструктор копирования инициализирует в списке инициализации элементарные члены и один из неэлементарных членов. Затем он распределяет память для нового буфера и копирует туда данные. Довольно просто, но почему используется такая последовательность действий? Вы могли бы возразить, что всю инициализацию можно сделать в списке инициализации, но такой подход может сопровождаться тонкими ошибками.

Например, вы могли бы следующим образом выделить память под буфер в списке инициализации.

Message(const Message& orig) :

 bufSize_(orig bufSize_), initBufSize_(orig initBufSize_),

 msgSize_(orig.msgSize_), key_(orig.key_),

 buf_(new char[orig.bufSize_]) {

copy(orig.buf_, orig.buf_+msgSize_, buf_);

}

Вы можете ожидать, что все будет нормально, так как если завершается неудачей выполнение оператора

new
, выделяющего память под буфер, все другие полностью сконструированные объекты будут уничтожены. Но это не гарантировано, потому что члены класса инициализируются в той последовательности, в которой они объявляются в заголовке класса, а не в порядке их перечисления в списке инициализации. Переменные-члены объявляются в следующем порядке.

int bufSize_;

int initBufSize_;

int msgSize_;

char* buf_;

string key_;

В результате

buf_
будет инициализироваться перед
key_
. Если при инициализации
key_
будет выброшено исключение,
buf_
не будет уничтожен, и у вас образуется участок недоступной памяти. От этого можно защититься путем использования в конструкторе блока
try/catch
(см. рецепт 9.2), но проще разместить оператор инициализации
buf_
в теле конструктора, что гарантирует его выполнение после операторов списка инициализации.

Выполнение функции

copy
не приведет к выбрасыванию исключения, так как она копирует элементарные значения. Но именно это место является тонким с точки зрения безопасности исключений: эта функция может выбросить исключение, если копируются объекты (например, если речь идет о контейнере, который параметризован типом своих элементов,
T
); в этом случае вам придется перехватывать исключение и освобождать связанную с ним память.

Вы можете поступить по-другому и копировать объект при помощи оператора присваивания,

operator=
. Поскольку этот оператор и конструктор копирования выполняют аналогичные действия (например, приравнивают члены моего класса к членам аргумента), воспользуйтесь тем, что вы уже сделали, и вы облегчите себе жизнь. Единственная особенность заключается в том, что вы можете сделать более привлекательным ваш программный код, используя закрытую функцию-член для обмена значений между данными-членами и временным объектом. Мне бы хотелось быть изобретателем этого приема, но я обязан отдать должное Гербу Саттеру (Herb Sutter) и Стефану Дьюхарсту (Stephen Dewhurst), в работе которых я впервые познакомился с этим подходом.

Возможно, вам все здесь ясно с первого взгляда, но я дам пояснения на тот случай, если это не так. Рассмотрим первую строку, в которой создается временный объект

tmp
с помощью конструктора копирования.

Message tmp(rhs);

В данном случае мы просто создали двойника объекта-аргумента. Естественно, теперь

tmp
эквивалентен
rhs
. После этого мы обмениваем значения его членов со значениями членов объекта
*this
.

swapInternals(tmp);

Вскоре я вернусь к функции

swapInternals
. В данный момент нам важно только то, что члены
*this
имеют значения, которые имели члены
tmp
секунду назад. Однако объект
tmp
представлял собой копию объекта
rhs
, поэтому теперь
*this
эквивалентен
rhs
. Но подождите: у нас по-прежнему имеется этот временный объект. Нет проблем, когда вы возвратите
*this
, tmp будет автоматически уничтожен вместе со старыми значениями переменных-членов при выходе за диапазон его видимости.

return(*this);

Все так. Но обеспечивает ли это безопасность при исключениях? Безопасно конструирование объекта

tmp
, поскольку наш конструктор является безопасным при исключениях. Большая часть работы выполняется функцией
swapInternals
, поэтому рассмотрим, что в ней делается, и безопасны ли эти действия при исключениях.

Функция

swapInternals
выполняет обмен значениями между каждым данным-членом текущего объекта и переданного ей объекта. Это делается с помощью функции
swap
, которая принимает два аргумента a и b, создает временную копию a, присваивает аргумент b аргументу а и затем присваивает временную копию аргументу b. В этом случае такие действия являются безопасными и нейтральными по отношению к исключениям, так как источником исключений здесь могут быть только объекты, над которыми выполняются операции. Здесь не используется динамическая память и поэтому обеспечивается базовая гарантия отсутствия утечки ресурсов.

Поскольку объект

key_
не является элементарным и поэтому операции над ним могут приводить к выбрасыванию исключений, я сначала обмениваю его значения. В этом случае, если выбрасывается исключение, никакие другие переменные-члены не будут испорчены. Однако это не значит, что не будет испорчен объект
key_
. Когда вы работаете с членами объекта, все зависит от обеспечения ими гарантий безопасности при исключениях. Если такой член не выбрасывает исключение, то это значит, что я добился своего, так как обмен значений переменных встроенных типов не приведет к выбрасыванию исключений. Следовательно, функция
swapInternals
является в основном и строгом смысле безопасной при исключениях.

Однако возникает интересный вопрос. Что, если у вас имеется несколько объектов-членов? Если бы вы имели два строковых члена, начало функции

swapInternals
могло бы выглядеть следующим образом.

void swapInternals(Message& msg) {

 swap(key_, msg key_);

 swap(myObj_, msg.myObj_);

 // ...

Существует одна проблема: если вторая операция

swap
выбрасывает исключение, как можно безопасно отменить первую операцию
swap
? Другими словами, теперь
key_
имеет новое значение, но операция
swap
для
myObj_
завершилась неудачей, поэтому
key_
теперь испорчен. Если вызывающая программа перехватывает исключение и попытается продолжить работу, как будто ничего не случилось, она теперь будет обрабатывать нечто отличное от того, что было в начале. Одно из решений — предварительно скопировать
key_
во временную строку, но это не гарантирует безопасность, так как при копировании может быть выброшено исключение.

Одно из возможных решений состоит в использовании объектов, распределенных в динамической памяти.

void swapInternals(Message& msg) {

 // key имеет тип string*, a myObj_ - тип MyClass*

 swap(key_, msg.key_);

 swap(myObj_, msg.myObj_);

Конечно, это означает, что теперь вам придется больше работать с динамической памятью, но обеспечение гарантий безопасности исключений будет часто оказывать влияние на ваш проект, поэтому будет правильно, если вы начнете думать об этом на ранних этапах процесса проектирования.

Поделиться с друзьями: