Доступ к членам профсоюза C через указатели

Имеет ли доступ к членам объединения через указатель, как в приведенном ниже примере, поведение undefined на C99? Намерение кажется достаточно ясным, но я знаю, что существуют некоторые ограничения в отношении псевдонимов и союзов.

union { int i; char c; } u;

int  *ip = &u.i;
char *ic = &u.c;

*ip = 0;
*ic = 'a';
printf("%c\n", u.c);

Ответ 1

Это неуказанное (тонко отличное от undefined) поведение для доступа к соединению любым другим элементом, кроме последнего. Это подробно описано в приложении C99 J:

Следующие неуказаны:
   :
    значение члена объединения, отличного от последнего, сохраненного в (6.2.6.1).

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

u.c = 'a';        // direct write.
*(&(u.c)) = 'a';  // variation on yours, writing through element pointer.
(&u)->c = 'a';    // writing through structure pointer.

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

// Compile with "-O3 -std=c99" eg:
//  clang -O3 -std=c99 test.c
//  gcc -O3 -std=c99 test.c
// On clang v3.5.1, output is "123"
// On gcc 4.8.4, output is "1073741824"
//
// Different outputs, so either:
// * program invokes undefined behaviour; both compilers are correct OR
// * compiler vendors interpret standard differently OR
// * one compiler or the other has a bug

#include <stdio.h>

union u
{
    int i;
    float f;
};

int someFunc(union u * up, float *fp)
{
    up->i = 123;
    *fp = 2.0;     // does this set the union member?
    return up->i;  // then this should not return 123!
}

int main(int argc, char **argv)
{
    union u uobj;
    printf("%d\n", someFunc(&uobj, &uobj.f));
    return 0;
}

который выводит разные значения для разных компиляторов. Однако я считаю, что это происходит потому, что он фактически нарушает правила здесь, потому что он записывает член f, затем читает член i и, как показано в Приложении J, это неуказано.

В 6.5.2.3 есть сноска 82, в которой говорится:

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

Однако, поскольку это, похоже, противоречит комментарию к приложению J, а в сноске к разделу, посвященному выражению формы x.y, оно может не применяться к обращениям через указатель.

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

В качестве примера рассмотрим предоставленную функцию:

int someFunc(union u * up, float *fp)
{
    up->i = 123;
    *fp = 2.0;     // does this set the union member?
    return up->i;  // then this should not return 123!
}

Реализация свободна предположить, что, поскольку вы не должны использовать память с псевдонимами, up->i и *fp - это два разных объекта. Поэтому можно предположить, что вы не изменяете значение up->i после того, как вы установите его на 123, чтобы он мог просто вернуть 123, не просматривая фактическое содержимое переменной снова.

Если вместо этого вы изменили оператор установки указателя на:

up->f = 2.0;

то это сделает сноску 82 применимой, а возвращаемое значение будет повторной интерпретацией float как целого.

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


Интересно отметить, что неуказанное поведение вызвано не самой функцией, а вызовом:

union u up;
int x = someFunc (&u, &(up.f)); // <- aliasing here

Если бы вы назовете это так:

union u up;
float down;
int x = someFunc (&u, &down); // <- no aliasing

это не проблема.

Ответ 2

Нет, это не будет, но вам нужно следить за тем, какой последний тип, который вы вложили в союз, был. Если бы я изменил порядок ваших назначений int и char, это было бы совсем по-другому:

#include <stdio.h>

union { int i; char c; } u;

int main()
{
    int  *ip = &u.i;
    char *ic = &u.c;

    *ic = 'a';
    *ip = 123456;

    printf("%c\n", u.c); /* trying to print a char even though 
                            it currently storing an int,
                            in this case it prints '@' on my machine */

    return 0;
}

EDIT: Некоторое объяснение, почему оно могло напечатать 64 ( "@" ).

Двоичное представление 123456 - 0001 1110 0010 0100 0000.

Для 64 это 0100 0000.

Вы можете видеть, что первые 8 бит идентичны, и поскольку printf инструктируется прочитать первые 8 бит, он печатает только столько.

Ответ 3

Единственная причина, по которой это не UB, состоит в том, что вам повезло/не повезло, чтобы выбрать char для одного из типов, а типы символов могут быть похожими на что-либо в C. Если типы были, например, int и float, доступ через указатели будет нарушением псевдонимов и, следовательно, undefined. Для прямого доступа через объединение поведение считалось вполне определенным как часть интерпретации отчета о дефектах 283:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

Конечно, вам все равно нужно убедиться, что представление типа, используемого для записи, также может быть интерпретировано как допустимое (не-ловушечное) представление для типа, используемого впоследствии для чтения.