Довольно часто можно встретить код на C++, в котором у одного или нескольких классов конструктор копирования и оператор присваивания объявлены private и написан комментарий вида «копирование запрещено».
Прием простой и с виду очевидный, тем не менее, при его использовании возможны подводные камни, приводящие к ошибкам, которые проявятся нескоро и поиск которых может занять не один день.
Рассмотрим возможные проблемы.
Сначала – краткий экскурс, зачем нужен этот прием.
Если программа пытается скопировать объект класса, компилятор C++ по умолчанию автоматически генерирует конструктор копирования или оператор присваивания, если они не объявлены в классе явно. Автоматически сгенерированный конструктор выполняет почленное копирование, а автоматически сгенерированный оператор присваивания – почленное присваивание.
Вот простой пример:
class DoubleDelete {
public:
DoubleDelete()
{
ptr = new char[100];
}
~DoubleDelete()
{
delete[] ptr;
}
private:
char* ptr;
};
В этом коде:
{
DoubleDelete first;
DoubleDelete second( first );
}
возникнет неопределенное поведение. Будет вызван сгенерированный компилятором конструктор копирования, который скопирует указатель. В результате оба объекта будут хранить указатели с равными адресами. Первым отработает деструктор объекта second, он выполнит delete[], затем будет вызван деструктор объекта first, он попытается выполнить delete[] повторно для того же адреса, и это приведет к неопределенному поведению.
Решение вроде бы очевидно – реализовать конструктор копирования и оператор присваивания с правильным поведением. Например, при копировании новый объект создает свой массив и копирует в него данные из старого.
Это не всегда верный путь. Не все объекты по своей сути подлежат копированию.
Например, объект может хранить дескриптор открытого файла в виде целого числа. Деструктор «закрывает файл» с помощью функции операционной системы. Очевидно, почленное копирование не подойдет. А что должно происходить при копировании? Должен ли файл открываться еще раз? Обычно в этом случае копирование не имеет смысла.
Другой пример – класс-скобка для захвата критической секции. Какой смысл ее копировать? Секция уже захвачена.
Краткий экскурс на этом закончен, переходим к попытке изобразить решение.
Если копирование объекта не имеет смысла, нужно сделать так, чтобы компилятор не смог случайно его выполнить. Для этого обычно делают так:
// NOT BAD
class NonCopyable {
// blahblahblahpublic:
private:
// copy and assignment prohibited
NonCopyable( const NonCopyable& );
void NonCopyable::operator=( const NonCopyable& );
};
или так:
// FAIL
class NonCopyable {
// blahblahblahpublic:
private:
// copy and assignment prohibited
NonCopyable( const NonCopyable& ) { assert( false ); }
void NonCopyable::operator=( const NonCopyable& ) { assert( false ); }
};
или так:
// EPIC FAIL
class NonCopyable {
// blahblahblahpublic:
private:
// copy and assignment prohibited
NonCopyable( const NonCopyable& ) {}
void NonCopyable::operator=( const NonCopyable& ) {}
};
Все три способа встречаются в реальном коде.
Казалось бы, чем второй и третий варианты отличаются от первого? Модификатор private в любом случае не даст вызвать копирование.
КРАЙНЕ НЕОЖИДАННО…
Функции-члены того же класса могут вызывать конструктор копирования и оператор присваивания, даже если те объявлены private. И «друзья» класса (friend) тоже могут. Никто не мешает написать в коде что-нибудь такое:
NonCopyable NonCopyable::SomeMethod()
{
// blahblahblah
return *this;
}
или такое:
void NonCopyable::SomeOtherMehod()
{
callSomething( *this );
}
Теперь налицо разница между первым вариантом и остальными.
Первый вариант (нет реализации) приведет к ошибке во время компоновки программы. Сообщение об ошибке не самое понятное, но, по крайней мере, надежное.
Во втором варианте будет срабатывать assert… при условии, что управление пройдет через этот код. Здесь многое зависит от того, насколько часто этот код вызывается, в частности, от покрытия кода тестами. Может быть, вы заметите проблему при первом же запуске, может быть – очень нескоро.
В третьем варианте еще лучше – оператор присваивания не меняет объект, а конструктор копирования вызывает конструкторы по умолчанию всех членов класса. Широкий простор для ошибок, заметить может быть еще сложнее, чем второй.
Ожидаемое возражение – раз конструктор копирования и оператор присваивания объявлены, но не определены, их можно по ошибке или злонамеренно определить где угодно в коде. Эта проблема решается очень просто.
От ошибки помогает комментарий вида «запрещенные операции» или, если есть сомнения, «запрещенные операции, не определять под страхом увольнения». От злого умысла не поможет ничто – в C++ никто не мешает взять адрес объекта, привести его к типу char* и побайтово перезаписать объект как угодно.
В C++0x есть ключевое слово delete:
// C++0x OPTIMAL
class NonCopyable {
private:
// copy and assignment not allowed
NonCopyable( const NonCopyable& ) = delete;
void NonCopyable( const NonCopyable& ) = delete;
// superior developers wanted – www.abbyy.ru/vacancy
};
В этом случае не только определить, но и вызвать их будет невозможно – при попытке компиляции места вызова будет выдана ошибка компиляции.
Вариант «объявить и не определять» доступен и ранее C++0x, его, в частности, использует boost::noncopyable. Вариант наследоваться от boost::noncopyable или аналогичного своего класса тоже достаточно надежен и доступен в любой версии.
Внимательный читатель наверняка обратил внимание, что во всех примерах выше оператор присваивания возвращает void, а не ссылку на тот же класс. Это сделано специально, чтобы конструкции вида
first = second = third;
вызывали ошибку компиляции в С++03.
Так небольшие улучшения кода иногда помогают избежать премии Дарвина.
Дмитрий Мещеряков,
департамент продуктов для разработчиков
Автор: DmitryMe