гъвкав масив в C и грешка при дерефериране на указател с набито тип

Когато се опитам да компилирам кода по-долу с gcc -O3 -Wall -Werror -std=c99 main.c, получавам грешка като „дереферирането на указател с набито тип ще наруши строги правила за псевдоним“ на #3, но не и на #2 или #1. Мога да деименувам "char *", но защо не мога да направя същото с гъвкави масиви?

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

struct Base {
        void (*release) (struct Base *);
        size_t sz;

        char *str_foo;
        char rest[];
};

struct Concrete {
        char *str_bar;
};

void
Base_release(struct Base *base)
{
        free(base);
}

struct Base *
Base_new(size_t sz_extra)
{
        size_t sz = sizeof(struct Base) + sz_extra;
        struct Base *base = (struct Base *)malloc(sz);
        base->release = &Base_release;
        base->sz = sz;

        base->str_foo = "foo";
        return base;
}

#define BASE_FREE(_obj) (_obj)->release(_obj)
#define BASE_CAST(_type, _obj) ((struct _type *)((_obj)->rest))
#define BASE_CAST_2(_type, _obj) ((struct _type *)((char *)(_obj)+sizeof(struct Base)))

struct Base *
Concrete_new()
{
        struct Base *base = Base_new(sizeof(struct Concrete));
        struct Concrete *concrete = BASE_CAST(Concrete, base);
        concrete->str_bar = "bar";
        return base;
}

int main(int argc, const char *argv[])
{
        struct Base *instance = Concrete_new();
        printf("Base str: %s\n", instance->str_foo);

        // #1 - Legal
        struct Concrete *cinstance = BASE_CAST(Concrete, instance);
        printf("#1: Concrete str: %s\n", cinstance->str_bar);

        // #2 - Legal
        printf("#2: Concrete str: %s\n", BASE_CAST_2(Concrete, instance)->str_bar);

        // #3 - Compile error
        printf("#3: Concrete str: %s\n", BASE_CAST(Concrete, instance)->str_bar);

        BASE_FREE(instance);

        return 0;
}

РЕДАКТИРАНЕ 1: Има по-конкретен пример, показващ проблема по-долу:

struct s {                               
        char a;                              
};                                          
char *const a = malloc(sizeof(struct s));
char b[sizeof(struct s)];   
((struct s *)((char *)a))->a = 5; // This is a valid case
((struct s *)(a))->a = 5; // OK
((struct s *)((char *)b))->a = 5; // ???

person ProgDevel    schedule 30.07.2014    source източник
comment
Издадено е предупреждение, защото е нарушение на правилата. Интересувате ли се защо първите ви два случая не са задействали предупреждение или търсите заобиколно решение?   -  person keltar    schedule 30.07.2014
comment
Интересува ме защо първият случай не задейства предупреждение. Знам две решения: обединения и наследяване чрез композиция. (Съжалявам, английският ми не е добър :)   -  person ProgDevel    schedule 30.07.2014


Отговори (2)


Всички те нарушават строгото псевдоним чрез псевдоним на масив от char като тип, който не е char. (Обратното на това обаче е разрешено).

Освен това всички те може да имат проблеми с подравняването; rest може да не е правилно подравнен за структура.

Възможно е да не получите предупреждение, защото:

  • Откриването на нарушение на псевдонима от компилатора не е толкова добро и/или
  • Вашата система всъщност позволява това псевдоним, въпреки че не е преносимо

Ако сте заменили rest с указател към динамично разпределена памет, тогава и двата проблема изчезват, тъй като „ефективният тип“ на динамично разпределената памет се определя от това, което съхранявате в нея.

Забележете, струва ми се много по-добро решение да направя:

struct Concrete
{
    struct Base base;
    char const *str_bar;
};

или алтернативно, запазете Concrete както го имате и направете:

struct BaseConcrete
{
    struct Base base;
    struct Concrete concrete;
};
person M.M    schedule 30.07.2014
comment
Благодаря за бележките относно подравняването на структурата и за char const *str_bar =) - person ProgDevel; 31.07.2014

Първо, това не е свързано с гъвкави масиви, а по-скоро с всеки масив. Можете да използвате достатъчно голям масив с фиксиран размер и да видите същите резултати.

Най-очевидното решение е вашето BASE_CAST_2. Друг може да използва offsetof вместо sizeof, особено в случаите, когато опашката на структурите не е гъвкава.

Разликата между първия и третия случай е трудна. И двете нарушават строги правила за псевдоним, но gcc понякога позволява това, когато не може да определи произхода на lvalue. Въпреки това, с -Wstrict-alising=2 ще даде предупреждение и в двата случая. Не съм сигурен, че е гарантирано генерирането на валиден код, дори ако няма предупреждение, издадено със стандарт -Wstrict-aliasing (но в дадения пример го прави).

Вторият случай изглежда добре, защото прехвърлянето на structure* към char* е разрешено. Има обаче разлика между типовете char* и char[].

person keltar    schedule 30.07.2014
comment
Благодаря за отговора. Казвате Вторият случай изглежда добре, защото прехвърлянето на структура* към char* е разрешено. Но защо ((struct _type *)((char *)((_obj)->rest))) не работи? - person ProgDevel; 30.07.2014
comment
Псевдонимите са проверени преди каквито и да е прехвърляния (е, зависи от нивото на предупреждение). - person keltar; 30.07.2014