Стандартная компоновка и обивка хвоста

Дэвид Холлман недавно написал в Твиттере следующий пример (который я немного сократил):

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.

Другие вопросы по тегам