Является ли законным реализовать наследование в C посредством указателей каста между одной структурой, которая является подмножеством другого, а не первого члена?

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

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

Является ли этот код законным?

 #include  #include  struct base { double some; char space_for_subclasses[]; }; struct derived { double some; int value; }; int main(void) { struct base *b = malloc(sizeof(struct derived)); b->some = 123.456; struct derived *d = (struct derived*)(b); d->value = 4; struct base *bb = (struct base*)(d); printf("%f\t%f\t%d\n", d->some, bb->some, d->value); return 0; } 

Этот код, как представляется, дает желаемые результаты , но, как мы знаем, это далеко не доказывает, что это не UB.

Причина, по которой я подозреваю, что такой код может быть законным, заключается в том, что я не вижу никаких проблем с выравниванием, которые могут возникнуть здесь. Но, конечно, это далеко не означает, что такие проблемы не возникают, и даже если на самом деле нет проблем с выравниванием, код по-прежнему может быть UB по любой другой причине.

  • Является ли приведенный выше код действительным?
  • Если это не так, есть ли способ сделать его действительным?
  • Is char space_for_subclasses[]; необходимо? Удалив эту строку, код все еще, кажется, ведет себя

Это более или менее одно и то же наследование бедных людей, используемое struct sockaddr , и оно не является надежным с нынешним поколением компиляторов. Самый простой способ продемонстрировать проблему:

 #include  #include  #include  struct base { double some; char space_for_subclasses[]; }; struct derived { double some; int value; }; double test(struct base *a, struct derived *b) { a->some = 1.0; b->some = 2.0; return a->some; } int main(void) { void *block = malloc(sizeof(struct derived)); if (!block) { perror("malloc"); return 1; } double x = test(block, block); printf("x=%g some=%g\n", x, *(double *)block); return 0; } 

Если a->some и b->some были разрешены буквой стандарта как один и тот же объект, эта программа должна была бы печатать x=2.0 some=2.0 , но с некоторыми компиляторами и при некоторых условиях (она выиграла ‘ t на всех уровнях оптимизации, и вам, возможно, придется перенести test на свой собственный файл), вместо этого он будет печатать x=1.0 some=2.0 .

Означает ли буква стандарта, что- a->some а a->some b->some являются одним и тем же объектом. См. http://blog.regehr.org/archives/1466 и документ, на который он ссылается.

Когда я прочитал стандарт, главу §6.2.6.1 / P5,

Определенные представления объектов не должны представлять значение типа объекта. Если хранимое значение объекта имеет такое представление и считывается выражением lvalue, которое не имеет типа символа, поведение не определено. […]

Итак, пока space_for_subclasses является членом char ( array-decays-to-pointer ), и вы используете его для чтения значения, вы должны быть в порядке.


Тем не менее, чтобы ответить

Is char space_for_subclasses[]; необходимо?

Да, это так.

Цитируя §6.7.2.1 / P18,

В качестве особого случая последний элемент структуры с более чем одним именованным элементом может иметь неполный тип массива; это называется гибким элементом массива . В большинстве ситуаций гибкий элемент массива игнорируется. В частности, размер структуры выглядит так, как если бы гибкий элемент массива был исключен, за исключением того, что он мог иметь более длинное дополнение, чем подразумевалось бы упущение. Однако, когда a . (или -> ) имеет левый операнд, который является (указателем на) структуру с гибким членом массива, а правый операнд – этим членом, он ведет себя так, как если бы этот элемент был заменен самым длинным массивом (с тем же типом элемента ), который не сделает структуру больше, чем объект, к которому обращаются; смещение массива должно оставаться равным элементу гибкого элемента массива, даже если это будет отличаться от размера массива замены. Если этот массив не будет содержать никаких элементов, он будет вести себя так, как если бы у него был один элемент, но поведение не определено, если была предпринята попытка доступа к этому элементу или для создания указателя, который прошел мимо него.

Удалите это, и вы получите доступ к недопустимой памяти, что приведет к неопределенному поведению . Однако в вашем случае (второй fragment) вы все равно не получаете доступ к value , поэтому здесь не будет никакой проблемы.