198
union U {
Foo
foo;
Bar
bar;
Banana
banana;
};
U whatIsThis;
Компилятор С++ не может определить, какой конструктор следует вызывать для
whatIsThis
—
Foo::Foo()
,
Bar::Bar()
или
Banana::Banana()
. Разумеется,
больше одного конструктора
вызывать нельзя, поскольку все члены занимают одно и то же место в памяти, но без инструкций от вас
не может выбрать нужный конструктор. Как и во многих других ситуациях, компилятор поднимет
руки; он сообщает об ошибке и отказывается принимать объединение, члены которого имеют
конструкторы.
Если вы хотите, чтобы одна область памяти могла инициализироваться несколькими
различными способами, придется подумать, как обмануть компилятор. Описанный выше «пустой»
конструктор подойдет лучше всего.
unsigned char space[4096];
Foo* whatIsThis = new(&space[0]) Foo;
Фактически происходит то, что в С++ происходить не должно — вызов конструктора. При этом память
на выделяется и не освобождается,
поскольку оператор
new
ничего не делает. Тем не менее,
компилятор С++ сочтет, что это новый объект, и
все равно вызовет конструктор. Если позднее вы
передумаете и захотите использовать ту же область
памяти для другого объекта, то сможете снова
вызвать хитроумный оператор
new
и инициализировать ее заново.
При создании объекта оператором new компилятор всегда использует двухшаговый процесс:
1. Выделение памяти под объект.
2. Вызов конструктора объекта.
Этот код запрятан в выполняемом коде, генерируемом компилятором, и в обычных ситуациях второй
шаг не выполняется без первого. Идиома виртуального конструктора позволяет надеть повязку на глаза
компилятору и обойти это ограничение.
Оперативное изменение типа объекта
Если позднее
Foo
вам надоест и вы захотите использовать ту же область для
Banana
, то при наличии у
Banana того же перегруженного оператора new вы сможете быстро сменить тип объекта.
Banana* b = new(&space[0]) Banana;
Пуф! Был
Foo
, стал
Banana
. Это и называется идиомой
виртуального конструктора. Такое решение
полностью соответствует спецификации языка.
Ограничения
Применяя эту идиому, необходимо помнить о двух обстоятельствах:
1. Область, передаваемая оператору
new
, должна быть достаточна для конструирования класса.
2. Об изменении должны знать все клиенты, хранящие адрес объекта!
Будет весьма неприятно, если вы сменили тип объекта с
Foo
на
Banana
только для того, чтобы какой-
нибудь клиентский объект тут же вызвал одну из функций
Foo
.
Уничтожение с разделением фаз
Объект, переданный в качестве аргумента оператору
delete
, обычно уничтожается компилятором в
два этапа:
1. Вызов деструктора.
2. Вызов оператора
delete
для освобождения памяти.
Довольно часто мы качаем головой и говорим: «Хорошо бы вызвать деструктор, но не трогать память».
Допустим, вы
разместили объект в пуле, а теперь не хотите, чтобы часть локально созданного пула