Стандартная компоновка и обивка хвоста - PullRequest
0 голосов
/ 18 декабря 2018

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

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 's padding).

Мне ясно, что FooBeforeBase - это стандартная компоновка, но FooAfterBase - нет (потому что не все элементы нестатических данных имеют одинаковый доступконтроль, [class.prop] / 3 ).Но что это за стандартная компоновка FooBeforeBase, которая требует этого уважения для заполнения байтов?

И gcc, и clang повторно используют заполнение FooAfterBase, заканчивающееся sizeof(FooAfter) == 16.Но MSVC этого не делает, заканчивая 24. Есть ли требуемая раскладка в соответствии со стандартом и, если нет, почему gcc и clang делают то, что они делают?

Существует некоторая путаница, поэтому просто для уточнения:

  • FooBeforeBase стандартная раскладка
  • FooBefore это не (обаи базовый класс имеет нестатические члены-данные, аналогично E в в этом примере )
  • FooAfterBase равно , а не (он имеет нестатические данныечлены разного доступа)
  • FooAfter - это , а не (по обоим вышеуказанным причинам)

Ответы [ 5 ]

0 голосов
/ 18 декабря 2018

Ответ на этот вопрос исходит не от стандарта, а от ABI Itanium (именно поэтому 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() - чтоне заботится о дополнении хвоста, потому что предполагает, что его не существует.

0 голосов
/ 18 декабря 2018

FooBefore также не является std-layout;два класса объявляют нестатические элементы данных (FooBefore и FooBeforeBase).Таким образом, компилятору разрешено произвольно размещать некоторые элементы данных.Отсюда возникают различия в разных цепях инструментов.В иерархии std-layout не более одного класса (либо самого производного класса, либо не более одного промежуточного класса) должны объявлять нестатические члены-данные.

0 голосов
/ 18 декабря 2018
FooBefore derived;
FooBeforeBase src, &dst=derived;
....
memcpy(&dst, &src, sizeof(dst));

Если бы дополнительный элемент данных был помещен в отверстие, memcpy перезаписал бы его.

Как правильно указано в комментариях, стандарт не требует, чтобы это memcpyвызов должен работать.Однако Itanium ABI, похоже, разработан с учетом этого случая.Возможно, правила ABI определены таким образом, чтобы сделать программирование на нескольких языках более надежным или сохранить некоторую обратную совместимость.

Соответствующие правила ABI можно найти здесь .

Соответствующий ответ можно найти здесь (этот вопрос может быть дубликатом этого вопроса).

0 голосов
/ 18 декабря 2018

Вот тот же случай, что и в ответе 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.

0 голосов
/ 18 декабря 2018

Вот конкретный случай, который демонстрирует, почему второй случай не может повторно использовать заполнение:

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) );

это неопределенное поведение.

...