• Перекладывание ответственности. Можно просто документировать необходимость вызова в пользовательском коде постконструктора после создания объекта.
• Отложенная постинициализация. Можно выполнять постинициализацию при первом вызове функции-члена, для чего использовать в базовом классе флаг типа bool
, который показывает, был уже вызван постконструктор или нет.
• Использование семантики базового класса. Правила языка требуют, чтобы конструктор наиболее производного класса определял, какой из конструкторов базовых классов будет вызван. Вы можете использовать это правило в своих целях (см. [Taligent94]).
• Использование функции-фабрики. Таким способом вы можете легко обеспечить принудительный вызов функции-постконструктора (см. примеры к данной рекомендации).
Ни одна из методик постконструирования не является идеальной. Наихудшее, что можно сделать, — это потребовать, чтобы пользователь класса вручную вызывал постконструктор. Однако даже наилучшие способы решения данной задачи требуют отличного от обычного синтаксиса конструирования объекта (что легко проверяется в процессе компиляции) и/или сотрудничества с авторами производных классов (что невозможно проверить в процессе компиляции).
Примеры
Пример. Использование функции-фабрики для принудительного вызова постконструктора. Рассмотрим следующий код:
class B { // Корень иерархии
protected:
В() {/*...*/ }
virtual void PostInitialize() // Вызывается сразу
{/*...*/} // после конструирования
publiс:
template
static shared_ptr Create() // Интерфейс для
{ // создания объектов
shared_ptr p(new T);
p->PostInitialize();
return p;
}
};
class D : public B { /* ... */ }; // Некоторый производный
// класс
shared_ptr p = D::Create(); // Создание объекта D
Этот не вполне надежный дизайн основан на ряде компромиссов.
• Производные классы, такие как D
, не должны иметь открытых конструкторов. В противном случае пользователи D
смогут создавать объекты D
, для которых не будет вызываться функция PostInitialize
.
• Создание объекта использует оператор new
, который, однако, может быть перекрыт классом В
(см. рекомендации 45 и 46).
• Класс D
обязательно должен определить конструктор с теми же параметрами, что и у конструктора класса B
. Смягчить проблему может наличие нескольких перегрузок функции Create
; эти перегрузки могут даже быть шаблонами.
• Если перечисленные требования удовлетворены, данный дизайн гарантирует, что функция PostInitialize
будет вызвана для любого полностью сконструированного объекта класса, производного от B
. Функция PostInitialize
не обязательно должна быть виртуальной; однако она может свободно вызывать виртуальные функции.
Ссылки
[Alexandrescu01] §3 • [Boost] • [Dewhurst03] §75 • [Meyers97] §46 • [Stroustrup00] §15.4.3 • [Taligent94]
50. Делайте деструкторы базовых классов открытыми и виртуальными либо защищенными и невиртуальными
Резюме
Удалять или не удалять — вот в чем вопрос! Если следует обеспечить возможность удаления посредством указателя на базовый класс, то деструктор базового класса должен быть открытым и виртуальным. В противном случае он должен быть защищенным и невиртуальным.
Обсуждение
Это простое правило иллюстрирует достаточно тонкий вопрос и отражает современное использование наследования и принципов объектно-ориентированного проектирования.
Для данного базового класса Base
вызывающий код может пытаться удалять производные от него объекты посредством указателей на Base
. Если деструктор Base
открытый и невиртуальный (свойства по умолчанию), он может быть случайно вызван для указателя, который в действительности указывает на производный объект, и в этом случае мы получим неопределенное поведение. Такое состояние дел привело к тому, что в старых стандартах кодирования требовалось делать деструкторы всех без исключения базовых классов виртуальными. Однако это уже перебор (даже если это и оправдано в общем случае); вместо этого следует использовать правило, согласно которому деструктор базового класса должен быть виртуальным тогда и только тогда, когда он открытый.
Написание базового класса представляет собой определение абстракции (см. рекомендации с 35 по 37). Вспомним, что для каждой функции-члена, участвующей в абстракции, вы должны решить:
Читать дальше