Как «структурное наследование» не нарушает правила строгого алиасинга? - PullRequest
3 голосов
/ 14 апреля 2020

«Техника наследования структуры» в C (, как описано в этом вопросе ) стала возможной благодаря тому, что стандарт C гарантирует, что первый член структуры никогда не будет иметь никаких заполнение перед ним (?), и что адрес первого члена всегда будет равен адресу самой структуры.

Это позволяет использовать, например, следующее:

typedef struct {
    // some fields
} A;

typedef struct {
    A base;
    // more fields
} B;

typedef struct {
    B base;
    // yet more fields
} C;

C* c = malloc(sizeof(C));
// ... init c or whatever ...
A* a = (A*) c;
// ... access stuff on a etc.
B* b = (B*) c;
// ... access stuff on b etc.

Этот вопрос состоит из двух частей:

A. Мне кажется, что эта техника нарушает строгое правило наложения имен. Я не прав, и если да, то почему?

B. Предположим, что эта техника действительно законна. В этом случае, имеет ли значение, если A: мы сначала сохраняем объект в lvalue его специфицированного типа c, перед тем как приводить его вниз или вверх к другому типу, или B : если мы приведем его непосредственно к конкретному типу, требуемому в данный момент, без предварительного сохранения его в lvalue указанного типа c?

Например, все ли эти три варианта одинаково допустимы?

Опция 1:

C* make_c(void) {
    return malloc(sizeof(C));
}    

int main(void) {
    C* c = make_c(); // First store in a lvalue of the specific type
    A* a = (A*) c;
    // ... do stuff with a
    C* c2 = (C*) a; // Cast back to C
    // ... do stuff with c2

    return 0;
}

Опция 2:

C* make_c(void) {
    return malloc(sizeof(C));
}    

int main(void) {
    A* a = (A*) make_c(); // Don't store in an lvalue of the specific type, cast right away
    // ... do stuff with a
    C* c2 = (C*) a; // Cast back to C
    // ... do stuff with c2

    return 0;
}

Опция 3:

int main(void) {
    A* a = (A*) malloc(sizeof(C)); // Don't store in an lvalue of the specific type, cast right away
    // ... do stuff with a
    C* c2 = (C*) a; // Cast to C - even though the object was never actually stored in a C* lvalue
    // ... do stuff with c2

    return 0;
}

Ответы [ 3 ]

1 голос
/ 18 апреля 2020

A. Мне кажется, что эта техника нарушает строгое правило наложения имен. Я не прав, и если да, то почему?

Да, вы не правы. Я рассмотрю два случая:

Случай 1: C полностью инициализирован

Это может быть так, например:

C *c = malloc(sizeof(*c));
*c = (C){0};  // or equivalently, "*c = (C){{{0}}}" to satisfy overzealous compilers

В этом случае все байты представления C установлены, а эффективный тип объекта, содержащего эти байты, равен C. Это происходит из пункта 6.5 / 6 стандарта:

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

Но типы структур и массивов агрегатные типы , что означает, что объекты таких типов содержат другие объекты внутри них. В частности, каждый C содержит B, идентифицированный как его член base. Поскольку выделенный объект в этот момент фактически является C, он содержит подобъект, который фактически является B. Один синтаксис для lvalue, ссылающийся на это B, равен c->base. Тип этого выражения B, поэтому оно соответствует правилу строгого псевдонима, чтобы использовать его для доступа к B, к которому оно относится. Это должно быть хорошо, иначе структуры (и массивы) не будут работать вообще, независимо от того, динамически или нет. *

Но, как обсуждалось в мой ответ на ваш предыдущий вопрос , (B *)c гарантированно равен (по значению и type) &c->base. Таким образом, *(B *)c - это другое lvalue, относящееся к B, который является первым членом *c. То, что синтаксис этого выражения отличается от синтаксиса предыдущего lvalue, который мы рассматривали, не имеет значения. Это lvalue типа B, связанное с объектом типа B, поэтому использование его для доступа к объекту, на который он ссылается, является одним из случаев, разрешенных SAR.

Ничего из этого отличается от статически и автоматически распределенных случаев.

Случай 2: C не полностью инициализирован

Это может быть что-то вроде этого:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};

