Почему sizeof (D) увеличился на 8 байт в VS2015, когда я получил D из виртуальной базы?


Я использую пример в C++14 §3.11/2:

struct B { long double d; };
struct D : virtual B { char c; }

После запуска фрагмента ниже в clang, g++ и VS2015

#include <iostream>
struct B { long double d; };
struct D : /*virtual*/ B { char c; };

int main()
{
    std::cout << "sizeof(long double) = " << sizeof(long double) << 'n';
    std::cout << "alignof(long double) = " << alignof(long double) << 'n';

    std::cout << "sizeof(B) = " << sizeof(B) << 'n';
    std::cout << "alignof(B) = " << alignof(B) << 'n';

    std::cout << "sizeof(D) = " << sizeof(D) << 'n';
    std::cout << "alignof(D) = " << alignof(D) << 'n';
}

Я получил следующие результаты:

                         clang           g++         VS2015  
sizeof(long double)        16             16            8
alignof(long double)       16             16            8
sizeof(B)                  16             16            8
alignof(B)                 16             16            8
sizeof(D)                  32             32           16
alignof(D)                 16             16            8
Теперь, после раскомментирования virtual в определении struct D в приведенном выше коде и повторного запуска кода для clang, g++ и VS2015, я получил следующие результаты:
                         clang           g++         VS2015  
sizeof(long double)        16             16            8
alignof(long double)       16             16            8
sizeof(B)                  16             16            8
alignof(B)                 16             16            8
sizeof(D)                  32             32           24
alignof(D)                 16             16            8
У меня нет никаких сомнений относительно результатов, полученных выше, за одним единственным исключением: почему sizeof(D) увеличился с 16 до 24 в VS2015? Я знаю, что это определение реализации, но может быть разумное объяснение этому увеличению размера. Вот что я хотел бы знать, если это возможно.
3 6

3 ответа:

Если вы действительно используете виртуальный аспект виртуального наследования, я думаю, что необходимость в указателе vtable становится ясной. Один элемент в таблице vtable, вероятно, смещение начала B от начала D.

Предположим, что E наследует виртуально от B и F наследует как от E, так и от D таким образом, что D внутри F в конечном итоге использует B внутри E для своего базового класса. В методе D , который не знает, что это базовый класс F, как можно ли найти элементы B без информации, хранящейся в vtable?

Итак, clang и G++ изменили 8 байт заполнения в указатель vtable,и вы думали, что никаких изменений не было. Но VS2015 никогда не имел такого заполнения, поэтому ему нужно было добавить 8 байт для указателя vtable.

Возможно, компилятор замечает, что единственный способ использования указателя vtable-это неэффективная схема вычисления базового указателя. Так что, возможно, это оптимизировано для простого наличия базового указателя вместо указателя vtable. Но это не изменит потребности в 8 байтах.

Когда существует базовый объект virtual, расположение базового объекта относительно адреса производного объекта не является статически предсказуемым. Примечательно, что если вы немного расширите иерархию классов, то станет ясно, что может существовать несколько подобъектов D, которые все еще должны ссылаться только на один базовый объект B:

class I1: public D {};
class I2: public D {};
class Most: public I1, public I2 {};

Вы можете получить D* из объекта Most, либо преобразовав сначала в I1, либо сначала в I2:

Most m;
D*   d1 = static_cast<I1*>(&m);
D*   d2 = static_cast<I2*>(&m);

У вас будет d1 != d2, т. е. два D подобъекта, но static_cast<B*>(d1) == static_cast<B*>(d2), т. е. есть только один B подобъект. Чтобы определить, как настроить d1 и d2, чтобы найти указатель на подобъект B, необходимо динамическое смещение. Информацию о том, как определить это смещение, нужно где-то хранить. Хранилище для этой информации является вероятным источником дополнительных 8 байт.

Я не думаю, что расположение объектов для типов в MSVC++ задокументировано [публично], т. е. невозможно сказать наверняка, что они делают. Судя по внешнему виду, они встраивают 64-битный объект, чтобы иметь возможность определить, где находится базовый объект относительно адреса производного объекта (указатель на некоторую информацию типа, указатель на базу, смещение к базе и т. д.). Остальные 8 байт, скорее всего, происходят от необходимости хранить char плюс некоторое отступление, чтобы выровнять объект на подходящей границе. Это похоже на то, что делают два других компилятора, за исключением того, что они использовали 16 байт для long double, чтобы начать с (вероятно, это всего лишь 10 байт, дополненных подходящим выравниванием).

Чтобы понять, как может работать объектная модель C++, вы можете взглянуть на Стэна Липпмана "внутри объектной модели C++". Он немного устарел, но описывает потенциальные методы реализации. Использует ли MSVC++ какой-либо из них, я не знаю, но это дает идеи, которые могут быть использованы.

Для объектной модели, используемой gcc и clang , вы можете взглянуть на Itanium ABI : они по существу используют Itanium ABI с незначительными корректировками фактически используемого процессора.

В visual studio поведение по умолчанию заключается в том, что все структуры выровнены по 8-байтовой границе. то есть, даже если вы это сделаете

struct A {
  char c;
}

И затем проверьте sizeof(A), вы увидите, что это 8 байт.

Теперь, в вашем случае, когда вы изменили тип наследования структуры D на виртуальный, компилятор должен сделать что-то дополнительное, чтобы выполнить это. Во-первых, он создает виртуальную таблицу для структуры D. Что содержит эта таблица vtable? Он содержит один указатель на смещение структуры B в память.Затем он добавляет vptr во главе структуры D, которая указывает на вновь созданную vtable.

Следовательно, теперь структура D должна выглядеть следующим образом:

 struct D : virtual B { void* vptr; char c; }

Таким образом, размер D будет:

sizeof (long double) + sizeof (void*) +  sizeof (char) = 8 + 8 + 1 = 17
Именно здесь происходит выравнивание границ, которое мы обсуждали в начале. Поскольку все структуры должны быть выровнены по 8-байтовой границе, а структура D составляет всего 17 байт, компилятор добавляет 7 байт заполнения к структуре, чтобы выровнять ее по 8-байтовой границе.

Итак, размер теперь становится:

Size of D = Size of elements of D  + Padding bytes for byte alignment = 17 + 7 = 24 bytes.