Почему 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 ответа:
Если вы действительно используете виртуальный аспект виртуального наследования, я думаю, что необходимость в указателе 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 байт, скорее всего, происходят от необходимости хранить
Чтобы понять, как может работать объектная модель C++, вы можете взглянуть на Стэна Липпмана "внутри объектной модели C++". Он немного устарел, но описывает потенциальные методы реализации. Использует ли MSVC++ какой-либо из них, я не знаю, но это дает идеи, которые могут быть использованы.char
плюс некоторое отступление, чтобы выровнять объект на подходящей границе. Это похоже на то, что делают два других компилятора, за исключением того, что они использовали 16 байт дляlong double
, чтобы начать с (вероятно, это всего лишь 10 байт, дополненных подходящим выравниванием).Для объектной модели, используемой gcc и clang , вы можете взглянуть на Itanium ABI : они по существу используют Itanium ABI с незначительными корректировками фактически используемого процессора.
В visual studio поведение по умолчанию заключается в том, что все структуры выровнены по 8-байтовой границе. то есть, даже если вы это сделаете
struct A { char c; }
И затем проверьте
Теперь, в вашем случае, когда вы изменили тип наследования структуры D на виртуальный, компилятор должен сделать что-то дополнительное, чтобы выполнить это. Во-первых, он создает виртуальную таблицу для структуры D. Что содержит эта таблица vtable? Он содержит один указатель на смещение структуры B в память.Затем он добавляет vptr во главе структуры D, которая указывает на вновь созданную vtable.sizeof(A)
, вы увидите, что это 8 байт.Следовательно, теперь структура D должна выглядеть следующим образом:
struct D : virtual B { void* vptr; char c; }
Таким образом, размер D будет:
Именно здесь происходит выравнивание границ, которое мы обсуждали в начале. Поскольку все структуры должны быть выровнены по 8-байтовой границе, а структура D составляет всего 17 байт, компилятор добавляет 7 байт заполнения к структуре, чтобы выровнять ее по 8-байтовой границе.sizeof (long double) + sizeof (void*) + sizeof (char) = 8 + 8 + 1 = 17
Итак, размер теперь становится:
Size of D = Size of elements of D + Padding bytes for byte alignment = 17 + 7 = 24 bytes.