Законно ли реализовать наследование в C, приводя указатели между одной структурой, которая является подмножеством другой, а не первого члена?
Теперь я знаю, что могу реализовать наследование, приведя указатель к struct
к типу первого члена этого struct
,
Однако, как опыт обучения, я начал задаваться вопросом, возможно ли реализовать наследование немного по-другому.
Законен ли этот код?
#include <stdio.h>
#include <stdlib.h>
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 по любой другой причине.
- Действителен ли приведенный выше код?
- Если это не так, есть ли способ сделать его действительным?
- Является
char space_for_subclasses[];
необходимо? После удаления этой строки код все еще ведет себя
2 ответа
Это более-менее то же самое наследство бедняков, которым пользуются struct sockaddr
и это не надежно с нынешним поколением компиляторов. Самый простой способ продемонстрировать проблему так:
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
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
, но с некоторыми компиляторами и при некоторых условиях (это не произойдет на всех уровнях оптимизации, и вам, возможно, придется двигаться test
в свой собственный файл) он напечатает x=1.0 some=2.0
вместо.
Позволяет ли буква стандарта a->some
а также b->some
быть тем же объектом оспаривается. См. http://blog.regehr.org/archives/1466 и статью, на которую он ссылается.
Когда я читаю стандарт, глава §6.2.6.1/P5,
Определенные представления объекта не должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, которое не имеет символьного типа, поведение не определено. [...]
Итак, пока space_for_subclasses
это char
(array-decays-to-pointer) член и вы используете его для чтения значения, вы должны быть в порядке.
Что сказал, чтобы ответить
Является
char space_for_subclasses[];
необходимо?
Да, это так.
Цитируя §6.7.2.1/P18,
В особом случае последний элемент структуры с более чем одним именованным элементом может иметь тип неполного массива; это называется членом гибкого массива. В большинстве случаев член гибкого массива игнорируется. В частности, размер структуры такой, как если бы элемент гибкого массива был опущен, за исключением того, что он может иметь больше заполняющего отступа, чем это может означать упущение. Тем не менее, когда
.
(или же->
) оператор имеет левый операнд, который является (указателем) на структуру с элементом гибкого массива и правый операнд называет этот член, он ведет себя так, как если бы этот элемент был заменен самым длинным массивом (с тем же типом элемента), который не будет сделать структуру больше, чем объект, к которому осуществляется доступ; смещение массива должно оставаться смещением элемента гибкого массива, даже если оно будет отличаться от смещения массива замены. Если в этом массиве нет элементов, он ведет себя так, как если бы у него был один элемент, но поведение не определено, если была предпринята какая-либо попытка получить доступ к этому элементу или сгенерировать указатель один за ним.
Удалите это, и вы получите доступ к неверной памяти, вызывая неопределенное поведение. Тем не менее, в вашем случае (второй фрагмент), вы не получаете доступ value
во всяком случае, так что это не будет проблемой здесь.