Все ли указатели, полученные из указателей на типы структур, одинаковы?

#c #pointers #struct #c99 #strict-aliasing

#c #указатели #структура #c99 #строгое сглаживание

Вопрос:

Вопрос

На вопрос о том, все ли указатели, полученные из указателей на типы структур, одинаковы, нелегко ответить. Я считаю, что это важный вопрос по следующим двум основным причинам.

A. Отсутствие указателя на указатель на «любой» неполный или объектный тип, накладывает ограничение на удобные функциональные интерфейсы, такие как:

 int allocate(ANY_TYPE  **p,
             size_t    s);

int main(void)
{
    int *p;
    int r = allocate(amp;p, sizeof *p);
}
 

[Полный пример кода]

Существующий указатель на «любой» неполный или объектный тип явно описывается как:

C99 / C11 §6.3.2.3 p1 :

Указатель на void может быть преобразован в или из указателя на любой неполный или объектный тип. […]

Указатель, полученный из существующего указателя на ‘любой’ неполный или объектный тип, указатель на указатель на void, является строго указателем на указатель на void и не должен быть конвертируемым с указателем, полученным из указателя на ‘любой’ неполный или объектный тип.

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


Слова стандарта

Согласно C99 §6.2.5 p27 / C11 §6.2.5 p28 :

[…] Все указатели на типы структур должны иметь одинаковые требования к представлению и выравниванию друг с другом. […]

За которым следует C99 TC3 Footnote 39 / C11 Footnote 48 :

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

Хотя в стандарте не указано: «Указатель на структурный тип», и были выбраны следующие слова: «Все указатели на структурные типы», в нем явно не указано, применимо ли оно к рекурсивному выводу таких указателей. В других случаях, когда в стандарте упоминаются специальные свойства указателей, он явно не указывает и не упоминает рекурсивный вывод указателя, что означает, что либо применяется «вывод типа», либо нет — но это явно не упоминается.

И хотя формулировка «Все указатели на» при обращении к типам используется только дважды (для типов структуры и объединения), в отличие от более четкой формулировки: «Указатель на», которая используется во всем стандарте, мы не можем сделать вывод, применимо ли это к рекурсивному выводу таких указателей.

Комментарии:

1. Каким бы ни был ответ на ваш главный вопрос, allocate это не удобный интерфейс при любом напряжении воображения.

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

3. @WouterHuysentruit Я предпочитаю ответы, которые все это объясняют. Если это становится длинным, ## tl;dr раздел имеет смысл. Но помните, что и вопросы, и ответы на SO также должны быть примером для других и помогать людям с похожими (даже если не совсем одинаковыми) вопросами.

4. Достаточно справедливо, проголосуйте за усилия. Продолжайте в том же духе 🙂

5. @n.m. если бы это сработало, это была бы более приятная форма realloc , чем текущая (перераспределите указатель на месте и используйте возвращаемое значение, чтобы проверить, удалось ли это или нет) — в настоящее время немного сложно писать код, который восстанавливается после сбоя перераспределения

Ответ №1:

Предыстория

Предположение о том, что стандарт неявно требует, чтобы все указатели на типы структур (полные, неполные, совместимые и несовместимые) имели одинаковые требования к представлению и выравниванию, появилось в C89 — за много лет до того, как стандарт потребовал этого явно. Причиной этого была совместимость неполных типов в отдельных единицах перевода, и хотя, согласно комитету по стандартам C, первоначальное намерение состояло в том, чтобы обеспечить совместимость неполного типа с его завершенным вариантом, фактические слова стандарта этого не описывали. Это было изменено во втором техническом исправлении к C89 и, следовательно, конкретизировало исходное предположение.


Совместимость и неполные типы

Читая рекомендации, связанные с совместимостью и неполными типами, благодаря Мэтту Макнаббу, мы находим дальнейшее понимание исходного предположения C89.

Вывод указателей объектных и неполных типов

C99 / C11 §6.2.5 p1 :

Типы разделяются на типы объектов, типы функций и неполные типы.

C99 / C11 §6.2.5 p20 :

