Стандартная компоновка и обивка хвоста
Дэвид Холлман недавно написал в Твиттере следующий пример (который я немного сократил):
struct FooBeforeBase {
double d;
bool b[4];
};
struct FooBefore : FooBeforeBase {
float value;
};
static_assert(sizeof(FooBefore) > 16);
//----------------------------------------------------
struct FooAfterBase {
protected:
double d;
public:
bool b[4];
};
struct FooAfter : FooAfterBase {
float value;
};
static_assert(sizeof(FooAfter) == 16);
Вы можете проверить макет в Clang на Godbolt и увидеть, что причина изменения размера в том, что в FooBefore
член value
размещается по смещению 16 (поддерживая полное выравнивание 8 от FooBeforeBase
) тогда как в FooAfter
член value
находится по смещению 12 (эффективно используя FooAfterBase
Хвостовая прокладка).
Мне понятно что FooBeforeBase
стандартная компоновка, но FooAfterBase
это не так (потому что его нестатические члены-данные не все имеют одинаковый контроль доступа, [class.prop] / 3). Но о чем это FooBeforeBase
Является ли стандартная компоновка, которая требует этого уважения заполнения байтов?
Повторное использование gcc и clang FooAfterBase
дополняет, заканчивая sizeof(FooAfter) == 16
, Но MSVC этого не делает, заканчивая 24. Есть ли требуемая раскладка для стандарта и, если нет, почему gcc и clang делают то, что они делают?
Существует некоторая путаница, поэтому просто чтобы прояснить:
FooBeforeBase
стандартная компоновкаFooBefore
нет (и it, и базовый класс имеют нестатические члены-данные, аналогичныеE
в этом примере)FooAfterBase
нет (имеет нестатические данные-члены разного доступа)FooAfter
нет (по обеим вышеуказанным причинам)
5 ответов
Ответ на этот вопрос приходит не от стандарта, а от Itanium ABI (именно поэтому gcc и clang имеют одно поведение, а msvc делает что-то другое). Этот ABI определяет макет, соответствующие части которого для целей этого вопроса:
Для внутренних целей спецификации мы также указываем:
- dsize(O): размер данных объекта, который является размером O без дополнения хвостом.
а также
Мы игнорируем хвостовое заполнение для POD, потому что ранняя версия стандарта не позволяла нам использовать его для чего-либо еще, а также потому, что иногда это позволяет быстрее копировать тип.
Где размещение членов, отличных от виртуальных базовых классов, определяется как:
Начните со смещения dsize(C), увеличивая при необходимости для выравнивания по nvalign(D) для базовых классов или для выравнивания (D) для членов данных. Поместите D в это смещение, если только [... не имеет значения...].
Термин POD исчез из стандарта C++, но он означает макет стандарта и легко копируется. В этом вопросе FooBeforeBase
это стручок Itanium ABI игнорирует хвостовую прокладку - следовательно dsize(FooBeforeBase)
16
Но FooAfterBase
это не POD (его легко копировать, но это не стандартная схема). В результате, хвостовая подкладка не игнорируется, поэтому dsize(FooAfterBase)
просто 12, а float
может пойти прямо туда
Это имеет интересные последствия, как указывает Quuxplusone в связанном ответе, разработчики также обычно предполагают, что заполнение хвоста не используется повторно, что приводит к хаосу в этом примере:
#include <algorithm> #include <stdio.h> struct A { int m_a; }; struct B : A { int m_b1; char m_b2; }; struct C : B { short m_c; }; int main() { C c1 { 1, 2, 3, 4 }; B& b1 = c1; B b2 { 5, 6, 7 }; printf("before operator=: %d\n", int(c1.m_c)); // 4 b1 = b2; printf("after operator=: %d\n", int(c1.m_c)); // 4 printf("before std::copy: %d\n", int(c1.m_c)); // 4 std::copy(&b2, &b2 + 1, &b1); printf("after std::copy: %d\n", int(c1.m_c)); // 64, or 0, or anything but 4 }
Вот, =
правильно делает (не отменяет B
Хвостовая обивка), но copy()
имеет оптимизацию библиотеки, которая сводится к memmove()
- который не заботится о набивке хвоста, потому что предполагает, что его не существует.
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
Если дополнительный элемент данных был помещен в отверстие, memcpy
переписал бы это.
Как правильно указано в комментариях, стандарт не требует, чтобы это memcpy
вызов должен работать. Однако Itanium ABI, похоже, разработан с учетом этого случая. Возможно, правила ABI определены таким образом, чтобы сделать программирование на нескольких языках более устойчивым или сохранить некоторую обратную совместимость.
Соответствующие правила ABI можно найти здесь.
Соответствующий ответ можно найти здесь (этот вопрос может быть дубликатом этого).
Вот конкретный случай, который демонстрирует, почему второй случай не может повторно использовать заполнение:
union bob {
FooBeforeBase a;
FooBefore b;
};
bob.b.value = 3.14;
memset( &bob.a, 0, sizeof(bob.a) );
это не может очистить bob.b.value
,
union bob2 {
FooAfterBase a;
FooAfter b;
};
bob2.b.value = 3.14;
memset( &bob2.a, 0, sizeof(bob2.a) );
это неопределенное поведение.
FooBefore
также не является std-layout; два класса объявляют не статические члены данных (FooBefore
а также FooBeforeBase
). Таким образом, компилятору разрешено произвольно размещать некоторые элементы данных. Отсюда возникают различия в разных цепях инструментов. В иерархии std-layout не более одного класса (либо самого производного класса, либо не более одного промежуточного класса) должны объявлять нестатические члены-данные.
Вот тот же случай, что и в ответе НМ.
Во-первых, давайте иметь функцию, которая очищает FooBeforeBase
:
void clearBase(FooBeforeBase *f) {
memset(f, 0, sizeof(*f));
}
Это нормально, как clearBase
получает указатель на FooBeforeBase
думает что как FooBeforeBase
имеет стандартную компоновку, поэтому memsetting это безопасно.
Теперь, если вы сделаете это:
FooBefore b;
b.value = 42;
clearBase(&b);
Вы не ожидаете, что clearBase
очистит b.value
, как b.value
не является частью FooBeforeBase
, Но если FooBefore::value
был помещен в набивку хвоста FooBeforeBase
было бы очищено.
Существует ли требуемый макет в соответствии со стандартом и, если нет, почему gcc и clang делают то, что они делают?
Нет, набивка хвоста не требуется. Это оптимизация, которую делают gcc и clang.