Проблема, с которой вы столкнулись, связана с распределением памяти. Когда массивы выделены, они должны содержать хранилище для всех своих элементов. Позвольте мне привести (очень упрощенный) пример. Скажем, у вас есть классы, настроенные так:
class Base
{
public:
int A;
int B;
}
class ChildOne : Base
{
public:
int C;
}
class ChildTwo : Base
{
public:
double C;
}
Когда вы выделяете Base[10]
, каждому элементу в массиве потребуется (в типичной 32-битной системе *) 8 байт памяти: достаточно для хранения двух 4-байтовых байтов. Однако классу ChildOne
требуется 8 байт памяти своего родителя, плюс дополнительные 4 байта для его члена C
. Класс ChildTwo
нуждается в 8 байтах своего родителя, плюс дополнительные 8 байтов для его double C
. Если вы попытаетесь вставить любой из этих двух дочерних классов в массив, выделенный для 8-байтового Base
, вы переполните свое хранилище.
Причина работы массивов указателей в том, что они имеют постоянный размер (по 4 байта в 32-битной системе), независимо от того, на что они указывают . Указатель на Base
совпадает с указателем на ChildTwo
, несмотря на то, что последний класс в два раза больше.
Оператор dynamic_cast
позволяет вам выполнять безопасное переключение типов, чтобы изменить Base*
на ChildTwo*
, поэтому он решит вашу проблему в данном конкретном случае.
Кроме того, вы можете отделить логику обработки от хранилища данных ( Pattern Pattern ), создав макет класса примерно так:
class Data
{
public:
int A;
int B;
Data(HandlerBase* myHandler);
int DoSomething() { return myHandler->DoSomething(this) }
protected:
HandlerBase* myHandler;
}
class HandlerBase
{
public:
virtual int DoSomething(Data* obj) = 0;
}
class ChildHandler : HandlerBase
{
public:
virtual int DoSomething(Data* obj) { return obj->A; }
}
Этот шаблон будет уместен в тех случаях, когда алгоритмическая логика DoSomething
может потребовать значительной настройки или инициализации, которая является общей для большого количества объектов (и может обрабатываться в конструкции ChildHandler
), но не универсальной ( и, следовательно, не подходит для статического члена). Затем объекты данных сохраняют согласованное хранилище и указывают на процесс-обработчик, который будет использоваться для выполнения их операций, передавая себя в качестве параметра, когда им нужно что-то вызвать. Data
объекты этого вида имеют согласованный, предсказуемый размер и могут быть сгруппированы в массивы для сохранения ссылочной локализации, но при этом обладают всей гибкостью обычного механизма наследования.
Обратите внимание, что вы все еще строите то, что равносильно массиву указателей, - они просто укрыты другим слоем глубоко под фактической структурой массива.
* Для придирки: да, я понимаю, что числа, которые я дал для распределения памяти, игнорируют заголовки классов, информацию vtable, отступы и большое количество других потенциальных соображений компилятора. Это не должно быть исчерпывающим.
Редактировать часть II: Все следующие материалы неверны. Я разместил его на макушке, не проверяя его, и перепутал способность переосмысливать два не связанных указателя с возможностью создавать два не связанных класса. Mea culpa , и спасибо Чарльзу Бейли за то, что он указал на мою оплошность.
Общий эффект все еще возможен - вы можете принудительно извлечь объект из массива и использовать его как другой класс - но это требует взятия адреса объекта и принудительного наведения указателя на новый тип объекта, который побеждает теоретическую цель избежать разыменования указателя. В любом случае, моя первоначальная точка зрения - что это ужасная «оптимизация», которую я пытаюсь сделать в первую очередь - все еще верна.
Редактировать: Хорошо, я думаю, что с вашими последними правками я понял, что вы пытаетесь сделать. Я собираюсь дать вам решение здесь, но, пожалуйста, ради любви ко всему святому, поклянись, что ты никогда не будешь использовать это в рабочем коде . Это инженерное любопытство, не хорошая практика.
Похоже, вы пытаетесь избежать разыменования указателя (возможно, в качестве микрооптимизации производительности?), Но все же хотите гибко вызывать субметоды для объектов. Если вы точно знаете, что ваши базовые и производные классы имеют одинаковые размеры - и only способ, которым вы собираетесь это знать, - это изучить макет физического класса, сгенерированный компилятором, поскольку он может вносить всевозможные корректировки по своему усмотрению, а спецификация не дает никаких гарантий - тогда вы можете использовать reinterpret_cast для принудительного обращения к родителю как к дочернему элементу в массиве.
class Base
{
public:
int A;
int B;
void DoSomething();
}
class Derived : Base
{
void DoSomething();
}
void DangerousGames()
{
// create an array of ten default-constructed Base on the stack
Base items[10];
// force the compiler to treat the bits of items[5] as a Derived,
// and make a ref
Derived& childItem = reinterpret_cast<Derived>(items[5]);
// invoke Derived::DoSomething() using the data bits of items[5],
// since it has an identical layout
childItem.DoSomething();
}
Это избавит вас от разыменования указателя и не приведет к снижению производительности, поскольку reinterpret_cast не является приведением во время выполнения, это по сути переопределение компилятора, которое говорит: «Независимо от того, что вы думаете, вы знаете, я знаю, что делаю, заткнись и сделай это. «Небольшой недостаток» заключается в том, что он делает ваш код очень хрупким, потому что любое изменение макета Base
или Derived
, инициированное вами или компилятором, приведет к краху всего этого, с тем, что, вероятно, будет чрезвычайно тонким и почти невозможным для отладки неопределенного поведения. Опять же, никогда не использует это в рабочем коде . Даже в наиболее критичных для производительности системах реального времени стоимость разыменования указателя стоит всегда , по сравнению со сборкой того, что составляет ядерную бомбу с пусковым механизмом в середине вашей кодовой базы.