Таким образом, мы присвоили начальную B размерную часть выделенного объекта через lvalue типа B, поэтому эффективный тип этой начальной части - B. Выделенное пространство на данный момент не содержит объект (эффективный) типа C. Мы можем получить доступ к B и его членам, для чтения или записи, через любые допустимые значения l, ссылающиеся на них, как обсуждалось выше. Но мы имеем строгое нарушение псевдонимов, если мы

  • пытаемся прочитать *c в целом ( например, C c2 = *c;);
  • пытаемся прочитать C членов, кроме base ( например X x = c->another;); или
  • попытка чтения выделенного объекта через lvalue большинства несвязанных типов ( например, Unrelated_but_not_char u = *(Unrelated_but_not_char *) c;

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

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

И это подводит нас к главному хитрому биту . Что если мы сделаем это:

C *c = malloc(sizeof(*c));
c->base = (B){0};

? Или это:

C *c = malloc(sizeof(*c));
c->another = 0;

Выделенный объект не имеет никакого действующего типа до первой записи в него (и, в частности, он не имеет действительного типа C), поэтому имеют ли смысл выражения для записи в элемент через *c? Они четко определены? Буква стандарта может поддерживать аргумент, который они не делают, но ни одна реализация не принимает такую ​​интерпретацию, и нет никаких оснований думать, что кто-либо когда-либо будет.

Интерпретация, наиболее совместимая с буквой стандарта и универсальная практика заключается в том, что запись через lvalue для доступа к элементу представляет собой одновременную запись для участника и его агрегата хоста, таким образом устанавливая эффективный тип всего региона, даже если записано только одно значение элемента. Конечно, это все еще не позволяет читать элементы, чьи значения не были записаны - потому что их значения неопределенны, а не из-за SAR.

Это оставляет этот случай:

C *c = malloc(sizeof(*c));
*(B *)c = (B){0};
B b2 = c->base;            // What about this?

То есть, если эффективный тип начальной области выделенного пространства равен B, можем ли мы использовать lvalue для доступа к элементу на основе типа C для чтения сохраненного значения этой области B? Опять же, можно утверждать, что нет, на основании того, что нет фактического C, но на практике никакая реализация не делает такую ​​интерпретацию. Эффективный тип читаемого объекта - начальная область выделенного пространства - такой же, как тип lvalue, используемого для доступа, поэтому в этом смысле нарушения SAR нет. То, что хост C является полностью гипотетическим, является вопросом, главным образом, синтаксиса , а не семантики, потому что та же самая область может определенно считываться как объект того же типа через альтернативное выражение.


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

1 голос
/ 18 апреля 2020

Я считаю, что эта цитата из C11 (ISO / IEC 9899: 2011 §6.5 7) должна ответить на некоторые ваши вопросы (мой акцент добавлен):

Объект должен иметь его сохраненное значение доступно только через выражение lvalue, имеющее один из следующих типов:
- тип, совместимый с действительным типом объекта,
- квалифицированная версия типа, совместимого с действительным типом объекта ,
- тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,
- тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,
- агрегированный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения) или
- символьный тип.

Тогда можно ответить еще на это (ISO / IEC 9899: 2011 §6.7.2.1 15):

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

Остальным можно ответить с помощью этого фрагмента (ISO / IEC 9899: 2011 §7.22.3 1 ):

Порядок и непрерывность памяти, выделяемой при последовательных вызовах функций aligned_alloc, calloc, malloc и realloc, не определены. Указатель, возвращаемый в случае успешного выделения, выравнивается соответствующим образом, чтобы его можно было присвоить указателю на любой тип объекта с фундаментальным требованием выравнивания, а затем использовать для доступа к такому объекту или массиву таких объектов в выделенном пространстве (до тех пор, пока пространство явно освобождено).

В заключение:

A. Ты не прав. См. Первую и вторую цитаты для обоснования.

B. Нет, это не имеет значения. См. Третью цитату (и, возможно, первую) для обоснования.

0 голосов
/ 18 апреля 2020

Да, первый элемент структуры не имеет никаких отступов перед ним.

Во-вторых, когда типом анонимного поля является typedef для структуры или объединения, код может ссылаться на поле, используя имя typedef. Это хорошая практика, взятая из руководств G CC:

typedef struct {
    // some fields
} A;

typedef struct {
    A;
    // more fields
} B;

typedef struct {
    B;
    // yet more fields
} C;

B get_B (struct C *c) { return c->B; } /* access B */ 

Пожалуйста, отметьте Безымянная структура и поля объединения

...