...
private:
Bitmap *pb; // указатель на объект, размещенный в куче
};
Ниже приведена реализация оператора присваивания operator=, которая выглядит совершенно нормально, но становится опасной в случае выполнения присваивания самому себе (она также небезопасна с точки зрения исключений, но сейчас не об этом).
Widget&
Widget::operator=(const Widget& rhs) // небезопасная реализация operator=
{
delete pb; // прекратить использование текущего
// объекта Bitmap
pb = new Bitmap(*rhs.pb); // начать использование копии объекта
// Bitmap, указанной в правой части
return *this; // см. правило 10
}
Проблема состоит в том, что внутри operator= *this (чему присваивается значение) и rhs (что присваивается) могут оказаться одним и тем же объектом. Если это случится, то delete уничтожит не только Bitmap, принадлежащий текущему объекту, но и Bitmap, принадлежащий объекту в правой части. По завершении работы этой функции Widget, который не должен был бы измениться в процессе присваивания самому себе, содержит указатель на удаленный объект!
Традиционный способ предотвратить эту ошибку состоит в том, что нужно выполнить проверку совпадения в начале operator=:
Widget&
Widget::operator=(const Widget& rhs) // небезопасная реализация operator=
{
if(this == &rhs) return *this; // проверка совпадения: если
// присваивание самому себе, то
// ничего не делать
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
Это решает проблему, но я уже упоминал, что предыдущая версия оператора присваивания была не только опасна в случае присваивания себе, но и небезопасна в смысле исключений, и последняя опасность остается актуальной во второй версии. В частности, если выражение «new Bitmap» вызовет исключение (либо по причине недостатка свободной памяти, либо исключение возбудит конструктор копирования Bitmap), то Widget также будет содержать указатель на несуществующий Bitmap. Такие указатели – источник неприятностей. Их нельзя безопасно удалить, их даже нельзя разыменовывать. А вот потратить массу времени на отладку, выясняя, откуда они взялись, – это можно.
К счастью, существует способ одновременно сделать operator= безопасным в смысле исключений и безопасным по части присваивания самому себе. Поэтому все чаще программисты не занимаются специально присваиванием самому себе, а сосредоточивают усилия на достижении безопасности в смысле исключений. В правиле 29 эта проблема рассмотрена детально, а сейчас достаточно упомянуть, что во многих случаях продуманная последовательность операторов присваивания может обеспечить безопасность в смысле исключений (а заодно безопасность присваивания самому себе) кода. Например, ниже мы просто не удаляем pb до тех пор, пока не скопируем то, на что он указывает:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // запомнить исходный pb
pb = new Bitmap(*rhs.pb); // установить указатель pb на копию *pb
delete pOrig; // удалить исходный pb
return *this;
}
Теперь, если «new Bitmap» возбудит исключение, то pb (и объект Widget, которому он принадлежит) останется неизменным. Даже без проверки на совпадение здесь обрабатывается присваивание самому себе, потому что мы сделали копию исходного объекта Bitmap, удалили его, а затем направили указатель на сделанную копию. Возможно, это не самый эффективный способ обработать присваивание самому себе, но он работает.
Если вы печетесь об эффективности, то можете вернуть проверку на совпадение в начале функции. Но прежде спросите себя, как часто может происходить присваивание самому себе, потому что выполнение проверки тоже не обходится даром. Это делает код (исходный и объектный) чуть больше, а ветвление несколько снижает скорость исполнения. Эффективность предварительной выборки команд, кэширования и конвейеризации тоже может пострадать.
Альтернативой ручному упорядочиванию предложений в operator= может быть обеспечение и безопасности в смысле исключений, и безопасности присваивания самому себе за счет применения техники «копирования с обменом» («copy and swap»). Она тесно связана с безопасностью в смысле исключений, поэтому рассматривается в правиле 29. Тем не менее это достаточно распространенный способ написания operator=, и на него стоит взглянуть:
class Widget {
...
void swap(Widget& rhs); // обмен данными *this и rhs
... // см. подробности в правиле 29
};
Widget& Widget:: operator=(const Widget& rhs)
{
Widget temp(rhs); // создать копию данных rhs
swap(tmp); // обменять данные *this с копией
return *this;
}
Здесь мы пользуемся тем, что: (1) оператор присваивания можно объявить как принимающим аргумент по значению и (2) передача объекта по значению означает создание копии этого объекта (см. правило 20):
Widget& Widget::operator=(Widget rhs) // rhs – копия переданного объекта
{ // обратите внимание на передачу по
// значению
swap(rhs); // обменять данные *this с копией
return *this;
}
Лично меня беспокоит, что такой подход приносит ясность в жертву изощренности, но, перемещая операцию копирования из тела функции в конструирование параметра, компилятор иногда может сгенерировать более эффективный код.
• Убедитесь, что operator= правильно ведет себя, когда объект присваивается самому себе. Для этого можно сравнить адреса исходного и целевого объектов, аккуратно упорядочить предложения или применить идиому копирования обменом.
• Убедитесь, что все функции, оперирующие более чем одним объектом, ведут себя корректно при совпадении двух или более объектов.