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

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

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

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

и у меня есть A a, гарантирует ли стандарт C, что ((B*)&a)->k совпадает с a.y?

Ответ 1

Являются ли 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 совпадает с a.y?

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

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

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

Ответ 2

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

Недавно я попытался выяснить, как другие компиляторы (в частности, 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, clang: -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, clang: -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, clang: -1, ICC: 1, без предупреждения

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

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

Ответ 3

Для такого типа сглаживания требуется тип union. C11 §6.5.2.3/6:

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

В этом примере:

Ниже приведен неверный фрагмент (поскольку тип объединения не является видимый внутри функции 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 находится в области видимости.

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

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