Является ли это использование профсоюзов строго соответствующими?

С учетом кода:

struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };

static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}

int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
  if (read_s1x(&p1->v1))
  {
    unsigned short temp;
    temp = p3->v1.x;
    p3->v2.x = temp;
    write_s2x(&p2->v2,1234);
    temp = p3->v2.x;
    p3->v1.x = temp;
  }
  return read_s1x(&p1->v1);
}
int test2(int x)
{
  union s1s2 q[2];
  q->v1.x = 4321;
  return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
  printf("%d\n",test2(0));
}

Во всей программе существует один объект объединения - q. Его активный член установлен на v1, а затем на v2, а затем на v1 снова. Код использует только адрес-оператора на q.v1 или результирующий указатель, когда этот элемент активен, а также q.v2. Поскольку p1, p2 и p3 являются одинаковыми, для доступа к p1->v1 и p3->v2 для доступа к p2->v2 должно быть совершенно легально использовать p3->v1.

Я не вижу ничего, что могло бы оправдать компилятор, неспособный вывести 1234, но многие компиляторы, включая clang и gcc, генерируют код, который выводит 4321. Я думаю, что происходит то, что они решают, что операции на p3 на самом деле не будут изменить содержимое любых бит в памяти, их можно просто игнорировать вообще, но я не вижу ничего в стандарте, который оправдывал бы игнорирование того факта, что p3 используется для копирования данных из p1->v1 в p2->v2 и наоборот.

Есть ли что-нибудь в стандарте, оправдывающее такое поведение, или компиляторы просто не следуют ему?

Ответ 1

Я считаю, что ваш код является совместимым, и есть недостаток в режиме -fstrict-aliasing GCC и Clang.

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

В стандарте С++ [class.union]/5 определяет, что происходит, когда оператор = используется в выражении доступа к объединению. В стандарте С++ указано, что, когда объединение участвует в выражении доступа к членству встроенного оператора =, активный член объединения изменяется на член, участвующий в выражении (если тип имеет тривиальный конструктор, но потому что это код C, у него есть тривиальный конструктор).

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

Даже если я использую место размещения new для явного изменения члена профсоюза, который должен быть подсказкой для компилятора, который изменил активный член, GCC по-прежнему генерирует код, который выводит 4321.

Это похоже на ошибку с GCC и Clang, предполагая, что переключение активного члена объединения не может произойти здесь, потому что они не могут распознать возможность p1, p2 и p3 всех указывать на один и тот же объект.

GCC и Clang (и почти каждый другой компилятор) поддерживают расширение на C/С++, где вы можете прочитать неактивный член объединения (получая все возможное значение мусора в результате), но только если вы сделаете этот доступ в выражение доступа к члену, включающее объединение. Если v1 не был активным членом, read_s1x не будет определяться поведением в этом конкретном правиле реализации, поскольку объединение не входит в выражение доступа к члену. Но поскольку v1 является активным членом, это не имеет значения.

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

Ответ 2

При строгой интерпретации стандарта этот код может не соответствовать. Позвольте сосредоточиться на тексте известного §6.5p7:

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

(акцент мой)

В ваших функциях read_s1x() и write_s2x() сделайте напротив того, что я выделил выше, в контексте всего вашего кода. Только в этом параграфе вы можете сделать вывод, что это не разрешено: указателю на union s1s2 будет разрешено псевдоним указателя на struct s1, но не наоборот.

Эта интерпретация курса означала бы, что код должен работать по назначению, если вы "встроите" эти функции вручную в свой test(). Это действительно имеет место здесь с gcc 6.2 для i686-w64-mingw32.


Добавление двух аргументов в пользу приведенной выше строгой интерпретации:

  • В то время как всегда разрешено псевдоним любого указателя с char *, массив символов не может быть псевдонимом любым другим типом.

  • Учитывая (здесь несвязанный) §6.5.2.3p6:

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

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

Ответ 3

Я не читал стандарт, но играть с указателями в режиме строгого сглаживания (т.е. используя -fstrict-alising) опасно. См. gcc online doc:

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

union a_union {
  int i;
  double d;
};

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

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

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

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

int f() {
  double d = 3.0;
  return ((union a_union *) &d)->i;
}

Опция -fstrict-aliasing активируется на уровнях -O2, -O3, -Os.

Нашел что-нибудь подобное во втором примере huh?

Ответ 4

Это не о том, чтобы соответствовать или не соответствовать - это одна из оптимизационных "ловушек". Все ваши структуры данных были оптимизированы, и вы передаете один и тот же указатель на оптимизированные данные, чтобы дерево выполнения было уменьшено до простого значения printf.

  sub rsp, 8
  mov esi, 4321
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

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

int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/

main:
  sub rsp, 8
  mov esi, 1234
  mov edi, OFFSET FLAT:.LC0
  xor eax, eax
  call printf
  xor eax, eax
  add rsp, 8
  ret

это довольно тривиальный тест, только искусственно сделанный немного сложнее.