Вложенные структуры и строгий псевдоним в c
Обратите внимание на следующий код:
typedef struct {
int type;
} object_t;
typedef struct {
object_t object;
int age;
} person_t;
int age(object_t *object) {
if (object->type == PERSON) {
return ((person_t *)object)->age;
} else {
return 0;
}
}
Является ли этот юридический код или он нарушает правило сглаживания C99? Пожалуйста, объясните, почему это законно/незаконно.
Ответы
Ответ 1
Строгое правило сглаживания - это два указателя разных типов, ссылающихся на одно и то же место в памяти (ISO/IEC9899/TC2). Хотя ваш пример ретранслирует адрес object_t object
как адрес person_t
, он не ссылается на ячейку памяти внутри object_t
через реинтерпретированный указатель, потому что age
находится за границей object_t
. Поскольку ячейки памяти, на которые ссылаются указатели, не совпадают, я бы сказал, что это не нарушает строгое правило псевдонимов. FWIW, gcc -fstrict-aliasing -Wstrict-aliasing=2 -O3 -std=c99
, похоже, согласен с этой оценкой и не выдает предупреждения.
Этого недостаточно, чтобы решить, что это юридический код: ваш пример делает предположение, что адрес вложенной структуры такой же, как адрес его внешней структуры. Кстати, это безопасное предположение сделать согласно стандарту C99:
6.7.2.1-13. Указатель на объект структуры, соответствующим образом преобразованный, указывает на его начальный член
Два вышеупомянутых соображения заставляют меня думать, что ваш код является законным.
Ответ 2
Строгое правило псевдонимов ограничивает то, к каким типам вы обращаетесь к объекту (область памяти). В коде, где может возникнуть правило, есть несколько мест: внутри age()
и при вызове age()
.
Внутри age
вы должны рассмотреть object
. ((person_t *)object)
- выражение lvalue, поскольку оно имеет тип объекта и обозначает объект (область памяти). Однако ветвь достигается только, если object->type == PERSON
, поэтому (предположительно) эффективный тип объекта является person_t*
, поэтому приведение не нарушает строгого сглаживания. В частности, строгий псевдоним позволяет:
- тип, совместимый с эффективным типом объекта,
При вызове age()
вы предположительно будете передавать object_t*
или тип, который спускается из object_t
: структура, которая имеет object_t
в качестве первого члена. Это разрешено как:
- тип агрегата или объединения, который включает один из вышеупомянутых типов среди его членов.
Кроме того, точка строгого сглаживания заключается в том, чтобы оптимизировать значения загрузки в регистры. Если объект мутирован с помощью одного указателя, предполагается, что все, что указано указателями несовместимого типа, остается неизменным и, следовательно, его не нужно перезагружать. Код ничего не меняет, поэтому оптимизация не должна влиять.
Ответ 3
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
В качестве дополнения к принятому ответу, вот полная цитата из стандарта, с важной частью выделено, что другой ответ опущен, и еще один:
6.7.2.1-13: Внутри объекта структуры члены небитового поля и единицы в в которых расположены битовые поля, адреса, которые увеличиваются в порядке которые они объявлены. Указатель на объект структуры, подходящим образом преобразуется, указывает на его начальный член (или если этот член является бит-поле, затем в блок, в котором он находится), и наоборот. В объекте структуры может быть неназванное заполнение, но не на его начало.
6.3.2.3-7: Указатель на объект или неполный тип может быть преобразован в указатель на другой объект или неполный тип. Если полученный указатель неправильно выровнен для указанного типа, поведение undefined. В противном случае при обратном результат сравнивается с исходным указателем. [...]
Я считаю ваш пример идеальным местом для указателя пустоты:
int age(void *object) {
Почему? Потому что ваше очевидное намерение состоит в том, чтобы предоставить разные "объекты" типа для такой функции и получать информацию в соответствии с закодированным типом. В вашей версии вам требуется приведение при каждом вызове функции: age((object_t*)person);
. Компилятор не будет жаловаться, когда вы указываете на него неправильный указатель, так что во всяком случае нет никакой безопасности типа. Затем вы можете также использовать указатель void и избегать приведения при вызове функции.
В качестве альтернативы вы могли бы вызвать функцию с помощью age(&person->object)
, конечно. Каждый раз, когда вы его вызываете.
Ответ 4
Одним из приемлемых способов, который явно допускается стандартом, является создание объединения структур с одинаковым начальным сегментом, например:
struct tag { int value; };
struct obj1 { int tag; Foo x; Bar y; };
struct obj2 { int tag; Zoo z; Car w; };
typedef union object_
{
struct tag;
struct obj1;
struct obj2;
} object_t;
Теперь вы можете пройти object_t * p
и протестировать p->tag.value
безнаказанно, а затем получить доступ к желаемому члену объединения.