Разъяснение на примере профсоюзов в стандарте C11

Следующий пример приведен в стандарте C11, 6.5.2.3

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

Почему имеет значение, что тип объединения виден функции f?

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

Ответ 1

Это важно из-за пункта 6.5.2.3 пункта 6 (выделено мной):

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

Это не ошибка, требующая диагностики (синтаксическая ошибка или нарушение ограничения), но поведение undefined, потому что члены m объектов struct t1 и struct t2 занимают одно и то же хранилище, но потому что struct t1 и struct t2 являются разными типами, компилятору разрешено предположить, что они этого не делают - в частности, что изменения в p1->m не повлияют на значение p2->m. Компилятор мог бы, например, сохранить значение p1->m в регистре при первом доступе, а затем не перезагрузить его из памяти при втором доступе.

Ответ 2

Примечание. Этот ответ напрямую не отвечает на ваш вопрос, но я думаю, что он имеет значение и слишком велик для комментариев.


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

Цель обычного правила начальной последовательности - гарантировать одинаковое расположение структур. Однако это даже не проблема, поскольку структуры содержат только один int, а структурам не разрешено иметь начальное заполнение.

Обратите внимание, что, как обсуждалось здесь, разделы в документах ISO/IEC под названием Note или Example являются "ненормативными", что означает, что они фактически не являются частью спецификация.


Было высказано предположение, что этот код нарушает правило строгого псевдонижа. Вот правило: от C11 6.5/7:

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

  • тип, совместимый с эффективным типом объекта, [...]

В этом примере доступ к объекту (обозначается символом p2->m или p1->m) имеет тип int. Выражения lvalue p1->m и p2->m имеют тип int. Поскольку int совместим с int, нарушения не существует.

Это правда, что p2->m означает (*p2).m, однако это выражение не имеет доступа к *p2. Он получает доступ только к m.


Любое из следующего будет undefined:

*p1 = *(struct t1 *)p2;   // strict aliasing: struct t2 not compatible with struct t1
p2->m = p1->m++;          // object modified twice without sequence point

Ответ 3

Учитывая декларации:

union U { int x; } u,*up = &u;
struct S { int x; } s,*sp = &s;

значения lx ux, up->x, sx и sp->x относятся к типу int, но любой доступ к любому из этих значений будет (по крайней мере, с указателями, инициализированными, как показано), также получит доступ к сохраненному значению объект типа union U или struct S Поскольку N1570 6.5p7 разрешает доступ к объектам этих типов только через lvalues, типы которых являются символьными типами, или другими структурами или объединениями, которые содержат объекты типа union U и struct S, это не налагает каких-либо требований к поведению кода, который пытается использовать любое из этих значений.

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

Если интерпретировать правило как применимое только в тех случаях, когда значения l используются так, что псевдонимы, по-видимому, не связаны со значениями других типов, такая интерпретация будет определять поведение кода следующим образом:

struct s1 {int x; float y;};
struct s2 {int x; double y;};
union s1s2 { struct s1 v1; struct s2 v2; };

int get_x(void *p) { return ((struct s1*)p)->x; }

когда последнему передали struct s1*, struct s2* или union s1s2* которая идентифицирует объект его типа, или только что полученный адрес любого из членов union s1s2. В любом контексте, когда реализация будет видеть достаточно, чтобы иметь основания заботиться о том, будут ли операции с исходными и производными l-значениями влиять друг на друга, она сможет увидеть взаимосвязь между ними.

Обратите внимание, однако, что такая реализация не потребовалась бы для возможности наложения псевдонимов в коде, как показано ниже:

struct position {double px,py,pz;};
struct velocity {double vx,vy,vz;};

void update_vectors(struct position *pos, struct velocity *vel, int n)
{
  for (int i=0; i<n; i++)
  {
    pos[i].px += vel[i].vx;
    pos[i].py += vel[i].vy;
    pos[i].pz += vel[i].vz;
  }
}

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

Существует много различий между этими двумя примерами, и, таким образом, множество указаний, которые компилятор может использовать для обеспечения реалистичной возможности того, что первый код передает struct s2*, он может получить доступ к struct s2, не допуская более сомнительных вероятность того, что операции над pos[] во втором исследовании могут повлиять на элементы vel[].

Многие реализации, стремящиеся к полезной поддержке правила Common Initial Sequence полезным способом, могли бы обрабатывать первое, даже если не было объявлено ни одного типа union, и я не знаю, что авторы Стандарта намеревались просто добавить объявление типа union заставить компиляторы учесть возможность произвольного псевдонима среди общих начальных последовательностей членов в нем. Самым естественным намерением, которое я вижу для упоминания типов объединения, было бы то, что компиляторы, которые не могут воспринимать любую из многочисленных подсказок, присутствующих в первом примере, могут использовать наличие или отсутствие любого полного объявления типа объединения, содержащего два типа, в качестве указания на то, Значения одного такого типа могут использоваться для доступа к другому.

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