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.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
Безопасное упрощение
Безопасное упрощение Как и в примере с фотогалереей, который обсуждался ранее в этой главе, брызги CSS3, которые мы добавляем в этом примере, никак не затрагивают браузеры, которые пока что не поддерживают эти свойства.В итоге самое важное, что каждый из этих элементов –
Совет 41: Безопасное скачивание
Совет 41: Безопасное скачивание Если вам приглянулась полезная программка, не стоит скачивать ее по первой ссылке, выданной поисковиком, — программа, возможно, будет работать, вот только вместе с ней вам завернут неприятный сюрприз. Ведь один из способов распространения
Понятия объекта и экземпляра объекта
Понятия объекта и экземпляра объекта В начале этой главы мы познакомились с типами данных, определяющими саму природу данных и набор действий, которые можно выполнять с этими данными. Так, строковый тип определяет, что данные этого типа представляют собой строки —
Понятия объекта и экземпляра объекта
Понятия объекта и экземпляра объекта В начале этой главы мы познакомились с типами данных, определяющими саму природу данных и набор действий, которые можно выполнять с этими данными. Так, строковый тип определяет, что данные этого типа представляют собой строки —
Безопасное администрирование
Безопасное администрирование Как мы выяснили ранее, домены не продаются, но за деньги можно приобрести право управления доменным именем. Права управления, полученные администратором домена, требуют надежного фиксирования и не менее надежных механизмов авторизации,
38. Практикуйте безопасное перекрытие
38. Практикуйте безопасное перекрытие РезюмеОтветственно подходите в перекрытию функций. Когда вы перекрываете виртуальную функцию, сохраняйте заменимость; в частности, обратите внимание на пред- и постусловия в базовом классе. Не изменяйте аргументы по умолчанию
3.1. Природа объекта
3.1. Природа объекта Что является и что не является объектом? Способностью к распознанию объектов физического мира человек обладает с самого раннего возраста. Ярко окрашенный мяч привлекает внимание младенца, но, если спрятать мяч, младенец, как правило, не пытается его
8.8. Безопасное хранение файлов в песочнице приложения
8.8. Безопасное хранение файлов в песочнице приложения Постановка задачи Требуется, чтобы iOS защищала файлы, расположенные в песочнице вашего приложения, от несанкционированного считывания. Такое считывание, в частности, могут выполнять файловые менеджеры для iOS, которые
Продолжение объекта
Продолжение объекта Snap to Extension – привязка к продолжениям объектов.Она необходима в том случае, когда при построении объектов требуется использовать линии, являющиеся временным продолжением существующих линий и дуг. Данный режим можно совмещать с режимом Apparent Intersect с
9.1.7. Безопасное связывание A
9.1.7. Безопасное связывание A При использовании перегрузки складывается впечатление, что в программе можно иметь несколько одноименных функций с разными списками параметров. Однако это лексическое удобство существует только на уровне исходного текста. В большинстве
Разрыв объекта
Разрыв объекта Команда BREAK позволяет удалять части объекта или разбивать объект на два. Для ее запуска следует нажать кнопку Break (Разрыв) на вкладке Home (Основная) в группе Modify (Редактирование) ленты или на панели инструментов Modify (Редактирование). Можно также выполнить
Софтерра: Безопасное катание на слонах
Софтерра: Безопасное катание на слонах Автор: Феликс МучникВ стародавние времена путешествие на слонах было для европейцев опасным и трудным и вряд ли доставляло большое удовольствие. Это было просто необходимостью, ведь другого транспорта для пересечения джунглей не
Безопасное извлечение USB-устройств
Безопасное извлечение USB-устройств Казалось бы, для чего создавать такую тему? Многим известно, что перед тем, как выдернуть флэшку, нужно путем нажатия на значок в системном трее безопасно извлечь устройство. Это понятно, но что делать, если такого значка не появляется?
Освещенность объекта
Освещенность объекта Освещенность объекта съемки прямо пропорциональна яркости источника света. При увеличении яркости света в два раза освещенность предмета также увеличится вдвое. При искусственном свете освещенность зависит от расстояния между источником света и