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