Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Саттер Герб. Стандарты программирования на С++. 101 правило и рекомендация - royallib.ru.doc
Скачиваний:
58
Добавлен:
11.03.2016
Размер:
715.24 Кб
Скачать

Обсуждение

Распространенной ошибкой (и не только среди новичков) является игнорирование семантики копирования и присваивания при определении класса. Это характерно для маленьких вспомогательных классов, таких как предназначенные для поддержки идиомы RAII (см. рекомендацию 13).

Убедитесь, что ваш класс предоставляет осмысленное копирование (или не предоставляет его вовсе). Вот возможные варианты.

Явное запрещение обеих функций. Если копирование для вашего типа лишено смысла, запретите как копирующее конструирование, так и копирующее присваивание, объявив их как закрытые нереализованные функции:

class T { // ...

private:                 // Делаем T некопируемым

 T(const T&);            // Функция не реализована

 T& operator=(const T&); // Функция не реализована

};

• Явное написание обеих функций. Если для объектов T предусмотрены копирование и копирующее присваивание, но корректное копирующее поведение отличается от поведения сгенерированных компилятором версий, то следует явно написать обе функции и сделать их не закрытыми.

• Использование сгенерированных компилятором версий, предпочтительно с явным комментарием. В противном случае, если копирование имеет смысл и поведение по умолчанию корректно, эти функции можно не объявлять самостоятельно и позволить компилятору самому сгенерировать их. Следует явно комментировать корректность поведения по умолчанию, чтобы читатели вашего кода знали, что вы преднамеренно не объявили данные функции.

Заметим, что запрещение копирования и копирующего присваивания означает, что вы не можете поместить объекты T в стандартные контейнеры. Это не обязательно плохо; очень может быть, что вы в любом случае не захотите хранить такие объекты в контейнерах. (Тем не менее, вы все равно можете поместить эти объекты в контейнер, если будете хранить их посредством интеллектуальных указателей; см. рекомендацию 79).

Вывод: будьте внимательны при работе с этими двумя операциями, так как компилятор имеет тенденцию к самостоятельной их генерации, а эти сгенерированные версии зачастую небезопасны для типов, не являющихся значениями (см. также рекомендацию 32).

Ссылки

[Dewhurst03] §88 • [Meyers97] §11 • [Stroustrup00] §11.2.2

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. Что приводит к полной потере динамического, полиморфного поведения, из-за которого в первую очередь и используется наследование.

Если, как автор класса В, вы хотите разрешить срезку, но не хотите, чтобы она могла происходить по ошибке, для такого случая существует один вариант действий, о котором мы упомянем для полноты изложения, но не рекомендуем использовать его в коде, к которому предъявляется требование переносимости: вы можете объявить копирующий конструктор В как explicit. Это может помочь избежать неявной срезки, но кроме этого запрещает все передачи параметров данного типа по значению (что может оказаться вполне приемлемым для базовых классов, объекты которых все равно не должны создаваться; см. рекомендацию 35).

// Объявляем копирующий конструктор как explicit (у данного

// решения имеется побочное действие, так что требуется

// улучшение этого метода)

class B { // ...

public:

 explicit B(const B& rhs);

};

class D : public B { /* ... */ };

Вызывающий код все равно в состоянии выполнить срезку, если это необходимо, но должен делать это явно:

void Transmogrify(B obj); // Теперь эта функция вообще не

                          // может быть вызвана (!)

void Transmogrify2(const B& obj) // Идиома для намерения в

{                                // любом случае получить

 В b( obj );                     // параметр obj по значению

 // ...                          // (с возможной срезкой)

}

B b;              // Базовые классы не должны быть конкретными

D d;              // (см. рекомендацию 35), но допустим это

Transmogrify(b);  // Должна быть ошибка (см. примечание)

Transmogrify(d);  // Должна быть ошибка (см. примечание)

Transmogrify2(d); // Все в порядке

Примечание: на момент написания данной рекомендации некоторые компиляторы ошибочно допускали один или оба приведенных вызова функции Transmogrify. Эта идиома вполне стандартна, но (пока что) не полностью переносима.

Имеется лучший способ предупреждения срезки, с более высокой степенью переносимости. Пусть, например, функция наподобие Transmogrify в действительности хочет получить полную глубокую копию без информации о действительном производном типе переданного объекта. Более общее идиоматическое решение состоит в том, чтобы сделать копирующий конструктор базового класса защищенным (чтобы функция наподобие Transmogrify не могла случайно его вызвать), а вместо него воспользоваться виртуальной функцией Clone:

// добавление функции Clone (уже лучше, но все еще требуется

// усовершенствование)

class B { // ...

public:

 virtual B* Clone() const = 0;

protected:

 B(const B&);

};

class D : public B { // ...

public:

 virtual D* Clone() const { return new D(*this); }

protected:

 D( const D& rhs ): B(rhs) {/*...*/ }

};

Теперь попытка срезки будет (переносимо) генерировать ошибку времени компиляции, а объявление функции Clone как чисто виртуальной заставляет непосредственный производный класс перекрыть ее. К сожалению, с данным решением все еще связаны две проблемы, которые компилятор не в состоянии обнаружить: в классе, производном от производного, функция Clone может оказаться неперекрытой, а перекрытие Clone может реализовать ее некорректно, так что копия будет не того же типа, что и оригинал. Функция Clone должна следовать шаблону проектирования Nonvirtual Interface (NVI; см. рекомендацию 39), который разделяет открытую и виртуальную природы Clone и позволяет вам использовать ряд важных проверок:

class В { // ...

publiс:

 B* Clone() const { // Невиртуальная функция

  B* р = DoClone();

  assert(typeid(*p) == typeid(*this) &&

   "DoClone incorrectly overridden");

  return p; // проверка типа, возвращаемого DoClone

 }

protected:

 B(const B&);

private:

 virtual B* DoClone() const = 0;

};

Функция Clone теперь является невиртуальным интерфейсом, используемым вызывающим кодом. Производные классы должны перекрыть функцию DoClone. Дополнительная проверка обнаружит все копии, которые имеют тип, отличный от оригинала, тем самым оповещая, что в некотором производном классе не перекрыта функция DoClone; в конце концов, задача assert состоит именно в обнаружении и сообщении о таких программных ошибках (см. рекомендации 68 и 70).