Почему недопустимо, чтобы тип объединения, объявленный в одной функции, использовался в другой функции? - PullRequest
0 голосов
/ 26 сентября 2018

Когда я прочитал ISO / IEC 9899: 1999 (см. 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);
}

Я не обнаружил ошибок и предупреждений при тестировании.

Мой вопрос: почему этот фрагмент недействителен?

Ответы [ 3 ]

0 голосов
/ 26 сентября 2018

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

Эта функция

int f(struct t1* p1, struct t2* p2);

предполагает, что p1 != p2, поскольку они формально указывают на разные типы.В результате оптимизатор может предположить, что p2->m = -p2->m; не влияет на p1->m;сначала он может прочитать значение p1->m в регистр, сравнить его с 0, если он сравнивает меньше 0, затем выполнить p2->m = -p2->m; и, наконец, вернуть значение регистра без изменений!

Объединение здесь - единственный способ сделать p1 == p2 на двоичном уровне, поскольку все члены объединения имеют одинаковый адрес.

Другой пример:

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;
    u.s1.m = -1;
    return f(&u.s1, &u.s2);
}

Что необходимоg вернуть?+1 в соответствии со здравым смыслом (мы меняем -1 на +1 в f).Но если мы посмотрим на сгенерированную сборку gcc с -O1 оптимизацией

f:
        cmp     DWORD PTR [rdi], 0
        js      .L3
.L2:
        mov     eax, DWORD PTR [rdi]
        ret
.L3:
        neg     DWORD PTR [rsi]
        jmp     .L2
g:
        mov     eax, 1
        ret

Пока все так же исключено.Но когда мы пробуем это с -O2

f:
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        js      .L4
        ret
.L4:
        neg     DWORD PTR [rsi]
        ret
g:
        mov     eax, -1
        ret

Возвращаемое значение теперь жестко закодировано -1

Это потому, что f в начале кэширует значение p1->mв регистре eax (mov eax, DWORD PTR [rdi]) и не перечитывает его после p2->m = -p2->m; (neg DWORD PTR [rsi]) - возвращает eax без изменений.


union здесьиспользуется только для Все нестатические члены-данные объекта объединения имеют одинаковый адрес. как результат &u.s1 == &u.s2.

- кто-то, кто не понимает ассемблерный код, может показать в c / c ++ , как строгие псевдонимы влияют на код f:

int f(struct t1* p1, struct t2* p2)
{
    int a = p1->m;
    if (a < 0) p2->m = -p2->m;
    return a; 
}

кэш компилятора * значение 1055 * в локальном vara (на самом деле в реестре, конечно) и вернуть его, несмотря на p2->m = -p2->m; изменение p1->m.но компилятор предполагает, что p1 память не затронута, потому что он предполагает, что p2 указывает на другую память, которая не перекрывается с p1

, поэтому при разных компиляторах и разном уровне оптимизации один и тот же исходный код может возвращать разныезначения (-1 или +1).так и неопределенное поведение как есть

0 голосов
/ 27 сентября 2018

Одна из основных целей правила Common Initial Sequence - позволить функциям работать на многих похожих структурах взаимозаменяемо.Требование к тому, чтобы компиляторы предполагали, что любая функция, которая действует на структуру, может изменить соответствующий член в любой другой структуре, которая разделяет общую начальную последовательность, однако, ухудшило бы полезные оптимизации.

Хотя большая часть кода, который полагается на CommonВ гарантиях «Первоначальная последовательность» используются несколько легко распознаваемых шаблонов, например,

struct genericFoo {int size; short mode; };
struct fancyFoo {int size; short mode, biz, boz, baz; };
struct bigFoo {int size; short mode; char payload[5000]; };

union anyKindOfFoo {struct genericFoo genericFoo;
  struct fancyFoo fancyFoo;
  struct bigFoo bigFoo;};

...
if (readSharedMemberOfGenericFoo( myUnion->genericFoo ))
  accessThingAsFancyFoo( myUnion->fancyFoo );
return readSharedMemberOfGenericFoo( myUnion->genericFoo );

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

Авторы gcc и clang считали, что трактовка объединенных объявлений как признака того, что задействованные типы могут быть задействованы в конструкциях, подобных описанным выше, будет непрактичным препятствием для оптимизации, и решила, чтопоскольку Стандарт не требует от них поддержки таких конструкций другими средствами, они просто не будут их поддерживать вообще.Следовательно, реальное требование к коду, который должен был бы каким-либо осмысленным образом использовать гарантии начальной начальной последовательности, заключается не в том, чтобы гарантировать, что объявление типа объединения является видимым, а в том, чтобы clang и gcc вызывались с флагом -fno-strict-aliasing.Также включает в себя видимое объявление объединения, когда это практически не повредит, но это не является ни необходимым, ни достаточным для обеспечения правильного поведения gcc и clang.

0 голосов
/ 26 сентября 2018

В примере делается попытка заранее проиллюстрировать абзац 1 (выделено мной):

6.5.2.3 ¶6

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

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

В примере не показано, как uинициализируется, но при условии, что последняя запись в член равна u.s2.m, функция имеет неопределенное поведение, потому что она проверяет p1->m без применения общей гарантии начальной последовательности.

То же самое происходит и в другом случае, еслиu.s1.m, который последний раз записывался перед вызовом функции, тогда доступ к p2->m является неопределенным поведением.

Обратите внимание, что сам f недопустим.Это совершенно разумное определение функции.Неопределенное поведение проистекает из передачи в него &u.s1 и &u.s2 в качестве аргументов.Вот что вызывает неопределенное поведение.


1 - Я цитирую n1570 , стандартную черновик C11.Но спецификация должна быть такой же, только при перемещении параграфа вверх или вниз.

...