54. Избегайте срезки. Подумайте об использовании в базовом классе клонирования вместо копирования
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).
Исключения
Некоторые проектные решения могут требовать, чтобы копирующие конструкторы базовых классов оставались открытыми (например, когда часть вашей иерархии представляет собой библиотеку стороннего производителя). В таком случае следует предпочесть передачу посредством (интеллектуального) указателя передаче по ссылке; как показано в рекомендации 25, передача посредством указателя существенно менее подвержена срезке и нежелательному созданию временных объектов.
Ссылки
[Dewhurst03] §30, §76, §94 • [Meyers96] §13 • [Meyers97] §22 • [Stroustrup94] §11.4.4 • [Stroustrup00] §12.2.3
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
Проблемы при использовании
Проблемы при использовании Даже если положиться на то, что спрайты поддерживаются (почти) всеми браузерами на данный момент, все равно остается достаточно много вопросов, которые они не только не решают, а скорее сами создают. Во-первых, это проблемы при использовании
Вопросы защиты при использовании VPN
Вопросы защиты при использовании VPN Система VPN призвана повысить безопасность при обмене по сети. Однако она же может открыть злоумышленнику доступ к сетевым ресурсам. Взаимодействие компьютеров и сетей посредством VPN условно показано на рис. 26.1, 26.2 и 26.6. Однако из этих
Простота в использовании
Простота в использовании После правильной установки и настройки пользоваться системой VoIP-телефонии не сложнее, чем обычным телефоном. Принцип все тот же: вы снимаете трубку, ждете гудка, набираете номер, а когда собеседник отвечает, начинаете разговор. Конечно, если
2.1.9. Передача данных при использовании UDP
2.1.9. Передача данных при использовании UDP Мы наконец-то добрались до изучения того, ради чего сокеты и создавались: как передавать и получать с их помощью данные. По традиции начнем рассмотрение с более простого протокола UDP. Функции, которые рассматриваются в этом разделе,
Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе
Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:class TimeKeeper
Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений
Правило 25: Подумайте о поддержке функции swap, не возбуждающей исключений swap – интересная функция. Изначально она появилась в библиотеке STL и с тех пор стала, во-первых, основой для написания программ, безопасных в смысле исключений (см. правило 29), а во-вторых, общим
Работа с инструментами клонирования
Работа с инструментами клонирования
Несколько слов о классе System.Environment
Несколько слов о классе System.Environment Давайте рассмотрим класс System.Environment подробнее. Этот класс содержит ряд статических членов, позволяющих получить информацию относительно операционной системы, в которой выполняется .NET-приложение. Чтобы иллюстрировать возможности этого
Ввод и вывод в классе Console
Ввод и вывод в классе Console Вдобавок к членам, указанным в табл. 3.2, тип Console определяет множество методов, обрабатывающих ввод и вывод, причем все эти методы определены как статические (static), поэтому они вызываются на уровне класса. Вы уже видели, что WriteLine() вставляет
Пример клонирования
Пример клонирования Предположим, что класс Point содержит член ссылочного типа с именем PointDescription, обеспечивающий поддержку "понятного" имени объекта Point и его идентификационного номера в виде System.Guid (еcли у вас нет опыта применения COM, знайте, что GUID – глобально уникальный
17.5. Виртуальные функции в базовом и производном классах
17.5. Виртуальные функции в базовом и производном классах По умолчанию функции-члены класса не являются виртуальными. В подобных случаях при обращении вызывается функция, определенная в статическом типе объекта класса (или указателя, или ссылки на объект), для которого она
Информация о классе объекта (структура CRuntimeClass)
Информация о классе объекта (структура CRuntimeClass) Во многих случаях бывает необходимо уже во время работы приложения получить информацию о классе объекта и его базовом классе. Для этого любой класс, наследованный от базового класса CObject связан с структурой CRuntimeClass. Она
Информация о классе
Информация о классе Класс CObject содержит два метода: GetRuntimeClass и IsKindOf, позволяющих получить информацию о классе объекта.Виртуальный метод GetRuntimeClassВиртуальный метод GetRuntimeClass возвращает указатель на структуру CRuntimeClass, описывающую класс объекта, для которого метод был
Форма операций клонирования и эквивалентности
Форма операций клонирования и эквивалентности Форма вызова подпрограмм clone и equal является стилевой особенностью, которая может вызвать удивление. На первый взгляд нотация:clone (x)equal (x, y)выглядит не слишком объектно-ориентированной. Догматичное следование принципу
Nokia 3500 Classic: Два мегапиксела в среднем классе
Nokia 3500 Classic: Два мегапиксела в среднем классе Автор: Алексей Стародымов+приятный внешний вид,2-мегапиксельная камера,microSD на 128 Мбайт в комплекте -зернистый экран невысокого разрешения, отсутствует ИК-порт, в комплект не входит miniUSB- кабельNokia 3500 Classic, пожалуй, первый