Строгое наследование и наложение наследования

Рассмотрим этот пример кода:

#include <stdio.h>

typedef struct A A;

struct A {
   int x;
   int y;
};

typedef struct B B;

struct B {
   int x;
   int y;
   int z;
};

int main()
{
    B b = {1,2,3};
    A *ap = (A*)&b;

    *ap = (A){100,200};      //a clear http://port70.net/~nsz/c/c11/n1570.html#6.5p7 violation

    ap->x = 10;  ap->y = 20; //lvalues of types int and int at the right addrresses, ergo correct ?

    printf("%d %d %d\n", b.x, b.y, b.z);
}

Раньше я думал, что что-то вроде приведения B* к A* и использования A* для манипулирования объектом B* является строгим нарушением псевдонимов. Но потом я понял, что стандарт действительно требует, чтобы:

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

и выражения, такие как ap->x имеют правильный тип и адрес, а также тип ap не должно действительно иметь значение там (или делает это?). Это, на мой взгляд, подразумевает, что этот тип наследования оверлеев является правильным, если подструктурой не манипулируют в целом.

Является ли эта интерпретация ошибочной или якобы противоречащей тому, что предполагали авторы стандарта?

2 ответа

Решение

Линия с *ap = является строгим нарушением псевдонимов: объект типа B записывается с использованием выражения типа lvalue A,

Предположим, что этой строки не было, и мы перешли на ap->x = 10; ap->y = 20;, В этом случае lvalue типа int используется для записи объектов типа int,

Существует разногласие относительно того, является ли это строгим нарушением алиасинга или нет. Я думаю, что в письме Стандарта говорится, что это не так, но другие (включая разработчиков gcc и clang) считают ap->x подразумевая, что *ap был доступен. Большинство согласны с тем, что стандартное определение строгого псевдонима слишком расплывчато и нуждается в улучшении.

Пример кода, используя ваши определения структуры:

void f(A* ap, B* bp)
{
  ap->x = 213;
  ++bp->x;
  ap->x = 213;
  ++bp->x;
}

int main()
{
   B b = { 0 };
   f( (A *)&b, &b );
   printf("%d\n", b.x);
}

Для меня это выводы 214 в -O2, а также 2 в -O3, с gcc. Сгенерированная сборка на Годболт для gcc 6.3 была:

f:
    movl    (%rsi), %eax
    movl    $213, (%rdi)
    addl    $2, %eax
    movl    %eax, (%rsi)
    ret

который показывает, что компилятор переставил функцию так:

int temp = bp->x + 2;
ap->x = 213;
bp->x = temp;

и поэтому компилятор должен учитывать, что ap->x может не псевдоним bp->x,

Когда был написан C89, компилятору было бы нецелесообразно поддерживать гарантии Common Initial Sequence для объединений, не поддерживая их и для указателей структуры. Напротив, указание гарантий СНГ для структурных указателей не означает, что профсоюзы будут вести себя аналогичным образом, если их адрес не будет принят. Учитывая, что гарантии СНГ были применимы к указателям структуры с января 1974 года - еще до union К языку было добавлено ключевое слово - и многие годы в течение многих лет код полагался на такое поведение в обстоятельствах, которые не могли правдоподобно включать объекты union типа, и что авторы C89 были более заинтересованы в том, чтобы сделать Стандарт кратким, чем в том, чтобы сделать его "защищенным от языка", я бы предположил, что спецификация C89 правила CIS в терминах союзов, а не структурных указателей была почти наверняка мотивирована желанием избежать избыточности, а не желанием предоставить компиляторам свободу изо всех сил нарушать прецедент 15 с лишним лет в применении гарантий СНГ к указателям на структуры.

Авторы C99 признали, что в некоторых случаях применение правила CIS к указателям на структуру может ухудшить то, что в противном случае было бы полезно для оптимизации, и указали, что если указатель одного типа структуры используется для проверки CIS члена другого, гарантия CIS выиграла не выполняется, если определение полного типа объединения, содержащего обе структуры, находится в области видимости. Таким образом, для того, чтобы ваш пример был совместим с C99, он должен содержать определение union тип, содержащий обе ваши структуры. Это правило, по-видимому, было мотивировано желанием разрешить компиляторам ограничивать применение CIS в тех случаях, когда у них есть основания ожидать, что два типа могут быть использованы в связанном виде, и разрешить коду указывать, что типы связаны без добавить новую языковую конструкцию для этой цели.

Авторы gcc, похоже, считают, что, поскольку для кода было бы необычно получить указатель на член объединения, а затем захотеть получить доступ к другому члену объединения, простой видимости полного определения типа объединения не должно быть достаточно заставить компилятор поддерживать гарантии СНГ, хотя большинство применений СНГ всегда вращалось вокруг структурных указателей, а не профсоюзов. Следовательно, авторы gcc отказываются поддерживать конструкции, подобные вашей, даже в тех случаях, когда этого требует стандарт C99.

Другие вопросы по тегам