Инициализация объектов с / без 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 3

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

Если вам не нужно делать копию объекта, вы можете использовать массив указателей.