Вложенные структуры и строгие псевдонимы в 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? Пожалуйста, объясните, почему это законно/незаконно.


person Johannes    schedule 07.12.2011    source источник
comment
@outis: что именно вы подразумеваете под классом?   -  person Johannes    schedule 07.12.2011
comment
@user1085684 user1085684 Это не домашнее задание и не вопрос для собеседования, не так ли?   -  person Sergey Kalinichenko    schedule 07.12.2011
comment
@dasblinkenlight Нет, я пишу виртуальную машину, и мне нужно определить тип объекта во время выполнения. Вот почему мне нужна какая-то информация в каждом объекте среды выполнения, к которому я могу получить доступ, не зная его типа.   -  person Johannes    schedule 07.12.2011
comment
Первый член каждой структуры должен быть int type, чтобы проверить тип, а затем правильно привести его.   -  person Joe    schedule 07.12.2011
comment
почему это вообще должно работать? object_t не имеет ничего общего с person_t?   -  person duedl0r    schedule 07.12.2011
comment
Хотя я согласен с другими в том, что вопрос дает плохой пример с людьми и возрастом, я не вижу ничего плохого в принципе, стоящем за ним. На самом деле многие структуры старой операционной системы Amiga использовали эту схему.   -  person Some programmer dude    schedule 07.12.2011
comment
@Joe, это нарушит строгое правило псевдонимов   -  person Johannes    schedule 07.12.2011
comment
@duedl0r Это довольно распространенный трюк: вы можете привести указатель структуры к указателю на его первый элемент, потому что расположение памяти гарантированно будет таким же (поэтому вы можете рассматривать person_t* как object_t*, поскольку его первый член — это object_t ). например вся иерархия объектов инструментария GTK построена вокруг этой концепции.   -  person nos    schedule 07.12.2011
comment
@duedl0r, но person_t как-то связан с object_t   -  person Johannes    schedule 07.12.2011
comment
@nos да, я знаю, но нарушает ли это строгое правило псевдонима?   -  person Johannes    schedule 07.12.2011
comment
Что ж, надеюсь, кто-нибудь сможет ответить на этот вопрос вместо того, чтобы указывать, как можно переписать код :-)   -  person nos    schedule 07.12.2011
comment
@nos - проблема с примером ОП заключается в том, что он не рассматривает person_t* как object_t*, а наоборот. И этот другой путь небезопасен даже с проверкой на type, потому что его можно обмануть.   -  person mouviciel    schedule 07.12.2011
comment
@mouviciel: проблема не в том, что его можно обмануть; проблема в том, что это может нарушать строгие правила псевдонимов.   -  person Oliver Charlesworth    schedule 07.12.2011
comment
@mouviciel, но вопрос не в этом - речь идет о нарушении правила псевдонимов   -  person Johannes    schedule 07.12.2011
comment
@OliCharlesworth - эти две проблемы связаны.   -  person mouviciel    schedule 07.12.2011
comment
@mouviciel как именно они связаны?   -  person Johannes    schedule 07.12.2011
comment
person_t включает object_t: использование псевдонимов разрешено. object_t не включает person_t: псевдонимы не разрешены.   -  person mouviciel    schedule 07.12.2011
comment
@mouviciel В таком случае это может быть ответом. Наличие поля типа, используемого для различения, является распространенным способом реализации вашей собственной системы типов/объектов. Такая система, конечно, полагается на правильность информации, она ломается, если, например. object-›type неверен, точно так же, как strlen() ломается (дает вам неопределенное поведение), если вы передаете ему указатель NULL или что-то, что не является строкой.   -  person nos    schedule 08.12.2011


Ответы (4)