Тип указателя может быть получен из типа функции, типа объекта или неполного типа, называемого ссылочным типом.

C99 / C11 §6.2.5 p22 :

Тип структуры или объединения неизвестного содержимого является неполным типом. Для всех объявлений этого типа он завершается объявлением одного и того же тега структуры или объединения с его определяющим содержимым позже в той же области.

Это означает, что указатели могут быть получены как из типов объектов, так и из неполных типов. Хотя не указано, что неполные типы не обязательно должны быть завершены; в прошлом комитет ответил на этот вопрос и заявил, что отсутствия запрета достаточно и нет необходимости в положительном утверждении.

The following pointer to pointer to incomplete ‘struct never_completed’, is never completed:

 int main(void)
{
    struct never_completed *p;
    p = malloc(1024);
}
 

[Complete code sample]

Compatible types of separate translation units

C99 / C11 §6.7.2.3 p4 :

All declarations of structure, union or enumerated types that have the same scope and use the same tag declare the same type.

C99 / C11 §6.2.7 p1 :

Two types have compatible type if their types are the same. Two structure types declared in separate translation units are compatible if their tags (are) the same tag. [trimmed quote] […]

This paragraph has a great significance, allow me to summarize it: two structure types declared in separate translation units are compatible if they use the same tag. If both of them are completed- their members have to be the same (according to the specified guidelines).

Compatibility of pointers

C99 §6.7.5.1 p2 / C11 §6.7.6.1 p2 :

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

If the standard mandates that two structures under specified conditions, are to be compatible in separate translation units whether being incomplete or complete, it means that the pointers derived from these structures are compatible just as well.

C99 / C11 §6.2.5 p20 :

Any number of derived types can be constructed from the object, function, and incomplete types

These methods of constructing derived types can be applied recursively.

And due to the fact that pointer derivation is recursive, it makes pointers derived from pointers to compatible structure types, to be compatible with each other.

Representation of compatible types

C99 §6.2.5 p27 / C11 §6.2.5 p28 :

pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements.

C99 / C11 §6.3 p2 :

Conversion of an operand value to a compatible type causes no change to the value or the representation.

C99 / C11 §6.2.5 p26 :

The qualified or unqualified versions of a type are distinct types that belong to the same type category and have the same representation and alignment requirements.

This means that a conforming implementation can’t have a distinct judgement concerning the representation and alignment requirements of pointers derived from incomplete or complete structure types, due to the possibility that a separate translation unit might have a compatible type, which will have to share the same representation and alignment requirements, and it is required to apply the same distinct judgement with either an incomplete or a complete variation of the same structure type.

The following pointer to pointer to incomplete ‘struct complete_incomplete’:

struct complete_incomplete **p;

Is compatible and shares the same representation and alignment requirements as the following pointer to pointer to complete ‘struct complete_incomplete’:

struct complete_incomplete { int i; } **p;


C89 related

If we wonder about the premise concerning C89, defect report #059 of Jun 93′ questioned:

Both sections do not explicitly require that an incomplete type eventually must be completed, nor do they explicitly allow incomplete types to remain incomplete for the whole compilation unit. Since this feature is of importance for the declaration of true opaque data types, it deserves clarification.

Considering mutual referential structures defined and implemented in different compilation units makes the idea of an opaque data type a natural extension of an incomplete data type.

The response of the committee was:

Opaque data types were considered, and endorsed, by the Committee when drafting the C Standard.


Compatibility versus Interchangeability

We have covered the aspect concerning the representation and alignment requirements of recursive pointer derivation of pointers to structure types, now we are facing a matter that a non-normative footnote mentioned, ‘interchangeability’:

C99 TC3 §6.2.5 p27 Footnote 39 / C11 §6.2.5 p28 Footnote 48 :

The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.

The standard says that the notes, footnotes, and examples are non-normative and are «for information only».

C99 FOREWORD p6 / C11 FOREWORD p8 :

[…] this foreword, the introduction, notes, footnotes, and examples are also for information only.

