Распространенной ошибкой (и не только среди новичков) является игнорирование семантики копирования и присваивания при определении класса. Это характерно для маленьких вспомогательных классов, таких как предназначенные для поддержки идиомы RAII (см. рекомендацию 13).
Убедитесь, что ваш класс предоставляет осмысленное копирование (или не предоставляет его вовсе). Вот возможные варианты.
class T { // ...
private: // Делаем T некопируемым
T(const T&); // Функция не реализована
T& operator=(const T&); // Функция не реализована
};
• T
предусмотрены копирование и копирующее присваивание, но корректное копирующее поведение отличается от поведения сгенерированных компилятором версий, то следует явно написать обе функции и сделать их не закрытыми.
•
Заметим, что запрещение копирования и копирующего присваивания означает, что вы не можете поместить объекты T
в стандартные контейнеры. Это не обязательно плохо; очень может быть, что вы в любом случае не захотите хранить такие объекты в контейнерах. (Тем не менее, вы все равно можете поместить эти объекты в контейнер, если будете хранить их посредством интеллектуальных указателей; см. рекомендацию 79).
Вывод: будьте внимательны при работе с этими двумя операциями, так как компилятор имеет тенденцию к самостоятельной их генерации, а эти сгенерированные версии зачастую небезопасны для типов, не являющихся значениями (см. также рекомендацию 32).
54. Избегайте срезки. Подумайте об использовании в базовом классе клонирования вместо копирования
Срезка объектов происходит автоматически, невидимо и может приводить к полному разрушению чудесного полиморфного дизайна. Подумайте о полном запрете копирующего конструктора и копирующего присваивания в базовых классах. Вместо них можно использовать виртуальную функцию-член Clone
, если пользователям вашего класса необходимо получать полиморфные (полные, глубокие) копии.
Когда вы строите иерархию классов, обычно она предназначена для получения полиморфного поведения. Вы хотите, чтобы объекты, будучи созданными, сохраняли свой тип и идентичность. Эта цель вступает в конфликт с обычной семантикой копирования объектов в C++, поскольку копирующий конструктор не является виртуальным и не может быть сделан таковым. Рассмотрим следующий пример:
class B { /* ... */ };
class D : public B { /* ... */ };
// Оп! Получение объекта по значению
void Transmogrify(B obj);
void Transubstantiate(B& obj) { // Все нормально -
// передача по ссылке
Transmogrify(obj); // Плохо! Срезка объекта!
// ...
}
D d;
Transubstantiate(d);
Программист намерен работать с объектами В
и производных классов полиморфно. Однако, по ошибке (или усталости — к тому же и кофе закончился…) программист или просто забыл написать &
в сигнатуре Transmogrify
, или собирался создать копию, но сделал это неверно. Код компилируется без ошибок, но когда функция Transmogrify
вызывается с передачей ей объекта D
, он мутирует в объект B. Это связано с тем, что передача по значению приводит к вызову B::В(const B&)
, т.е. копирующего конструктора В
, параметр которого const B&
представляет собой автоматически преобразованную ссылку на d
. Что приводит к полной потере динамического, полиморфного поведения, из-за которого в первую очередь и используется наследование.