Нарушение строгой сглаживания в C, даже без кастинга?

Как *i и u.i печатать разные номера в этом коде, хотя i определяется как int *i = &u.i;? Я могу только предположить, что я запускаю UB здесь, но я не понимаю, как именно.

(ideone demo реплицируется, если я выбираю "C" в качестве языка. Но, как указывал @2501, не "C99 strict" - это язык. снова, я получаю проблему с gcc-5.3.0 -std=c99!)

// gcc       -fstrict-aliasing -std=c99   -O2
union
{   
    int i;
    short s;
} u;

int     * i = &u.i;
short   * s = &u.s;

int main()
{   
    *i  = 2;
    *s  = 100;

    printf(" *i = %d\n",  *i); // prints 2
    printf("u.i = %d\n", u.i); // prints 100

    return 0;
}

(gcc 5.3.0, с -fstrict-aliasing -std=c99 -O2, также с -std=c11)

Моя теория такова, что 100 является "правильным" ответом, потому что запись члену объединения через short -lvalue *s определяется как таковая (для этой платформы /endianness/whatever ). Но я думаю, что оптимизатор не понимает, что запись в *s может быть псевдоним u.i, и поэтому он считает, что *i=2; - единственная строка, которая может повлиять на *i. Это разумная теория?

Если *s может псевдоним u.i, а u.i может быть псевдоним *i, то, конечно, компилятор должен думать, что *s может псевдоним *i? Не следует ли переходить на псевдонимы "транзитивным"?

Наконец, у меня всегда было это предположение, что проблемы с жестким сглаживанием были вызваны плохим кастингом. Но в этом нет никакого каста!

(Мой фон - С++, я надеюсь, что задаю здесь разумный вопрос о C. Мое (ограниченное) понимание состоит в том, что на C99 допустимо писать через один член профсоюза, а затем читать через другого члена другого типа.)

Ответ 1

Неисправность выдается опцией оптимизации -fstrict-aliasing. Его поведение и возможные ловушки описаны в документации GCC:

Обратите особое внимание на такой код:

      union a_union {
        int i;
        double d;
      };

      int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

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

      int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

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

Ответ 2

Стандарт C (т.е. C11, n1570), 6.5p7:

Объект должен иметь сохраненное значение, доступное только выражением lvalue, которое имеет один из следующих типов:

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

Выражения lvalue ваших указателей: не union, поэтому это исключение не применяется. Компилятор корректно использует это поведение undefined.

Сделайте указатели типов указателей на тип union и разыщите с соответствующим членом. Это должно работать:

union {
    ...
} u, *i, *p;

Ответ 3

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

Для обоснования этого следует рассмотреть:

void f(int *a, short *b) { 

Цель этого правила заключается в том, что компилятор может считать a и b не псевдонимом и генерировать эффективный код в f. Но если компилятор должен был учитывать тот факт, что a и b могут быть перекрывающимися членами объединения, на самом деле они не могут сделать эти предположения.

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

Ответ 4

Этот код действительно вызывает UB, потому что вы не соблюдаете строгое правило псевдонимов. n1256 черновик состояний C99 в 6.5 Выражения §7:

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

Между *i = 2; и printf(" *i = %d\n", *i); изменен только короткий объект. С помощью строгого правила псевдонимов компилятор может предположить, что объект int, на который указывает i, не был изменен, и он может напрямую использовать кешированное значение без перезагрузки его из основной памяти.

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

Для второй печати соединения ссылаются на один и тот же стандарт в 6.2.6.1. Представления типов/Общие положения §7:

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

Так как u.s был сохранен, u.i принял значение, неопределенное стандартным

Но мы можем прочитать далее в 6.5.2.3 Структура и члены объединения §3 примечание 82:

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

Хотя примечания не являются нормативными, они позволяют лучше понять стандарт. Когда u.s хранится с помощью указателя *s, байты, соответствующие короткому значению, были изменены на значение 2. Предполагая небольшую систему endian, поскольку 100 меньше, чем значение short, представление как int должно теперь быть 2, так как байты верхнего порядка равны 0.

TL/DR: даже если он не является нормативным, примечание 82 должно требовать, чтобы в маленькой системной системе семейства x86 или x64 printf("u.i = %d\n", u.i); печаталось 2. Но в соответствии с правилом строгого сглаживания компилятору все же допускается что значение, указанное i, не изменилось и может печатать 100

Ответ 5

Вы исследуете несколько спорную область стандарта C.

Это строгое правило псевдонимов:

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

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

(C2011, 6.5/7)

Значение lvalue *i имеет тип int. Выражение lvalue *s имеет тип short. Эти типы несовместимы друг с другом и не совместимы с каким-либо другим конкретным типом, а также не допускает, чтобы строгое правило псевдонимов позволяло использовать любую другую альтернативу, которая позволяет обоим обращениям соответствовать, если указатели сглажены.

Если хотя бы один из обращений является несоответствующим, тогда поведение undefined, поэтому результат, который вы сообщаете, или вообще любой другой результат, является полностью приемлемым. На практике компилятор должен создать код, который переупорядочивает назначения с помощью вызовов printf() или использует ранее загруженное значение *i из регистра вместо того, чтобы перечитывать его из памяти или что-то подобное.

Вышеупомянутые споры возникают, потому что люди иногда указывают на сноску 95:

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

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

Ответ 6

Похоже, это результат того, что оптимизатор делает свою магию.

С -O0 обе строки печатают 100, как ожидалось (предполагая мало-endian). С -O2 происходит некоторая переупорядоченность.

gdb дает следующий вывод:

(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000

Temporary breakpoint 1, main () at /tmp/x1.c:14
14      {
(gdb) step
15          *i  = 2;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
15          *i  = 2;
(gdb)
16          *s  = 100;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
 *i = 2
19          printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22      }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)

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

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

Любопытно, что даже с -Wstrict-aliasing=2 gcc (по 4.8.4) не жалуется на этот код.

Ответ 7

Независимо от случая или по дизайну, C89 включает язык, который интерпретируется двумя разными способами (наряду с различными интерпретациями между ними). Речь идет о том, когда необходимо, чтобы компилятор должен был признать, что хранилище, используемое для одного типа, может быть доступно через указатели другого. В примере, приведенном в обосновании C89, aliasing рассматривается между глобальной переменной, которая явно не является частью какого-либо объединения и указателем на другой тип, и ничто в коде не предполагает, что может возникнуть сглаживание.

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

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

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