Инициализация объектов с / без vtable
Предположим, у меня есть пул, который выделяет некоторый буфер.
int size = 10;
T* buffer = (T*) new char[size * sizeof(T)];
Если я теперь хочу назначить некоторые данные буферу, я делаю следующее.
buffer[0] = data;
Теперь мой вопрос заключается в том, в чем разница в инициализации объектов, которые имеют vtable, и тех, которые этого не делают.
Из того, что я вижу, я могу без проблем назначать классы этому буферу, и пока я не вызываю никаких виртуальных функций, вызовы функций работают просто отлично. напримерclass A{
void function(){}
};
A a;
buffer[0] = a;
a.function(); // works
Но:
class B{
void function(){}
virtual void virtual_function(){}
};
B b;
buffer[0] = b;
b.function(); // does work
b.virtual_function() // does not work.
Почему же невиртуальные функции работают?
Это потому, что функция статически объявлена из-за того, что она является нормальной функцией класса и поэтому копируется, когда мы выполняем присваивание?
Но тогда это не имеет смысла, что мне нужно вызвать конструктор на буфер, который я создал в случае, если мне нужно убедиться, что виртуальная функция работает так же. new (buffer[0]) T();
для вызова конструктора на созданном объекте.
Оба примера сначала создают соответствующий размер буфера, а затем выполняют назначение, рассматривайте это как пул, где я предварительно выделяю память в зависимости от количества объектов, которые я хочу поместить в пул.
Может быть, я просто смотрел на это долго и запутался в себе:)
3 ответа:
Ваши невиртуальные функции "работают" (относительный термин), потому что они не нуждаются в поиске vtable. Под капотом находится реализация-зависимая, но рассмотрим, что необходимо для выполнения невиртуального члена.
Вам нужен указатель на функцию и
this
. Последнее очевидно, но откуда берется fn-ptr? это просто простой вызов функции (ожиданиеthis
, а затем любые предоставленные аргументы). Здесь нет полиморфного потенциала. Отсутствие необходимости поиска vtable означает, что компилятор может (и часто does) просто берет адрес того, что мы считаем объектом, нажимает его, нажимает любые поставляемые args и вызывает функцию-член в виде простого старого -call
. Компилятор знает, какую функцию вызывать, и не нуждается в vtable-посреднике.Нередко это вызывает головную боль при вызове нестатической, невиртуальной функции-члена на запрещенных указателях. Если функция виртуальна, вы обычно (если Вам ПОВЕЗЕТ) взорветесь на вызове . Если функция невиртуальна, вы будете обычно (если вам повезло) взрывается где-то в теле функции, когда она пытается получить доступ к данным-членам, которых там нет (включая выполнение, направленное на vtable, если ваш невиртуальный вызов виртуального).
Чтобы продемонстрировать это, рассмотрим этот (очевидно, UB) пример. Попробовать его.
#include <iostream> class NullClass { public: void call_me() { std::cout << static_cast<void*>(this) << '\n'; std::cout << "How did I get *here* ???" << '\n'; } }; int main() { NullClass *noObject = NULL; noObject->call_me(); }
Выход (OSX 10.10.1 x64, clang 3.5)
0x0 How did I get *here* ???
Суть в том, что никакая vtable не привязывается к объекту, когда вы выделяете необработанную память и назначаете указатель через приведение, как вы есть. Если вы хотите сделать это, вам нужно построить объект через размещение-новое. И при этом не забывайте, что вы также должны уничтожить объект (который не имеет ничего общего с памятью, которую он занимает, поскольку вы управляете этим отдельно), вызвав его деструктор вручную.
, Наконец-то, назначение вы вызываете не скопировать таблицы vtable. Честно говоря, для этого нет никаких оснований. Vtable правильно построенного объекта уже правильно построенный и на который ссылается указатель vtable для данного экземпляра объекта. Указанный указатель не участвует в копировании объекта, который имеет свой собственный набор обязательных требований из стандарта языка.
new char[...]
Это не создает объект T (не вызывает конструктор). Виртуальная таблица создается во время построения.
Проблема не только в виртуальных функциях, но и вообще в наследовании. Поскольку буфер-это массив A, то при записи:
Сначала вы строите объектB b; buffer[0] = b;
B
(Первая строка), а затем строите объектA
, используя его конструктор копирования, инициализированныйb
(вторая строка). Поэтому, когда вы позже вызываетеbuffer[0].virtual_function()
, вы фактически применяете виртуальную функцию к объектуA
, а не к объектуB
.Кстати, прямой звонок в
b.virtual_function()
все равно должен быть правильно вызвать версиюB
, так как она применяется к реальному объектуB
:B b; buffer[0] = b; b.virtual_function(); // calls B version
Если вам не нужно делать копию объекта, вы можете использовать массив указателей.