наследование — детальный byte-level разборinheritance — a detailed byte-level breakdown
Ромбовидная проблема решается не «магией» компилятора, а конкретной, предсказуемой перестройкой физической раскладки объекта в памяти: смещения полей меняются, появляются дополнительные указатели, а обращение к общему подобъекту требует лишнего чтения из памяти. Здесь — та же тема, что и на страницах про наследование и виртуальные таблицы, но с конкретными байтами и смещениями по Itanium C++ ABI (GCC/Clang).The diamond problem isn't solved by compiler "magic" — it's a concrete, predictable restructuring of the object's physical memory layout: field offsets change, extra pointers appear, and accessing the shared subobject costs one more memory read. This covers the same ground as the inheritance and virtual tables pages, but with concrete bytes and offsets per the Itanium C++ ABI (GCC/Clang).
Прежде чем усложнять, зафиксируем базовый случай. При одиночном наследовании подобъект базового класса просто кладётся в начало объекта производного класса — их адреса совпадают.Before adding complexity, let's fix the baseline case. Under single inheritance, the base class's subobject is simply placed at the start of the derived class's object — their addresses coincide.
struct A { int a; }; // sizeof(A) == 4// sizeof(A) == 4
struct B : A { int b; }; // sizeof(B) == 8// sizeof(B) == 8
B* и вложенный в него A* указывают на один и тот же адресa B* and the A* nested inside it point at the exact same address
Именно поэтому static_cast<A*>(bPtr) при одиночном наследовании — это буквально no-op на уровне машинного кода: значение указателя не меняется, меняется только его тип для компилятора.This is exactly why static_cast<A*>(bPtr) under single inheritance is literally a no-op at the machine-code level: the pointer's value doesn't change, only its type does, for the compiler's bookkeeping.
struct Base1 { int x; };
struct Base2 { int y; };
struct Derived : Base1, Base2 { int z; }; // sizeof(Derived) == 12// sizeof(Derived) == 12
подобъекты баз идут подряд в порядке списка наследованияbase subobjects are laid out back to back, in inheritance-list order
Здесь уже видна первая практическая деталь: static_cast<Base2*>(derivedPtr) — это НЕ no-op. Компилятор обязан прибавить к адресу смещение Base2 (здесь — 4 байта), иначе указатель будет указывать на Base1::x, а не на Base2::y. Это смещение известно на этапе компиляции (нет виртуального наследования — нет неопределённости), поэтому подставляется прямо в код как константа.Here the first practical detail already shows up: static_cast<Base2*>(derivedPtr) is NOT a no-op. The compiler must add Base2's offset (4 bytes here) to the address, otherwise the pointer would point at Base1::x instead of Base2::y. That offset is known at compile time (no virtual inheritance means no uncertainty), so it's baked into the code as a constant.
struct A { int a; }; // sizeof(A) == 4// sizeof(A) == 4
struct B1 : A { int b1; }; // sizeof(B1) == 8// sizeof(B1) == 8
struct B2 : A { int b2; }; // sizeof(B2) == 8// sizeof(B2) == 8
struct D : B1, B2 { int d; }; // sizeof(D) == 20 — два A целиком!// sizeof(D) == 20 — two whole A's!
две копии A по смещениям 0 и 8 — d.a неоднозначен, потому что физически существуют обаtwo copies of A at offsets 0 and 8 — d.a is ambiguous because both physically exist
Ошибка компиляции «d.a неоднозначен» — не искусственное ограничение языка, а прямое следствие layout: по смещению 0 лежит один int, по смещению 8 — другой, оба одинаково валидны, и компилятор физически не может «угадать», какой из двух байтовых участков памяти имелся в виду.The "d.a is ambiguous" compile error isn't an artificial language restriction — it's a direct consequence of the layout: one int sits at offset 0, another at offset 8, both equally valid, and the compiler physically cannot "guess" which of the two memory regions was meant.
Когда B1 и B2 наследуют от A виртуально, компилятор физически перестраивает объект: единственная копия A перемещается в конец объекта D (после всех невиртуальных частей), а на её место, в начале подобъектов B1 и B2, встают скрытые указатели на таблицу смещений виртуальной базы (vbase offset, хранится в структуре, похожей на vtable, — см. страницу про виртуальные таблицы).When B1 and B2 inherit from A virtually, the compiler physically restructures the object: the single copy of A moves to the end of D's object (after every non-virtual part), and in its place, at the start of the B1 and B2 subobjects, hidden pointers to the virtual base offset table appear (stored in a vtable-like structure — see the virtual tables page).
struct A { int a; };
struct B1 : virtual A { int b1; };
struct B2 : virtual A { int b2; };
struct D : B1, B2 { int d; };
// на x86-64, GCC/Clang: sizeof(D) == 32 (не 20!) — но A теперь только одна копия// on x86-64, GCC/Clang: sizeof(D) == 32 (not 20!) — but there's only one A now
оба vbase-указателя (в B1 и в B2) в конечном счёте ведут к одному и тому же A на смещении 24both vbase pointers (in B1 and in B2) ultimately lead to the same A at offset 24
Обратите внимание, что размер объекта вырос (32 байта против 20 без virtual) — экономия на дублировании A (4 байта) съедена двумя новыми vbase-указателями (по 8 байт каждый на x86-64). Виртуальное наследование выгодно семантически (единый общий подобъект, никакой неоднозначности), но не по чистому размеру объекта — реальная выгода в том, что d.a теперь однозначен и логически корректен, а не в экономии памяти.Notice the object's size actually grew (32 bytes vs. 20 without virtual) — the savings from not duplicating A (4 bytes) are eaten up by two new vbase pointers (8 bytes each on x86-64). Virtual inheritance pays off semantically (one shared subobject, no ambiguity), not in raw object size — the real win is that d.a is now unambiguous and logically correct, not memory saved.
Доступ к d.a теперь компилируется примерно так: прочитать vbase-указатель из B1-части (или из vtable объекта — конкретный механизм зависит от компилятора), получить смещение виртуальной базы, прибавить его к адресу объекта — то самое «одно дополнительное разыменование», упомянутое на странице про наследование, здесь видно буквально как одна лишняя инструкция чтения из памяти на пути к полю a.Accessing d.a now compiles roughly to: read the vbase pointer from the B1 portion (or from the object's vtable — the exact mechanism is compiler-dependent), fetch the virtual base's offset, add it to the object's address — that "one extra dereference" mentioned on the inheritance page shows up here literally as one extra memory-read instruction on the way to field a.
| одиночноеsingle | ромб без virtualdiamond, no virtual | ромб с virtualdiamond, virtual | |
|---|---|---|---|
| копий Acopies of A | 1 | 2 | 1 |
| доступ к aaccess to a | прямойdirect | неоднозначен (ошибка)ambiguous (error) | косвенный, через vbase ptrindirect, via a vbase ptr |
| static_cast к базеto a base | no-op | константное смещениеa constant offset | запрещён (нужен dynamic_cast)forbidden (dynamic_cast required) |
| лишний указатель на объектextra pointer per object | нетnone | нетnone | по одному на каждый виртуальный путь наследованияone per virtual inheritance path |
Конкретные смещения и порядок полей в этой статье описаны для Itanium C++ ABI (GCC/Clang на большинстве платформ) — стандарт C++ не специфицирует layout объектов с виртуальным наследованием вообще, поэтому MSVC на Windows кладёт vbase-информацию иначе. Полагаться на конкретные смещения в переносимом коде нельзя — только sizeof, offsetof для standard-layout типов и явные API языка.The specific offsets and field order in this article describe the Itanium C++ ABI (GCC/Clang on most platforms) — the C++ standard doesn't specify object layout under virtual inheritance at all, so MSVC on Windows places vbase information differently. Portable code can't rely on specific offsets — only on sizeof, offsetof for standard-layout types, and the language's explicit APIs.
Поскольку static_cast к базовому классу при множественном/виртуальном наследовании — это реальная арифметика указателя, а не смена интерпретации того же адреса, reinterpret_cast (который никогда не корректирует адрес) даст указатель на неправильное место в объекте — компилирующийся, но совершенно неверный код.Since static_cast to a base class under multiple/virtual inheritance is real pointer arithmetic, not a reinterpretation of the same address, reinterpret_cast (which never adjusts the address) yields a pointer to the wrong place inside the object — code that compiles but is completely wrong.
Как показано выше, добавление virtual к наследованию может увеличить, а не уменьшить sizeof объекта — интуиция «virtual убирает дублирование, значит объект должен стать меньше» неверна, если новых vbase-указателей оказывается больше, чем сэкономленных байт от единственной копии базы.As shown above, adding virtual to inheritance can grow, not shrink, an object's sizeof — the intuition "virtual removes duplication, so the object must get smaller" is wrong whenever the new vbase pointers outweigh the bytes saved by having only one copy of the base.