Обычный полиморфизм C, каламбур и строгий псевдоним. Насколько это законно?

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

#include <stdio.h>
#include <stdlib.h>

typedef struct foo {
    int foo;
    int bar;
} foo;

void make_foo(void * p)
{
    foo * this = (foo *)p;

    this->foo = 0;
    this->bar = 1;
}

typedef struct more_foo {
    int foo;
    int bar;
    int more;
} more_foo;

void make_more_foo(void * p)
{
    make_foo(p);

    more_foo * this = (more_foo *)p;
    this->more = 2;
}

int main(void)
{
    more_foo * mf = malloc(sizeof(more_foo));

    make_more_foo(mf);
    printf("%d %d %d\n", mf->foo, mf->bar, mf->more);

    return 0;
}

Насколько я понял, это является каламбуром типов и, как предполагается, нарушает строгие правила псевдонима. Но есть ли это? Переданные указатели недействительны. Вы можете интерпретировать указатель void как хотите, верно?

Кроме того, я читал, что могут быть проблемы с выравниванием памяти. Но выравнивание структуры детерминировано. Если начальные члены одинаковы, то они будут выровнены таким же образом, и не должно возникнуть проблем с доступом ко всем членам foo из указателя more_foo. Это верно?

GCC компилируется с -Wall без предупреждений, программа работает как положено. Однако я не уверен, UB это или нет и почему.

Я также видел, что это:

typedef union baz {
    struct foo f;
    struct more_foo mf;
} baz;

void some_func(void)
{
    baz b;
    more_foo * mf = &b.mf; // or more_foo * mf = (more_foo *)&b;

    make_more_foo(mf);
    printf("%d %d %d\n", mf->foo, mf->bar, mf->more);
}

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

Изменить: union baz теперь компилируется.


person Vlad Dinev    schedule 13.02.2018    source источник
comment
union baz недействителен и не компилируется. Пожалуйста исправьте.   -  person Paul Ogilvie    schedule 13.02.2018
comment
Нарушение AA, я думаю, происходит с make_foo(p); more_foo * this = (more_foo *)p; this->more = 2;, поскольку компилятор может игнорировать, что p и this указывают на перекрывающиеся данные. Таким образом, порядок оценки make_foo(p); и more_foo * this = (more_foo *)p; this->more = 2; может происходить либо в порядке, либо одновременно. Теперь компилятор мог подумать, что он может использовать int доступ в 2 раза к this и перезаписать то, что сделал make_foo(p);. Звучит надумано, но это АА.   -  person chux - Reinstate Monica    schedule 13.02.2018
comment
Вы можете интерпретировать указатель void как хотите, верно? Нет. Любой void* может быть изменен на указатель символа или на его исходный тип указателя. Но если указатель изначально был адресом int, он не может быть скрытым до struct foo*. Я не думаю, что эта проблема сводит на нет ваше расследование.   -  person chux - Reinstate Monica    schedule 13.02.2018
comment
C действительно определяет. Все указатели на структурные типы должны иметь те же требования к представлению и выравниванию, что и друг друга. C11dr §6.2.5 28, поэтому выравнивание структуры может не быть проблемой. (если эта спецификация выравнивания не относится к самому указателю).   -  person chux - Reinstate Monica    schedule 13.02.2018
comment
@chux Что вы имеете в виду под компилятором, который может думать, что он может использовать int доступ шириной 2x? Ширина int известна. Даже если порядок оценки меняется, элементы не перекрываются в памяти.   -  person Vlad Dinev    schedule 14.02.2018
comment
@chux Все указатели на структурные типы должны иметь одинаковые требования к представлению и выравниванию. Это может быть глупый вопрос, но что это вообще значит?   -  person Vlad Dinev    schedule 14.02.2018
comment
Даже если порядок оценки меняется, члены не перекрываются в памяти, но компилятору не нужно знать, перекрываются они или нет. Из-за AA можно предположить, что целиком *this и *p не перекрываются. Предположим, что компилятор может иметь доступ к быстрой 128-битной инструкции чтения и читать сразу весь *this в регистр. (Компилятор также дополнил struct more_foo до 128 бит.) Включает this->more = 2; в регистр и записывает его обратно в *this. В то же самое время make_foo(p); происходило. Порядок этих двух неопределен и UB на AA - насколько я понимаю.   -  person chux - Reinstate Monica    schedule 14.02.2018
comment
@VladDinev C допускает множество архитектур, а не только плоские. Я беру Все указатели на типы структур ... означают, что любой struct * закодирован так же, как и другие struct *, и они указывают на данные в той же области памяти, которая имеет общее требование выравнивания. char объекты могут существовать где-то еще с другим выравниванием и представлением указателя. То же самое для double объект может существовать где-то еще.   -  person chux - Reinstate Monica    schedule 14.02.2018
comment
@chux Я бы подумал, что внутри make_more_foo не будет нарушения SA, поскольку единственные преобразования - в / из void *. Ему присваивается void *, он переходит к другой функции, ожидающей void *, затем преобразует его в more_foo * и разыменовывает.   -  person dbush    schedule 22.03.2018


Ответы (2)


