9.5. Безопасное копирование объекта

9.5. Безопасное копирование объекта

Проблема

Требуется иметь безопасные при исключениях конструктор копирования и оператор присваивания базового класса.

Решение

Примените тактику, предложенную в рецепте 9.4, а именно сначала выполните все действия, которые могут выбрасывать исключения, и изменяйте состояние объектов с помощью операций, которые не могут выбрасывать исключения только после того, как будет завершена вся опасная работа. В примере 9.6 вновь представлен класс Message, который на этот раз содержит определения оператора присваивания и конструктора копирования.

Пример 9.6. Безопасные при исключениях оператор присваивания и конструктор копирования

#include <iostream>

#include <string>

const static int DEFAULT_BUF_SIZE = 3;

const Static int MAX_SIZE = 4096;

class Message {

public:

 Message(int bufSize = DEFAULT_BUF_SIZE) :

  bufSize_(bufSize), initBufSize_(bufSize), msgSize_(0), key_("") {

   buf_ = new char[bufSize]; // Примечание: теперь это делается в теле

                            // конструктора

 }

 ~Message() {

  delete[] buf_;

 }

 // Безопасный при исключениях конструктор копирования

 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_); // Здесь нет

 }

 // Безопасный при исключениях оператор присваивания использующий

 // конструктор копирования

 Message& operator=(const Message& rhs) {

  Message tmp(rhs); // Копировать сообщение во временную переменную,

                    // используя конструктор копирования

  swapInternals(tmp); // Обменять значения переменных-членов и членов

                      // временного объекта

  return(*this); // После выхода переменная tmp уничтожается вместе

                 // с первоначальными данными

 }

 const char* data() {

  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_);

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

Основной лейтмотив этого рецепта не отличается от лейтмотива предыдущих рецептов, связанных с обеспечением безопасности исключений. Сначала выполняйте действия, которые могут создать проблемы, предусмотрите блок try/catch на тот случай, если что-то пойдет не так, и в последнем случае выполните подчистку за собой. Если все проходит нормально, поздравьте себя и обновите состояние объекта.

Смотри также

Рецепт 9.2 и рецепт 9.3.