Стандартная компоновка и обивка хвоста
Дэвид Холлман недавно написал в Твиттере следующий пример (который я немного сократил):
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
, что в FooBefore
value
элемента размещается по смещению 16 (с сохранением полного выравнивания 8 из FooBeforeBase
), тогда как в FooAfter
value
элемента размещается в смещение 12 (эффективно с использованием FooAfterBase
tail-padding).
Для меня очевидно, что FooBeforeBase
- это стандартная компоновка, а FooAfterBase
- нет (поскольку не все элементы нестатических данных имеют одинаковый контроль доступа, [class.prop]/3). Но каково то, что FooBeforeBase
является стандартным макетом, который требует этого отношения байтов заполнения?
И gcc, и clang повторно FooAfterBase
заполнение FooAfterBase
, заканчиваясь на sizeof(FooAfter) == 16
. Но MSVC этого не делает, заканчивая 24. Есть ли требуемая раскладка в соответствии со стандартом и, если нет, почему gcc и clang делают то, что они делают?
Существует некоторая путаница, поэтому просто чтобы прояснить:
-
FooBeforeBase
является стандартным макетом -
FooBefore
нет (и it, и базовый класс имеют нестатические члены-данные, аналогично E
в этом примере) -
FooAfterBase
- нет (он имеет нестатические элементы данных разного доступа) -
FooAfter
нет (по обеим выше причинам)
Ответы
Ответ 1
Вот конкретный случай, который демонстрирует, почему второй случай не может повторно использовать заполнение:
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) );
это неопределенное поведение.
Ответ 2
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));
Если бы дополнительный элемент данных был помещен в отверстие, memcpy
перезаписал бы его.
Как правильно указано в комментариях, стандарт не требует, чтобы этот вызов memcpy
работал. Однако Itanium ABI, похоже, разработан с учетом этого случая. Возможно, правила ABI определены таким образом, чтобы сделать программирование на нескольких языках более надежным или сохранить некоторую обратную совместимость.
Соответствующие правила ABI можно найти здесь.
Соответствующий ответ можно найти здесь (этот вопрос может быть дубликатом этого).
Ответ 3
FooBefore
не является std-layout; два класса объявляют нестатические члены данных (FooBefore
и FooBeforeBase
). Таким образом, компилятору разрешено произвольно размещать некоторые элементы данных. Отсюда возникают различия в разных цепях инструментов. В иерархии std-layout не более одного класса (либо самого производного класса, либо не более одного промежуточного класса) должны объявлять нестатические члены-данные.
Ответ 4
Ответ на этот вопрос исходит не от стандарта, а от Itanium ABI (именно поэтому gcc и clang имеют одно поведение, а msvc - другое). Этот ABI определяет макет, соответствующими частями которого для целей этого вопроса являются:
Для внутренних целей спецификации мы также указываем:
- dsize (O): размер данных объекта, который равен размеру O без добавления хвоста.
а также
Мы игнорируем добавление хвоста для POD, потому что ранняя версия стандарта не позволяла нам использовать его для чего-либо еще, а также потому, что иногда это позволяет быстрее копировать тип.
Где размещение членов, отличных от виртуальных базовых классов, определяется как:
Начните со смещения dsize (C), увеличивая при необходимости для выравнивания по nvalign (D) для базовых классов или для выравнивания (D) для членов данных. Поместите D в это смещение, если [... не релевантно...].
Термин POD исчез из стандарта C++, но он означает макет стандарта и тривиально копируемый. В этом вопросе FooBeforeBase
является POD. 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()
которая не заботится о заполнении хвоста, потому что предполагает, что она не существует.
Ответ 5
Здесь аналогичный случай, как ответ NM.
Во-первых, пусть есть функция, которая очищает 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.