Строгие правила псевдонимов — это два указателя разных типов, ссылающихся на одно и то же место в памяти (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. Указатель на объект структуры, соответствующим образом преобразованный, указывает на его начальный элемент

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

person Sergey Kalinichenko    schedule 07.12.2011
comment
@Johannes Мне тоже понравился вопрос, потому что я получил возможность узнать что-то новое в процессе ответа на него. В частности, вторая часть показалась мне немного нелогичной: я не знал, что C запрещает компиляторам заполнять поля перед начальным членом структуры. В любом случае, если вы считаете, что сообщение отвечает на ваш вопрос, было бы неплохо принять ответ: ваш принятый %% будет расти, и вы получите значок в процессе. Удачи в вашем проекте VM! - person Sergey Kalinichenko; 07.12.2011
comment
что, если он разыменует ячейку памяти в пределах границы object_t? (person_t *)object-›object-›type + (person_t *)object-›age подойдет? (код не имеет никакого смысла, написан только для демонстрации доступа к памяти в пределах границы object_t) - person Hayri Uğur Koltuk; 13.03.2012
comment
Подходящее преобразование — едва ли не наихудшая формулировка, которую могли бы выбрать здесь авторы стандарта — означает ли это, что указатель должен быть преобразован в void*, или что мы действительно можем использовать другой тип указателя struct? - person Fred Foo; 17.01.2014
comment
@larsmans Я почти уверен, что под соответствующим преобразованием они подразумевают преобразование в тип исходного члена внешнего struct. - person Sergey Kalinichenko; 17.01.2014

http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

В качестве дополнения к принятому ответу, вот полная цитата из стандарта, в которой выделена важная часть, в которой пропущен другой ответ, и еще один:

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

6.3.2.3-7: Указатель на объект или неполный тип может быть преобразован в указатель на другой объект или неполный тип. Если результирующий указатель неправильно выровнен для указанного типа, поведение не определено. В противном случае при обратном преобразовании результат будет равен исходному указателю. [...]

Я считаю ваш пример идеальным местом для указателя void:

int age(void *object) {

Почему? Потому что ваше очевидное намерение состоит в том, чтобы дать такой функции разные типы объектов, и она получает информацию в соответствии с закодированным типом. В вашей версии вам нужно приводить каждый раз, когда вы вызываете функцию: age((object_t*)person);. Компилятор не будет жаловаться, если вы дадите ему неправильный указатель, так что в любом случае безопасность типов не задействована. Затем вы также можете использовать указатель void и избегать приведения при вызове функции.

В качестве альтернативы вы можете, конечно, вызвать функцию с помощью age(&person->object). Каждый раз, когда вы звоните.

person Secure    schedule 07.12.2011

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

В пределах age вам нужно рассмотреть object. ((person_t *)object) — это выражение lvalue, потому что оно имеет объектный тип и обозначает объект (область памяти). Однако ветвь достигается только в том случае, если object->type == PERSON, поэтому (предположительно) эффективным типом объекта является person_t*, поэтому приведение не нарушает строгое сглаживание. В частности, строгое сглаживание позволяет:

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

При вызове age() вы, вероятно, будете передавать object_t* или тип, производный от object_t: структуру, в которой object_t является первым членом. Это разрешено как:

  • тип агрегата или объединения, который включает в себя один из вышеупомянутых типов среди своих членов

Кроме того, смысл строгого сглаживания заключается в том, чтобы оптимизировать загрузку значений в регистры. Если объект изменяется с помощью одного указателя, предполагается, что все, на что указывают указатели несовместимого типа, остается неизменным и, следовательно, не нуждается в перезагрузке. Код ничего не изменяет, поэтому оптимизация на него не должна влиять.

person outis    schedule 07.12.2011
comment
Хотя этот код ничего не изменяет, мы не знаем, какой код возникает после возврата age. Если следующая строка изменяет память, может ли компилятор переупорядочить вещи так, чтобы изменение произошло до вызова age? - person Oliver Charlesworth; 07.12.2011
comment
Это ни на что не повлияет, пока вызов age сохраняется как вызов функции, а не встроенный, поскольку внутри age по-прежнему не будет никаких изменений. Более того, если age встроено, то отдельной переменной object не будет, поэтому алиасинг, созданный вызовом функции, исчезнет. - person outis; 08.12.2011

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

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, а затем получить доступ к нужному члену профсоюза.

person Kerrek SB    schedule 07.12.2011
comment
это сделало бы все объекты такими же большими, как самый большой член object_t, и я хочу избежать этого, если это возможно - person Johannes; 07.12.2011
comment
@Йоханнес: Верно. Я не уверен, но мне кажется, что те же самые правила позволят вам проверить начальный элемент любой структуры из набора структур, которые имеют один и тот же начальный элемент. У меня нет стандартной ссылки на это. Хотя, казалось бы, это следует из требования профсоюза. - person Kerrek SB; 07.12.2011
comment
Согласно cellperformance.beyond3d.com/articles/2006/06/ приведение типов через объединение также строго не определено. - person Oliver Charlesworth; 07.12.2011
comment
@OliCharlesworth: Однако мы не проводим кастинг, мы просто проверяем начальный элемент. См. C99/6.5.2.3/5. - person Kerrek SB; 07.12.2011
comment
@KerrekSB: у авторов gcc есть собственное представление о том, что должно означать правило Common Initial Sequence. Они интерпретируют его таким образом, что оно в принципе бесполезно, а затем заявляют, что было бы глупо, если бы компилятор поддерживал такое бесполезное правило, поэтому они этого не делают. - person supercat; 23.02.2017