// Поскольку 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
. Конструктор копирования инициализирует в списке инициализации элементарные члены и один из неэлементарных членов. Затем он распределяет память для нового буфера и копирует туда данные. Довольно просто, но почему используется такая последовательность действий? Вы могли бы возразить, что всю инициализацию можно сделать в списке инициализации, но такой подход может сопровождаться тонкими ошибками.
Например, вы могли бы следующим образом выделить память под буфер в списке инициализации.
Вы можете ожидать, что все будет нормально, так как если завершается неудачей выполнение оператора
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_);
Конечно, это означает, что теперь вам придется больше работать с динамической памятью, но обеспечение гарантий безопасности исключений будет часто оказывать влияние на ваш проект, поэтому будет правильно, если вы начнете думать об этом на ранних этапах процесса проектирования.