Авторы Стандарта не считали необходимым указывать какие-либо средства, с помощью которых lvalue типа члена структуры или объединения может использоваться для доступа к базовой структуре или объединению. Способ написания N1570 6.5p7 даже не допускает someStruct.member = 4;, если member если символьного типа. Однако возможность применять оператор & к членам структуры и объединения не имеет никакого смысла, если только авторы Стандарта не ожидают, что полученные указатели будут полезны для чего-то. Учитывая сноску 88: «Цель этого списка состоит в том, чтобы указать те обстоятельства, при которых объект может иметь или не иметь псевдоним», наиболее логичным ожиданием является то, что он был предназначен только для применения в тех случаях, когда полезные сроки жизни lvalue будут перекрываться способами это будет связано с алиасингом.

Рассмотрим две функции в приведенном ниже коде:

struct s1 {int x;};
struct s2 {int x;};
union {struct s1 v1; struct s2 v2;} arr[10];

void test1(int i, int j)
{
  int result;
  { struct s1 *p1 = &arr[i].v1; result = p1->x; }
  if (result)
    { struct s2 *p2 = &arr[j].v2; p2->x = 2; }
  { struct s1 *p3 = &arr[i].v1; result = p3->x; }
  return result;
}

void test2(int i, int j)
{
  int result;
  struct s1 *p1 = &arr[i].v1; result = p1->x;
  if (result)
    { struct s2 *p2 = &arr[j].v2; p2->x = 2; }
  result = p1->x; }
  return result;
}

В test1, даже если i == j, доступ ко всем указателям, к которым когда-либо будет осуществлен доступ в течение времени жизни p1, будет осуществляться через p1, поэтому p1 не будет иметь никакого псевдонима. Аналогично с p2 и p3. Таким образом, поскольку нет псевдонимов, не должно быть проблем, если i==j. В test2, однако, если i==j, то создание p1 и последнее использование его для доступа к p1->x будет разделено другим действием, которое обращается к этому хранилищу с указателем, не производным от p1. Следовательно, если i==j, то доступ через p2 будет псевдонимом p1, и в соответствии с N1570 5.6p7 компилятор не потребуется, чтобы учесть эту возможность.

Если правила 5.6p7 применимы даже в случаях, которые не связаны с псевдонимом, то структуры и объединения будут бесполезны. Если они применимы только в случаях, когда действительно используется псевдоним, тогда можно было бы избавиться от множества ненужных сложностей, таких как правила «Эффективного типа». К сожалению, некоторые компиляторы, такие как gcc и clang, используют правила, чтобы оправдать «оптимизацию» первой вышеупомянутой функции, а затем предполагать, что им не нужно беспокоиться о результирующем псевдониме, который присутствует в их «оптимизированной» версии, но не входит в оригинал.

Ваш код будет нормально работать в любом компиляторе, авторы которого приложат все усилия, чтобы распознать производные lvalue. Однако и gcc, и clang испортят даже указанную выше функцию test1 (), если они не вызываются с флагом -fno-strict-aliasing. Учитывая, что Стандарт даже не допускает someStruct.member = 4;, я бы посоветовал вам воздержаться от вида псевдонимов, показанного в test2() выше, и не беспокоиться о компиляторах, которые даже не могут обрабатывать test1().

person supercat    schedule 04.04.2018

Я бы сказал, что это не строго, поскольку, если вы измените структуру «foo», структура «more foo» должна измениться вместе с ней. «foo» должно стать основой «more foo», это наследование, а не совсем полиморфизм. Но вы можете использовать указатели на функции, чтобы ввести полиморфизм, чтобы помочь с этими структурами.

Пример

#include <stdio.h>
#include <stdlib.h>

#define NEW(x) (x*)malloc(sizeof(x));

typedef struct 
{
    void(*printme)(void*);

    int _foo;
    int bar;

} foo;

typedef struct 
{
    // inherits foo
    foo base;
    int more;
} more_foo;


void foo_print(void *t)
{
    foo *this = (foo*)t;

    printf("[foo]\r\n\tfoo=%d\r\n\tbar=%d\r\n[/foo]\r\n", this->bar, this->_foo);
}

void more_foo_print(void *t)
{
    more_foo *this = t;

    printf("[more foo]\r\n");

    foo_print(&this->base);

    printf("\tmore=%d\r\n", this->more);

    printf("[/more foo]\r\n");
}


void foo_construct( foo *this, int foo, int bar )
{
    this->_foo = foo;
    this->bar = bar;

    this->printme = foo_print;
}

void more_foo_construct(more_foo *t, int _foo, int bar, int more)
{
    foo_construct((foo*)t, _foo, bar);

    t->more = more;

    // Overrides printme
    t->base.printme = more_foo_print;
}

more_foo *new_more_foo(int _foo, int bar, int more)
{
    more_foo * new_mf = NEW(more_foo);

    more_foo_construct(new_mf, _foo, bar, more);

    return new_mf;
}

foo *new_foo(int _foo, int bar)
{
    foo *new_f = NEW(foo);

    foo_construct(new_f, _foo, bar);

    return new_f;
}

int main(void)
{
    foo * mf = (foo*)new_more_foo(1, 2, 3);
    foo * f = new_foo(7,8);

    mf->printme(mf);

    f->printme(f);

    return 0;
}
  • printme () переопределяется при создании "more foo". (полиморфизм)

  • more_foo включает foo как базовую структуру (наследование), поэтому, когда структура «foo» изменяется, «more foo» изменяется вместе с ней (например, добавляются новые значения).

  • more_foo можно преобразовать как «foo».

person pm101    schedule 22.03.2018