Quais são as semânticas dos objetos sobrepostos em C?

25

Considere a seguinte estrutura:

struct s {
  int a, b;
};

Normalmente 1 , essa estrutura terá tamanho 8 e alinhamento 4.

E se criarmos dois struct sobjetos (mais precisamente, gravamos no armazenamento alocado dois desses objetos), com o segundo objeto sobreposto ao primeiro?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

Existe algo sobre esse comportamento indefinido do programa? Se sim, onde fica indefinido? Se não for UB, é garantido que você sempre imprima o seguinte:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

Em particular, quero saber o que acontece com o objeto apontado por o1quando o2, que se sobrepõe, é gravado. Ainda é permitido acessar a parte desobstruída ( o1->a)? O acesso à parte estragada o1->bé o mesmo que o acesso o2->a?

Como o tipo eficaz se aplica aqui? As regras são claras o suficiente quando você está falando sobre objetos e ponteiros que não se sobrepõem e apontam para o mesmo local da última loja, mas quando você começa a falar sobre o tipo eficaz de partes de objetos ou objetos sobrepostos, fica menos claro.

Alguma coisa mudaria se a segunda gravação fosse de um tipo diferente? Se os membros fossem dizer inte em shortvez de dois ints?

Aqui está um godbolt, se você quiser brincar com ele lá.


1 Esta resposta se aplica a plataformas onde também não é o caso: por exemplo, algumas podem ter tamanho 4 e alinhamento 2. Em uma plataforma em que o tamanho e o alinhamento eram iguais, essa pergunta não se aplicaria, pois objetos alinhados e sobrepostos ser impossível, mas não tenho certeza se existe alguma plataforma como essa.

BeeOnRope
fonte
2
Tenho certeza de que é UB, mas deixarei que um advogado de idiomas forneça capítulo e verso.
Barmar 7/04
Penso que o compilador C nos antigos sistemas vetoriais Cray forçou o alinhamento e o tamanho a serem iguais, com um modelo ILP64 e alinhamento forçado de 64 bits (endereços são palavras de 64 bits - sem endereçamento de bytes). Claro que isso gerou muitos outros problemas ....
John D McCalpin 09/04

Respostas:

15

Basicamente, essa é toda a área cinza do padrão; a regra de aliasing estrita especifica casos básicos e deixa o leitor (e os fornecedores do compilador) preencher os detalhes.

Houve um esforço para escrever uma regra melhor, mas até agora eles não resultaram em nenhum texto normativo e não tenho certeza qual é o status disso para o C2x.

Como mencionado na minha resposta à sua pergunta anterior, a interpretação mais comum é que isso p->qsignifica (*p).qe o tipo efetivo se aplica a todos *p, mesmo depois de aplicarmos .q.

Sob essa interpretação, printf("o1.a=%d\n", o1->a);causaria um comportamento indefinido, pois o tipo efetivo do local *o1não é s(uma vez que parte dele foi substituída).

A justificativa para essa interpretação pode ser vista em uma função como:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

Com essa interpretação, a última linha poderia ser otimizada puts("5");, mas sem ela, o compilador precisaria considerar que a chamada de função pode ter sido f(o1, o2);e, portanto, perderá todos os benefícios que são supostamente fornecidos pela regra de aliasing estrita.

Um argumento semelhante se aplica a dois tipos de estrutura não relacionados que possuem um intmembro em deslocamento diferente.

MILÍMETROS
fonte
11
Com f(s* s1, s* s2), sem restrict, o compilador não pode assumir s1e s2são indicadores diferentes. Eu acho que , novamente sem restrict, não pode nem assumir que eles não se sobrepõem parcialmente. IAC, não vejo que a preocupação do OP seja bem demonstrada pela f()analogia. Boa sorte invencível. UV para o primeiro semestre.
chux - Restabelece Monica
@ chux-ReinstateMonica sem restrição, s1 == s2seria permitido, mas não se sobrepõe parcialmente. (A otimização no meu exemplo de código ainda pode ser executada se s1 == s2)
MM
@ chux-ReinstateMonica você também pode considerar o mesmo problema com apenas em intvez de estruturas (e um sistema com _Alignof(int) < sizeof(int)).
MM
3
O status desse tipo de pergunta sobre o tipo efetivo de C2x é praticamente aberto e ainda está sujeito a debate no grupo de estudo. Mas tenha cuidado ao reivindicar equivalência de p->qe (*p).q. Isso pode ser verdade para a interpretação do tipo, como você declara, mas não é verdade do ponto de vista operacional. É importante para acessos simultâneos à mesma estrutura que o acesso de um membro não implica o acesso de nenhum outro membro.
Jens Gustedt
A regra estrita de alias é sobre acesso . A expressão do lado esquerdo na E1.E2expressão não realiza acesso (quero dizer a E1expressão inteira . Algumas de suas subexpressões podem realizar acesso. Ou seja, se E1for (*p), então, ler o valor do ponteiro ao avaliar o pacesso, mas a avaliação *pou (*p)não realiza nenhum Acesso). A regra estrita de alias não se aplica quando não há acesso.
Language Lawyer