Подтвердить что ты не робот

Тип punning struct в C и С++ через объединение

Я скомпилировал это в gcc и g++ с педантичным, и я не получаю предупреждение ни в одном:

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

struct a {
    struct a *next;
    int i;
};

struct b {
    struct b *next;
    int i;
};

struct c {
    int x, x2, x3;
    union {
        struct a a;
        struct b b;
    } u;
};

void foo(struct b *bar) {
    bar->next->i = 9;
    return;
}

int main(int argc, char *argv[]) {
    struct c c;
    memset(&c, 0, sizeof c);
    c.u.a.next = (struct a *)calloc(1, sizeof(struct a));
    foo(&c.u.b);
    printf("%d\n", c.u.a.next->i);
    return 0;
}

Является ли это законным для C и С++? Я читал о том, как писать, но я не понимаю. Является ли foo(&c.u.b) отличным от foo((struct b *)&c.u.a)? Разве они не были бы точно такими же? Это исключение для структур в объединении (от C89 в 3.3.2.3) говорит:

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

В объединении первый член struct a равен struct a *next, а первый член struct b равен struct b *next. Как вы видите, указатель на struct a *next записывается, а затем в foo читается указатель на struct b *next. Являются ли они совместимыми типами? Они оба указывают на структуру, и указатели на любую структуру должны быть одного размера, поэтому они должны быть совместимыми, а макет должен быть таким же правильным? Можно ли читать i из одной структуры и писать в другую? Могу ли я совершать какие-либо нарушения псевдонимов или нарушения типа?

4b9b3361

Ответ 1

В C:

struct a и struct b не являются совместимыми типами. Даже в

typedef struct s1 { int x; } t1, *tp1;
typedef struct s2 { int x; } t2, *tp2;

s1 и s2 не являются совместимыми типами. (См. Пример в 6.7.8/p5.) Легкий способ определения несовместимых структур заключается в том, что если два типа структуры совместимы, то что-то одного типа может быть присвоено чему-то другого типа. Если вы ожидаете, что компилятор будет жаловаться при попытке сделать это, то они не являются совместимыми типами.

Следовательно, struct a * и struct b * также не являются совместимыми типами, поэтому struct a и struct b не имеют общей исходной подпоследовательности. Вместо этого ваш профсоюз-караунд регулируется тем же правилом, что и объединение в других случаях (6.5.2.3 сноска 95):

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


В С++ struct a и struct b также не имеют общей исходной подпоследовательности. [class.mem]/p18 (цитирование N4140):

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

[basic.types]/P9:

Если два типа T1 и T2 являются одним и тем же типом, то T1 и T2 являются совместимые с макетами типы. [Примечание. Соответствующие компоновке перечисления описаны в 7.2. Структуры компоновки, совместимые с макетами, и Стандартные макеты соединений описаны в 9.2. -end note]

struct a * и struct b * не являются ни структурами, ни союзами, ни перечислениями; поэтому они совместимы только с макетами, если они одного типа, а это не так.

Верно, что ([basic.compound]/p3)

Указатели на cv-квалификационные и cv-неквалифицированные версии (3.9.3) of совместимые с макетами типы должны иметь одинаковое представление значений и (3.11).

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

Ответ 2

Что вы могли бы сделать (и я был укушен этим раньше), объявляет как начальный указатель структуры как void*, так и делает кастинг. Поскольку void конвертируется в/из любого типа указателя, вы будете вынуждены платить налог за уродство, а не рисковать gcc, переупорядочивая ваши операции (что я видел, даже если вы используете союз), в результате ошибки компилятора в некоторых версиях. Как @T.C. правильно указывает, совместимость макета данного типа означает, что на уровне языка они являются конвертируемыми; даже если типы, кстати, имеют одинаковый размер, они не обязательно совместимы с макетами; которые могли бы дать некоторым жадным компиляторам взять на себя некоторые другие вещи, основанные на этом.

Ответ 3

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

Да, struct a и struct b не являются совместимыми типами, а указатели на них также несовместимы.

Да, то, что вы делаете, является незаконным даже с устаревшей точки зрения стандарта C89. Однако может быть интересно отметить, что если вы измените порядок элементов в struct a и struct b, вы сможете получить доступ к int i экземпляра struct c (но не получить доступ к его указателю next в любым способом, т.е. bar->i = 9; вместо bar->next->i = 9;), но только с точки зрения C89.

Но даже если вы измените порядок элементов в двух struct s, то, что вы делаете, все равно будет незаконным с точки зрения стандартов C99 и C11 (как интерпретируется коммитом). На C99 часть указанного вами стандарта была изменена на:

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

Последняя фраза немного неоднозначна, так как вы можете интерпретировать "видимые" несколькими способами, но, согласно коммиту, это означает, что проверка должна выполняться на объекте типа объединения, о котором идет речь.

Итак, в вашем случае правильный способ справиться с этим был бы следующим:

struct a {
    int i;
    struct a *next;
};

struct b {
    int i;
    struct b *next;
};

union un {
    struct a a;
    struct b b;
};

struct c {
    int x, x2, x3;
    union un u;
};

/* ... */

void foo(union un *bar) {
    bar.b->next->i = 9; /* This is the "inspection" operation */
    return;
}

/* ... */

foo(&c.u);

Это хорошо и интересно с точки зрения языка-юриста, но на самом деле, если вы не применяете к ним разные настройки упаковки, struct с той же начальной последовательностью будет иметь тот же макет ( в 99,9% случаев). Фактически, они должны иметь один и тот же макет даже в исходной настройке, поскольку указатели на struct a и struct b должны иметь одинаковый размер. Таким образом, если ваш компилятор не становится противным, когда вы нарушаете строгий псевдоним, вы можете более или менее безопасно отображать между ними или использовать их в союзе так, как вы их используете сейчас.

РЕДАКТИРОВАТЬ: как отмечено @underscore_d в комментариях к этому ответу, так как соответствующие предложения в стандартах С++ не имеют строки "где бы ни было объявлено завершенным типом объединения видимым" в их соответствующих частях, возможно, что стандарт С++ имеет такую ​​же позицию по этому вопросу, как и стандарт C89.

Ответ 4

Да, это прекрасно; полужирная часть цитаты в вашем вопросе охватывает этот случай.