Гарантируется ли, что C-структуры с одинаковыми типами элементов имеют одинаковый макет в памяти?

По сути, если у меня есть

typedef struct {
    int x;
    int y;
} A;

typedef struct {
    int h;
    int k;
} B;

и я имею A a, гарантирует ли стандарт С, что ((B*)&a)->k такой же как a.y?

4 ответа

Решение

Гарантируется ли, что C-структуры с одинаковыми типами элементов имеют одинаковый макет в памяти?

Почти да Достаточно близко для меня.

От n1516, раздел 6.5.2.3, пункт 6:

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

Это означает, что если у вас есть следующий код:

struct a {
    int x;
    int y;
};

struct b {
    int h;
    int k;
};

union {
    struct a a;
    struct b b;
} u;

Если вы назначаете u.aстандарт говорит, что вы можете прочитать соответствующие значения из u.b, Это расширяет границы правдоподобия, чтобы предположить, что struct a а также struct b может иметь разную компоновку, учитывая это требование. Такая система была бы патологической до крайности.

Помните, что стандарт также гарантирует, что:

  • Структуры никогда не являются ловушками представлений.

  • Адреса полей в структуре увеличиваются (a.x всегда раньше a.y).

  • Смещение первого поля всегда равно нулю.

Однако и это важно!

Вы перефразировали вопрос,

стандарт C гарантирует, что ((B*)&a)->k такое же как ау?

Нет! И это очень явно заявляет, что они не то же самое!

struct a { int x; };
struct b { int x; };
int test(int value)
{
    struct a a;
    a.x = value;
    return ((struct b *) &a)->x;
}

Это нарушение псевдонимов.

Перепутывание других ответов с предупреждением о разделе 6.5.2.3. Видимо, есть некоторые споры о точной формулировке anywhere that a declaration of the completed type of the union is visible и, по крайней мере, GCC не реализует это так, как написано. Здесь и здесь есть несколько сообщений о тангенциальных дефектах C WG с последующими комментариями комитета.

Недавно я попытался выяснить, как другие компиляторы (в частности, GCC 4.8.2, ICC 14 и clang 3.4) интерпретируют это, используя следующий код из стандарта:

// Undefined, result could (realistically) be either -1 or 1
struct t1 { int m; } s1;
struct t2 { int m; } s2;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union {
        struct t1 s1;
        struct t2 s2;
    } u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1, лязг: -1, ICC: 1 и предупреждает о нарушении алиасинга

// Global union declaration, result should be 1 according to a literal reading of 6.5.2.3/6
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
};
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union u u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1, лязг: -1, ICC: 1, но предупреждает о нарушении псевдонимов

// Global union definition, result should be 1 as well.
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
} u;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1, лязг: -1, ICC: 1, без предупреждения

Конечно, без строгой оптимизации псевдонимов все три компилятора каждый раз возвращают ожидаемый результат. Так как clang и gcc не имеют выдающихся результатов ни в одном из случаев, единственная реальная информация исходит от отсутствия у ICC диагностики по последнему. Это также согласуется с примером, приведенным комитетом по стандартам в первом отчете о дефектах, упомянутом выше.

Другими словами, этот аспект C является настоящим минным полем, и вам следует опасаться, что ваш компилятор делает правильные вещи, даже если вы следуете стандарту в букве. Тем хуже, что интуитивно понятно, что такая пара структур должна быть совместима в памяти.

Этот вид псевдонимов специально требует union тип. C11 §6.5.2.3/6:

Одна специальная гарантия сделана для того, чтобы упростить использование объединений: если объединение содержит несколько структур, которые имеют общую начальную последовательность (см. Ниже), и если объект объединения в настоящее время содержит одну из этих структур, разрешается проверять общие Начальная часть любого из них везде, где видна декларация о законченном типе объединения. Две структуры имеют общую начальную последовательность, если соответствующие элементы имеют совместимые типы (и, для битовых полей, одинаковой ширины) для последовательности из одного или нескольких начальных элементов.

Этот пример следует:

Следующее не является допустимым фрагментом (потому что тип объединения не виден в функции f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
    if (p1->m < 0)
          p2->m = -p2->m;
    return p1->m;
}

int g() {
    union {
          struct t1 s1;
          struct t2 s2;
    } u;
    /* ... */
    return f(&u.s1, &u.s2);}
}

Требования заключаются в том, что 1. объект с псевдонимом хранится внутри union и 2. что определение этого union тип находится в области видимости.

Для чего это стоит, соответствующее отношение начальная-подпоследовательность в C++ не требует union, И вообще, такие union зависимость была бы чрезвычайно патологическим поведением для компилятора. Если существует какой-либо способ существования типа объединения, который может повлиять на конкретную модель памяти, вероятно, лучше не пытаться представить это.

Я предполагаю, что цель заключается в том, что верификатор доступа к памяти (например, Valgrind на стероидах) может проверить потенциальную ошибку наложения псевдонимов в соответствии с этими "строгими" правилами.

Я хочу расширить ответ @Dietrich Epp. Вот цитата из C99:

6.7.2.1 пункт 14... Указатель на объект объединения, соответствующим образом преобразованный, указывает на каждый из его членов... и наоборот.

Это означает, что мы можем скопировать память из структуры в объединение, содержащее ее:

      struct a
{
    int foo;
    char bar;
};

struct b
{
    int foo;
    char bar;
};

union ab
{
    struct a a;
    struct b b;
};

void test(struct a *aa)
{
    union ab ab;
    memcpy(&ab, aa, sizeof *aa);

    // ...
}

C99 также говорит:

6.5.2.3 пункт 5 Для упрощения использования объединений делается одна специальная гарантия: если объединение содержит несколько структур, имеющих общую начальную последовательность..., и если объект объединения в данный момент содержит одну из этих структур, разрешено чтобы проверить общую начальную часть любого из них в любом месте, где видно объявление полного типа объединения. Две структуры совместно используют общую начальную последовательность, если соответствующие члены имеют совместимые типы.... для последовательности из одного или нескольких начальных элементов.

Это означает, что после :

      ab.a.bar;
ab.b.bar;

Структура может быть инициализирована в отдельной единице трансляции, а копирование выполняется в стандартной библиотеке (вне контроля компилятора). Таким образом,будет копировать побайтно значение объекта типаи компилятор должен убедиться, что результат действителен для обеих структур. Компилятор не может делать ничего, кроме как генерировать инструкции, которые считываются из соответствующего смещения памяти для обеих этих строк, поэтому адрес должен быть одинаковым.

Несмотря на то, что это не указано явно, я бы сказал, что стандарт подразумевает, что C-структуры с одинаковыми типами членов имеют одинаковую компоновку в памяти.

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