Союзы и строгое сглаживание в C11

Предполагая, что у меня есть такой союз

union buffer {
  struct { T* data; int count; int capacity; };
  struct { void* data; int count; int  capacity; } __type_erased;
};

Не возникнут ли у меня проблемы, если я смешаю чтение/запись с анонимными членами структуры и членами __type_erased в соответствии с правилами псевдонимов C11?

В частности, меня интересует поведение, которое происходит, если доступ к компонентам осуществляется независимо (например, через разные указатели). Проиллюстрировать:

grow_buffer(&buffer.__type_erased);
buffer.data[buffer.count] = ...

Я прочитал все соответствующие вопросы, которые смог найти, но я до сих пор не на 100% разобрался в этом, поскольку некоторые люди, кажется, предполагают, что такое поведение не определено, в то время как другие говорят, что оно законно. Кроме того, информация, которую я нахожу, представляет собой смесь правил C++, C99, C11 и т. д., которые довольно трудно переварить. Здесь меня явно интересует поведение, предписанное C11 и демонстрируемое популярными компиляторами (Clang, GCC).

Изменить: больше информации

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

#define ALLOC_ONE(A)\
    (_array_ensure_size(&A.__type_erased, A.count+1), A.count++)

который при необходимости увеличивает массив и возвращает индекс вновь выделенного элемента. В спецификации (6.5.2.3) указано, что доступ к одному и тому же местоположению через разных членов союза разрешен. Моя интерпретация этого заключается в том, что, хотя _array_ensure_size() не знает о типе объединения, компилятор должен знать, что член __type_erased потенциально может быть изменен побочным эффектом. То есть я бы предположил, что это должно работать. Однако кажется, что это серая зона (и, честно говоря, в спецификации действительно не ясно, что представляет собой доступ к членам). Последний Clang от Apple (clang-800.0.33.1) не имеет с ним проблем. Код компилируется без предупреждений и работает как положено. Однако при компиляции с помощью GCC 5.3.0 код вылетает с ошибкой сегментации. На самом деле, у меня есть сильное подозрение, что поведение GCC является ошибкой — я попытался сделать мутацию члена объединения явной, удалив изменяемый указатель ref и приняв четкий функциональный стиль, например:

#define ALLOC_ONE(A) \
   (A.__type_erased = _array_ensure_size(A.__type_erased, A.count+1),\
    A.count++)

Это снова работает с Clang, как и ожидалось, но снова приводит к сбою GCC. Мой вывод состоит в том, что продвинутая манипуляция типами с объединениями — это серая зона, в которой следует действовать осторожно.


person MrMobster    schedule 19.07.2016    source источник
comment
Для очень любопытных, мне удалось решить проблему, используя только макросы и полностью отказавшись от союзов. Вместо того, чтобы использовать функцию для увеличения массива, я просто использую сложный макрос, который использует выражение с запятой для realloc() данных массива, если емкость превышает пороговое значение. Это работает как с GCC, так и с Clang, а также невероятно быстро.   -  person MrMobster    schedule 20.07.2016


Ответы (1)


Стандарт C11 говорит следующее:

6.5.2.3 Структура и члены союза

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

Так что с точки зрения чтения/записи поля объединения в C11 это правильно. Но strict-aliasing — это анализ, основанный на типах, поэтому его наивная реализация может сказать, что эти операции чтения/записи независимы. Насколько я понимаю, современный gcc может обнаруживать случаи с полями объединения и избегать таких ошибок.

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

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

На мой взгляд, использование союзов для чтения/записи в разных членах опасно и лучше его избегать.

person alexanius    schedule 19.07.2016
comment
Неважно, виден ли тип объединения в f, важно, осуществляется ли доступ через lvalue типа объединения. то есть перемещение определения союза ранее в файле ничего не изменит. - person Jonathan Wakely; 19.07.2016
comment
@JonathanWakely да, ты прав. Но это цитата из стандарта. С точки зрения компилятора при создании sa внутри f вы действительно не видите, что аргументы в call point являются членами одного union - person alexanius; 19.07.2016
comment
Ах да, извините, я не понял, что эта часть была процитирована прямо из стандарта. Вы правы, поскольку он не может видеть тип, он определенно не может осуществлять доступ через этот тип! - person Jonathan Wakely; 19.07.2016
comment
Интересно! Я ясно вижу, как g() нарушает условие алиасинга, но мой случай немного отличается. Это правда, что я использую указатель на член объединения и передаю его функции, которая не видит тип объединения, однако я не отправляю псевдонимы указателей на ту же функцию, и компилятор должен знать, что объединение может быть изменено с помощью этого указателя. Я отредактировал свой вопрос, чтобы добавить некоторые результаты экспериментов с Clang и GCC 5, которые действительно показывают здесь очень разное поведение. - person MrMobster; 20.07.2016
comment
@JonathanWakely: авторам gcc это может не нравиться, но формулировка видимости типа объединения complete послужила полезной цели, если компилятор соблюдает Стандарт, в то время как интерпретация gcc делает его бессмысленным. Если компилятор подчиняется Стандарту, как написано, но включает #pragma, которая сообщает компилятору, что ему не нужно соблюдать правило Common Initial Sequence для членов определенного объединения, тогда написанное правило не сделает никакие оптимизации невозможными. Если компилятор игнорирует правило, которое требует, чтобы он распознавал псевдонимы при отсутствии противоположных директив... - person supercat; 21.07.2016
comment
... нет никаких других даже отдаленно разумных средств, определенных в Стандарте, с помощью которых программист может заставить компилятор применять правило Common Initial Sequence к указателям похожих, но разных структур. - person supercat; 21.07.2016