It’s unfortunate that this confusing footnote was never changed, because at best- the footnote is specifically about the direct types referring to it, so phrasing the footnote as-if the properties of «representation and alignment requirements» are without the context of these specific types, makes it easy to interpret as being a general rule for all types that share a representation and alignment. If the footnote is to be interpreted without the context of specific types, then it’s obvious that the normative text of the standard doesn’t imply it, even without the need to debate the interpretation of the term ‘interchangeable’.

Compatibility of pointers to structure types

C99 / C11 §6.7.2.3 p4:

All declarations of structure, union or enumerated types that have the same scope and use the same tag declare the same type.

C99 / C11 §6.2.7 p1 :

Two types have compatible type if their types are the same.

C99 §6.7.5.1 p2 / C11 §6.7.6.1 p2 :

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

This states the obvious conclusion, different structure types are indeed different types, and because they are different they are incompatible. Therefore, two pointers to two different and incompatible types, are incompatible just as well, regardless of their representation and alignment requirements.

Effective types

C99 / C11 §6.5 p7 :

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

a type compatible with the effective type of the object

C99 / C11 §6.5 p6 :

The effective type of an object for an access to its stored value is the declared type of the object, if any.

Incompatible pointers are not ‘interchangeable’ as arguments to functions, nor as return values from functions. Implicit conversions and specified special cases are the exceptions, and these types are not part of any such exception. Even if we decide to add an unrealistic requirement for said ‘interchangeability’, and say that an explicit conversion is required to make it applicable, then accessing the stored value of an object with an incompatible effective type breaks the effective types rules. For making it a reality we need a new property that currently the standard doesn’t have. Therefore sharing the same representation and alignment requirements, and being convertible, is simply not enough.

This leaves us with being interchangeable ‘as members of unions’, and although they are indeed interchangeable as members of union- it bears no special significance.

Official interpretations

1. The first ‘official’ interpretation belongs to a member of the C standards committee. His interpretation for: «are meant to imply interchangeability», is that it doesn’t actually imply that such an interchangeability exists, but actually makes a suggestion for it.

As much as I would like it to become a reality, I wouldn’t consider an implementation that took a suggestion from a non-normative footnote, not to mention an unreasonably vague footnote, while contradicting normative guidelines- to be a conforming implementation. This obviously renders a program that utilizes and depends on such a ‘suggestion’, to be a non-strictly conforming one.

2. The second ‘official’ interpretation belongs to a member/contributor to the C standards committee, by his interpretation the footnote doesn’t introduce a suggestion, and because the (normative) text of standard doesn’t imply it- he considers it to be a defect in the standard. He even made a suggestion to change the effective types rules for addressing this matter.

3. The third ‘official’ interpretation is from defect report #070 of Dec 93`. It has been asked, within the context of C89, whether a program that passes an ‘unsigned int’ type, where the type ‘int’ is expected, as an argument to a function with a non-prototype declarator, to introduce undefined behavior.

In C89 there’s the very same footnote, with the same implied interchangeability as arguments to functions, attached to:

C89 §3.1.2.5 p2 :

The range of nonnegative values of a signed integer type is a subrange of the corresponding unsigned integer type, and the representation of the same value in each type is the same.

The committee responded that they encourage implementors to allow this interchangeability to work, but since it’s not a requirement, it renders the program to be a non-strictly conforming one.


Следующий пример кода не соответствует требованиям. ‘amp;s1’ и ‘struct generic **’ имеют одинаковые требования к представлению и выравниванию, но, тем не менее, они несовместимы. Согласно правилам эффективных типов, мы обращаемся к сохраненному значению объекта ‘s1’ с несовместимым эффективным типом, указателем на ‘struct generic’, в то время как его объявленный тип, и, следовательно, эффективный тип, является указателем на ‘struct s1’. Чтобы преодолеть это ограничение, мы могли бы использовать указатели как члены объединения, но это соглашение наносит ущерб цели быть универсальным.

 int allocate_struct(void    *p,
                    size_t  s)
{
    struct generic **p2 = p;
    if ((*p2 = malloc(s)) == NULL)
        return -1;
    
    return 0;
}

