gcc/clang размещает поля производной структуры в отступах базовой структуры

Я запутался в том, как gcc и clang выстраивают структуры, когда задействованы и padding, и наследование. Вот пример программы:

#include <string.h>
#include <stdio.h>

struct A
{
    void* m_a;
};

struct B: A
{
    void* m_b1;
    char m_b2;
};

struct B2
{
    void* m_a;
    void* m_b1;
    char m_b2;
};

struct C: B
{
    short m_c;
};

struct C2: B2
{
    short m_c;
};

int main ()
{
    C c;
    memset (&c, 0, sizeof (C));
    memset ((B*) &c, -1, sizeof (B));

    printf (
        "c.m_c = %d; sizeof (A) = %d sizeof (B) = %d sizeof (C) = %d\n", 
        c.m_c, sizeof (A), sizeof (B), sizeof (C)
        );

    C2 c2;
    memset (&c2, 0, sizeof (C2));
    memset ((B2*) &c2, -1, sizeof (B2));

    printf (
        "c2.m_c = %d; sizeof (A) = %d sizeof (B2) = %d sizeof (C2) = %d\n", 
        c2.m_c, sizeof (A), sizeof (B2), sizeof (C2)
        );

    return 0;
}

Выход:

$ ./a.out
c.m_c = -1; sizeof (A) = 8 sizeof (B) = 24 sizeof (C) = 24
c2.m_c = 0; sizeof (A) = 8 sizeof (B2) = 24 sizeof (C2) = 32

Структуры C1 и C2 расположены по-разному. В C1 m_c размещается в отступе структуры B1 и поэтому перезаписывается 2-м memset (); с С2 этого не происходит.

Используемые компиляторы:

$ clang --version
Ubuntu clang version 3.3-16ubuntu1 (branches/release_33) (based on LLVM 3.3)
Target: x86_64-pc-linux-gnu
Thread model: posix

$ c++ --version
c++ (Ubuntu 4.8.2-19ubuntu1) 4.8.2
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

То же самое происходит с опцией -m32 (очевидно, что размеры на выходе будут другими).

Как в x86, так и в x86_64 версиях компилятора Microsoft Visual Studio 2010 C++ такой проблемы нет (т. Е. Они одинаково выкладывают структуры С1 и С2)

Если это не ошибка и это дизайн, то мои вопросы:

  1. Каковы точные правила для выделения или не выделения полей производной структуры в отступах (например, почему это не происходит с C2?)
  2. Есть ли способ переопределить это поведение с помощью переключателей / атрибутов (т.е. выложить так же, как MSVC)?

Заранее спасибо.

Владимир

4 ответа

Для всех, кто опускает этот вопрос и отвечает на вопросы ОП с самодовольным негодованием по поводу того, как ужасно UB его рукописные memcpy было... учтите, что разработчики как libC++, так и libstdC++ попадают в одну и ту же яму. В обозримом будущем на самом деле очень важно понять, когда набивка хвоста используется повторно, а когда - нет. Хорошо на OP для постановки этого вопроса.

Правила Itanium ABI для разметки структуры здесь. Соответствующая формулировка

Если D является базовым классом, обновите sizeof(C) до max (sizeof(C), offset(D)+nvsize(D)).

Здесь "dsize, nvsize и nvalign [POD-типов] определены как их обычный размер и выравнивание", но nvsize не-POD-типа определяется как " не виртуальный размер объекта, который размер O без виртуальных баз [а также без хвостовой подкладки]. " Так что, если D - POD, мы никогда ничего не вкладываем в его хвостовую подкладку; в то время как если D не POD, нам разрешается втиснуть следующий элемент (или базу) в его хвостовую часть.

Следовательно, любой не POD-тип (даже тривиально копируемый!) Должен учитывать возможность того, что он имеет важные данные, вставленные в его хвостовую часть. Как правило, это нарушает предположения разработчиков о том, что допустимо делать с тривиально копируемыми типами (а именно, что вы можете тривиально их копировать).

Тестовый случай Wandbox:

#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
}

Ваш код демонстрирует неопределенное поведение, так как C и C2 не являются POD, и запоминание случайных битов их данных не допускается.

Однако в более отдаленной перспективе это сложный вопрос. Существующий C ABI на платформе (Unix) разрешал такое поведение (это для C++98, который разрешил это). Затем Комитет несовместимо изменил правила в C++03 и C++11. По крайней мере, у Clang есть возможность перейти на более новые правила. Конечно, C ABI в Unix не изменился, чтобы приспособиться к новым правилам C++ 11 для помещения вещей в отступы, поэтому компиляторы не могут просто обновить, так как это сломало бы весь ABI.

Я полагаю, что GCC хранит изменения, нарушающие ABI для 5.0, и это может быть одним из них.

Windows всегда запрещал эту практику в своем C ABI и поэтому не имеет проблем, насколько я знаю.

Разница в том, что компилятору разрешено использовать заполнение предыдущего объекта, если этот объект уже "не просто данные", и манипулировать им, скажем, с помощью memcpy не поддерживается.

B структура - это не просто данные, потому что это производный объект, и поэтому его свободное пространство может быть использовано, потому что если вы memcpy-ин B Например, вы уже нарушаете контракт.

B2 вместо этого это просто структура и обратная совместимость требует, чтобы его размер (включая свободное пространство) был просто памятью, с которой ваш код может играть, используя memcpy,

Спасибо всем за вашу помощь.

Суть в том, что компиляторам C++ разрешено повторно использовать заполнение хвостом структур, отличных от POD, при разметке полей производных структур. И GCC, и Clang используют это разрешение, MSVC - нет. Похоже, что у GCC есть флаг предупреждения -Wabi, который должен помочь в выявлении случаев потенциальной несовместимости ABI, но он не выдал предупреждений в приведенном выше примере.

Похоже, что единственный способ предотвратить это - ввести явные поля заполнения хвоста.

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