int main(void)
{
    struct s1 { int i; } *s1;

    if (allocate_struct(amp;s1, sizeof *s1) != 0)
        return EXIT_FAILURE;
}
 

[Полный пример кода]


Следующий пример кода строго соответствует требованиям, чтобы преодолеть проблемы с эффективными типами и быть универсальным, мы используем: 1. указатель на void, 2. требования к представлению и выравниванию всех указателей на структуры и 3. доступ к байтовому представлению указателя «в общем виде», используяmemcpy для копирования представления, не влияя на его эффективный тип.

 int allocate_struct(void    *pv,
                    size_t  s)
{
    struct generic *pgs;

    if ((pgs = malloc(s)) == NULL)
        return -1;
    
    memcpy(pv, amp;pgs, sizeof pgs);
    return 0;
}

int main(void)
{
    struct s1 { int i; } *s1;

    if (allocate_struct(amp;s1, sizeof *s1) != 0)
        return EXIT_FAILURE;
}
 

[Полный пример кода]


Заключение

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

Комментарии:

1. Подобно тому, как у нас есть массив целых чисел, массив указателей и т. Д., У нас также может быть массив структурных переменных. И чтобы сделать использование массива структурных переменных эффективным, мы используем указатели структурного типа. У нас также может быть указатель на одну структурную переменную, но в основном он используется с массивом структурных переменных.

2. Язык, определенный K amp; R, позволил заставить функцию принимать массив указателей на структуры и работать с элементами массива, не беспокоясь о том, какие структуры они идентифицировали. При передаче массива такой функции потребуется приведение, но в противном случае все будет работать. C89 не требовал , чтобы реализации продолжали поддерживать такую способность, но поскольку эта способность была полезной, реализации поддерживали ее до тех пор, пока кто-то не решил, что язык станет «лучше», удалив функции, которые, хотя и полезны, не были предписаны Стандартом.

3. Будет ли allocate_struct гарантированно работать версия, которая использует memcpy ), если цель находится в выделенном хранилище? По моему прочтению Стандарта, после memcpy эффективного типа хранилища, содержащего рассматриваемый указатель, будет struct generic* ,

Ответ №2:

Мой ответ «нет».

Ни в одном известном мне стандарте C нет формулировки, которая предполагала бы обратное. Тот факт, что все указатели на типы структур имеют одинаковые требования к представлению и выравниванию, не имеет никакого отношения к какому-либо производному типу.

Это имеет полный смысл, и любая другая реальность может показаться непоследовательной. Рассмотрим альтернативу:

Давайте назовем требования к выравниванию и представлению для указателей на типы структур «A». Предположим, что любой «рекурсивно производный тип» разделяет требования «A».

Давайте назовем требования к выравниванию и представлению для указателей на типы объединения «B». Предположим, что любой «рекурсивно производный тип» разделяет требования «B».

Предположим, что «A» и «B» не совпадают [1]. Кроме того, давайте предположим, что они не могут быть выполнены одновременно. (Например, 4-байтовое представление и 8-байтовое представление.)

Теперь выведите тип из обоих:

  1. Тип с требованиями «A»
  2. Тип с требованиями «B»

Теперь у вас есть тип, требования которого невозможно удовлетворить, потому что он должен удовлетворять «A» и «B», но они не могут быть удовлетворены одновременно.

Возможно, вы думаете, что производные типы имеют плоскую родословную вплоть до одного предка, но это не так. Производные типы могут иметь много предков. Это обсуждается в стандартном определении «производных типов».

[1] Хотя это может показаться необоснованным, маловероятным и глупым, это разрешено.

Комментарии:

1. «Предположим, что любой «рекурсивно производный тип» соответствует требованиям «A».» — каково ваше обоснование для такого предположения? Соответствующая реализация может иметь разный размер / представление. struct X ** struct X *

2. М. М.: Пожалуйста, пересмотрите значение слова «предположим». Цель мыслительного упражнения состояла в том, чтобы возразить против возможности такого требования, продемонстрировав его непоследовательность. Мы в одной команде.