Qual é a regra estrita de alias?

804

Ao perguntar sobre o comportamento indefinido comum em C , as pessoas às vezes se referem à regra estrita de alias.
Sobre o que eles estão falando?

Benoit
fonte
12
@ Ben Voigt: As regras de alias são diferentes para c ++ e c. Por que esta pergunta está marcada com ce c++faq.
MikeMB
6
@ MikeMB: Se você verificar o histórico, verá que eu mantive as tags do jeito que eram originalmente, apesar da tentativa de alguns outros especialistas de alterar a pergunta das respostas existentes. Além disso, a dependência de idioma e de versão é uma parte muito importante da resposta para "Qual é a regra estrita de alias?" e conhecer as diferenças é importante para as equipes migrarem código entre C e C ++ ou gravarem macros para uso em ambas.
Ben Voigt
6
Ben Voigt: Na verdade - até onde eu sei - a maioria das respostas diz respeito apenas a c e não a c ++, também a redação da pergunta indica um foco nas regras C (ou o OP simplesmente não sabia, há uma diferença ) Na maioria das vezes, as regras e a idéia geral são as mesmas, é claro, mas, especialmente, no que diz respeito aos sindicatos, as respostas não se aplicam ao c ++. Estou um pouco preocupado, que alguns programadores de c ++ procurem a regra estrita de alias e suponham que tudo o que foi declarado aqui também se aplica ao c ++.
MikeMB
Por outro lado, concordo que é problemático alterar a pergunta depois que muitas boas respostas foram publicadas e o problema é menor, de qualquer maneira.
MikeMB
1
@ MikeMB: Eu acho que você verá que o foco C na resposta aceita, tornando-a incorreta para C ++, foi editado por terceiros. Essa parte provavelmente deve ser revisada novamente.
Ben Voigt

Respostas:

562

Uma situação típica em que você encontra problemas estritos de aliasing é sobrepor uma estrutura (como uma mensagem de dispositivo / rede) em um buffer do tamanho da palavra do seu sistema (como um ponteiro para uint32_ts ou uint16_ts). Quando você sobrepõe uma estrutura a esse buffer ou um buffer a essa estrutura por meio da conversão de ponteiro, é possível violar facilmente regras estritas de alias.

Portanto, nesse tipo de configuração, se eu quiser enviar uma mensagem para algo, eu teria que ter dois ponteiros incompatíveis apontando para o mesmo pedaço de memória. Eu poderia então ingenuamente codificar algo como isto (em um sistema com sizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

A regra estrita de alias torna ilegal essa configuração: a exclusão de um ponteiro que alias um objeto que não seja de um tipo compatível ou um dos outros tipos permitidos pelo parágrafo 7 1 do C 2011 6.5 é um comportamento indefinido. Infelizmente, você ainda pode codificar dessa maneira, talvez receber alguns avisos, compilar bem, apenas para ter um comportamento inesperado e estranho ao executar o código.

(O GCC parece um pouco inconsistente em sua capacidade de emitir avisos de alias, às vezes nos dando um aviso amigável e às vezes não.)

Para ver por que esse comportamento é indefinido, temos que pensar sobre o que a regra de aliasing estrita compra o compilador. Basicamente, com essa regra, ele não precisa pensar em inserir instruções para atualizar o conteúdo de buffcada execução do loop. Em vez disso, ao otimizar, com algumas suposições irritantemente não aplicadas sobre alias, ele pode omitir essas instruções, carregar buff[0]e buff[1] nos registros da CPU uma vez antes da execução do loop e acelerar o corpo do loop. Antes da introdução do aliasing estrito, o compilador tinha que viver em um estado de paranóia que o conteúdo de buffpoderia mudar a qualquer momento e de qualquer lugar por qualquer pessoa. Portanto, para obter uma vantagem extra de desempenho e assumindo que a maioria das pessoas não digita dicas, foi introduzida a regra estrita de alias.

Lembre-se, se você acha que o exemplo é artificial, isso pode até acontecer se você estiver passando um buffer para outra função que está enviando para você, se tiver.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

E reescreveu nosso loop anterior para tirar proveito dessa função conveniente

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

O compilador pode ou não ser capaz ou inteligente o suficiente para tentar incorporar o SendMessage e pode ou não decidir carregar ou não o buff novamente. Se SendMessagefaz parte de outra API que é compilada separadamente, provavelmente possui instruções para carregar o conteúdo do buff. Por outro lado, talvez você esteja em C ++ e esta é uma implementação apenas de cabeçalho modelado que o compilador acha que pode incorporar. Ou talvez seja apenas algo que você escreveu em seu arquivo .c para sua própria conveniência. De qualquer forma, um comportamento indefinido ainda pode ocorrer. Mesmo quando sabemos um pouco do que está acontecendo sob o capô, ainda é uma violação da regra, portanto, nenhum comportamento bem definido é garantido. Portanto, apenas agrupar uma função que usa nosso buffer delimitado por palavras não ajuda necessariamente.

Então, como faço para contornar isso?

  • Use uma união. A maioria dos compiladores suporta isso sem reclamar sobre aliasing estrito. Isso é permitido no C99 e explicitamente permitido no C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Você pode desativar o aliasing estrito no seu compilador ( f [no-] aliasing estrito no gcc))

  • Você pode usar o char*alias em vez da palavra do seu sistema. As regras permitem uma exceção para char*(incluindo signed chare unsigned char). Sempre se assume que char*aliases outros tipos. No entanto, isso não funcionará da outra maneira: não há suposição de que sua estrutura aliase um buffer de caracteres.

Cuidado para iniciantes

Este é apenas um campo minado em potencial ao sobrepor dois tipos um ao outro. Você também deve aprender sobre endianness , alinhamento de palavras e como lidar com problemas de alinhamento através de estruturas de embalagem corretamente.

Nota de rodapé

1 Os tipos que o C 2011 6.5 7 permite que um lvalue acesse são:

  • um tipo compatível com o tipo efetivo do objeto,
  • uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do objeto,
  • um tipo agregado ou de união que inclua um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou
  • um tipo de caractere.
Doug T.
fonte
16
Estou voltando depois da batalha que parece .. pode unsigned char*ser usado agora char*? I tendem a usar unsigned char, em vez de charcomo o tipo subjacente para byteporque meus bytes não são assinados e eu não quero que a estranheza do comportamento assinado (nomeadamente wrt para overflow)
Matthieu M.
30
@ Matthieu: Assinatura não faz diferença para regras de alias, então usar unsigned char *é bom.
Thomas Eding
22
Não é um comportamento indefinido ler de um membro do sindicato diferente do último a ser escrito?
R. Martinho Fernandes
23
Bollocks, esta resposta é completamente ao contrário . O exemplo que mostra como ilegal é realmente legal e o exemplo que mostra como legal é realmente ilegal.
R. Martinho Fernandes
7
Suas declarações de buffer de uint32_t* buff = malloc(sizeof(Msg));união e as subsequentes unsigned int asBuffer[sizeof(Msg)];terão tamanhos diferentes e nenhuma está correta. A mallocligação é baseada no alinhamento de 4 bytes sob o capô (não faça isso) e a união será 4 vezes maior do que precisa ser ... Entendo que é por clareza, mas isso não me incomoda. menos ...
nonsensickle
233

A melhor explicação que encontrei é de Mike Acton, Entendendo o aliasing estrito . Ele se concentrou um pouco no desenvolvimento do PS3, mas isso é basicamente apenas o GCC.

Do artigo:

"Aliasing estrito é uma suposição, feita pelo compilador C (ou C ++), de que referenciar ponteiros para objetos de tipos diferentes nunca se referirá ao mesmo local de memória (por exemplo, alias um ao outro)."

Então, basicamente, se você int*apontar para alguma memória que contenha um inte, em seguida, apontar a float*para essa memória e usá-la como uma floatviolação da regra. Se seu código não respeitar isso, o otimizador do compilador provavelmente quebrará seu código.

A exceção à regra é a char*, que pode apontar para qualquer tipo.

Niall
fonte
6
Então, qual é a maneira canônica de usar legalmente a mesma memória com variáveis ​​de 2 tipos diferentes? ou todo mundo simplesmente copia?
jiggunjer
4
A página de Mike Acton é falho. A parte de "Transmitir através de uma união (2)", pelo menos, é totalmente errada; o código que ele afirma ser legal não é.
Davmac
11
@davmac: Os autores do C89 nunca pretenderam forçar os programadores a pularem por cima dos aros. Eu acho completamente bizarra a noção de que uma regra que existe com o único objetivo de otimização deva ser interpretada de maneira a exigir que os programadores escrevam código que copia redundantemente dados na esperança de que um otimizador remova o código redundante.
supercat
1
@curiousguy: "Não pode ter sindicatos"? Em primeiro lugar, o objetivo original / primário dos sindicatos não tem nenhuma relação com o alias. Em segundo lugar, a especificação da linguagem moderna permite explicitamente o uso de uniões para aliasing. O compilador deve observar que uma união é usada e tratar a situação de uma maneira especial.
AnT
5
@curiousguy: False. Em primeiro lugar, a idéia conceitual original por trás dos sindicatos era que, a qualquer momento, havia apenas um objeto membro "ativo" no objeto da união, enquanto os outros simplesmente não existiam. Portanto, não existem "objetos diferentes no mesmo endereço" em que você acredita. Em segundo lugar, as violações de aliasing sobre as quais todos estão falando são sobre acessar um objeto como um objeto diferente, e não simplesmente ter dois objetos com o mesmo endereço. Desde que não haja acesso punitivo , não há problema. Essa foi a ideia original. Mais tarde, a punição de tipo através dos sindicatos foi permitida.
AnT
133

Esta é a regra estrita de alias, encontrada na seção 3.10 do padrão C ++ 03 (outras respostas fornecem uma boa explicação, mas nenhuma forneceu a própria regra):

Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor l diferente de um dos seguintes tipos, o comportamento será indefinido:

  • o tipo dinâmico do objeto,
  • uma versão qualificada para cv do tipo dinâmico do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,
  • um tipo agregado ou de união que inclua um dos tipos acima mencionados entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida),
  • um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
  • a charou unsigned chartipo.

Redação C ++ 11 e C ++ 14 (alterações enfatizadas):

Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor gl diferente de um dos seguintes tipos, o comportamento será indefinido:

  • o tipo dinâmico do objeto,
  • uma versão qualificada para cv do tipo dinâmico do objeto,
  • um tipo semelhante (conforme definido em 4.4) ao tipo dinâmico do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,
  • um tipo de agregação ou união que inclui um dos tipos mencionados acima entre seus elementos ou membros de dados não estáticos (incluindo, recursivamente, um elemento ou membro de dados não estático de uma união subagregada ou contida),
  • um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,
  • a charou unsigned chartipo.

Duas mudanças foram pequenas: glvalue em vez de lvalue e esclarecimento do caso agregado / união.

A terceira alteração oferece uma garantia mais forte (relaxa a forte regra de alias): O novo conceito de tipos semelhantes que agora são seguros para alias.


Também a redação C (C99; ISO / IEC 9899: 1999 6.5 / 7; exatamente a mesma redação é usada na ISO / IEC 9899: 2011 §6.5 ¶7):

Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos 73) ou 88) :

  • um tipo compatível com o tipo efetivo do objeto,
  • uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,
  • um tipo que é o tipo assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do objeto,
  • um tipo agregado ou de união que inclua um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou
  • um tipo de caractere.

73) ou 88) O objetivo desta lista é especificar as circunstâncias nas quais um objeto pode ou não ser aliasado.

Ben Voigt
fonte
7
Ben, como as pessoas geralmente são direcionadas aqui, eu me permiti adicionar a referência ao padrão C também, por uma questão de exaustividade.
Kos
1
Veja a seção C89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf, seção 3.3, que fala sobre isso.
precisa saber é o seguinte
2
Se alguém possui um valor l de um tipo de estrutura, pega o endereço de um membro e o passa para uma função que o usa como um ponteiro para o tipo de membro, isso seria considerado como acessando um objeto do tipo de membro (legal), ou um objeto do tipo de estrutura (proibido)? UMA monte de código assume que é legal para estruturas de acesso de tal forma, e eu acho que um monte de gente ia chiar a uma regra que foi entendido como proibindo tais ações, mas não está claro quais são as regras exatas são. Além disso, uniões e estruturas são tratadas da mesma forma, mas regras sensatas para cada uma devem ser diferentes.
Supercat
2
@ supercat: A maneira como a regra para estruturas é redigida, o acesso real é sempre ao tipo primitivo. Em seguida, o acesso por meio de uma referência ao tipo primitivo é legal porque os tipos correspondem e o acesso por meio de uma referência ao tipo de estrutura que contém é legal porque é especialmente permitido.
Ben Voigt
2
@BenVoigt: Eu não acho que a sequência inicial comum funcione, a menos que os acessos sejam feitos via união. Veja goo.gl/HGOyoK para ver o que o gcc está fazendo. Se o acesso a um lvalue do tipo de união por meio de um lvalue de um tipo de membro (não usar o operador de acesso à união) fosse legal, wow(&u->s1,&u->s2)seria necessário ser legal mesmo quando um ponteiro for usado para modificar ue isso negaria a maioria das otimizações que o regra de aliasing foi projetada para facilitar.
Supercat
81

Nota

Isso foi extraído do meu "O que é a regra estrita de alias e por que nos importamos?" escrever.

O que é aliasing estrito?

Em C e C ++, o aliasing tem a ver com quais tipos de expressão temos permissão para acessar valores armazenados. Em C e C ++, o padrão especifica quais tipos de expressão são permitidos para alias quais tipos. O compilador e o otimizador podem assumir que seguimos estritamente as regras de alias, daí o termo regra estrita de alias . Se tentarmos acessar um valor usando um tipo não permitido, ele será classificado como comportamento indefinido ( UB ). Depois de ter um comportamento indefinido, todas as apostas estão desativadas, os resultados do nosso programa não são mais confiáveis.

Infelizmente, com violações estritas de alias, geralmente obtemos os resultados esperados, deixando a possibilidade de que uma versão futura de um compilador com uma nova otimização decida o código que julgávamos válido. Isso é indesejável e é um objetivo que vale a pena entender as regras estritas de alias e como evitar violá-las.

Para entender mais sobre por que nos importamos, discutiremos questões que surgem ao violar regras estritas de aliasing, punição de tipo, já que técnicas comuns usadas na punição de tipo geralmente violam regras estritas de alias e como digitar trocadilho corretamente.

Exemplos preliminares

Vejamos alguns exemplos, depois podemos falar exatamente sobre o que dizem os padrões, examinar alguns exemplos adicionais e ver como evitar aliases estritos e capturar violações que perdemos. Aqui está um exemplo que não deve surpreender ( exemplo ao vivo ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Temos um int * apontando para a memória ocupada por um int e esse é um alias válido. O otimizador deve assumir que as atribuições por meio de ip podem atualizar o valor ocupado por x .

O próximo exemplo mostra um alias que leva a um comportamento indefinido ( exemplo ao vivo ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

Na função foo, pegamos um int * e um float * , neste exemplo, chamamos foo e configuramos os dois parâmetros para apontar para o mesmo local de memória que neste exemplo contém um int . Observe que o reinterpret_cast está dizendo ao compilador para tratar a expressão como se tivesse o tipo especificado por seu parâmetro de modelo. Nesse caso, estamos dizendo para tratar a expressão & x como se tivesse o tipo float * . Podemos esperar ingenuamente que o resultado do segundo corte seja 0, mas com a otimização ativada usando -O2, tanto gcc quanto clang produzem o seguinte resultado:

0
1

O que pode não ser esperado, mas é perfeitamente válido, pois invocamos um comportamento indefinido. Um flutuador não pode validamente alias um objeto int . Portanto, o otimizador pode assumir a constante 1 armazenada ao remover a referência i, que será o valor de retorno, pois um armazenamento através de f não pode afetar validamente um objeto int . A inserção do código no Compiler Explorer mostra que é exatamente isso que está acontecendo ( exemplo ao vivo ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

O otimizador que usa a Análise de alias baseada em tipo (TBAA) assume que 1 será retornado e move diretamente o valor constante no registro eax, que carrega o valor de retorno. O TBAA usa as regras de idiomas sobre quais tipos têm permissão de alias para otimizar cargas e armazenamentos. Nesse caso, o TBAA sabe que um float não pode alias e int e otimiza a carga de i .

Agora, para o livro de regras

O que exatamente o padrão diz que somos permitidos e não permitidos? O idioma padrão não é simples, portanto, para cada item, tentarei fornecer exemplos de código que demonstram o significado.

O que o padrão C11 diz?

O padrão C11 diz o seguinte na seção 6.5 Expressões, parágrafo 7 :

Um objeto deve ter seu valor armazenado acessado apenas por uma expressão lvalue que possui um dos seguintes tipos: 88) - um tipo compatível com o tipo efetivo do objeto,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- uma versão qualificada de um tipo compatível com o tipo efetivo do objeto,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do objeto,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang tem uma extensão e também que permite atribuir unsigned int * para int * mesmo que eles não são tipos compatíveis.

- um tipo que é o tipo assinado ou não assinado, correspondente a uma versão qualificada do tipo efetivo do objeto,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- um tipo agregado ou de união que inclui um dos tipos mencionados acima entre seus membros (incluindo, recursivamente, um membro de uma união subagregada ou contida), ou

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- um tipo de personagem.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

O que o Draft 17 Draft Standard diz

O rascunho do padrão C ++ 17 na seção [basic.lval], parágrafo 11, diz:

Se um programa tentar acessar o valor armazenado de um objeto por meio de um valor gl gl diferente de um dos seguintes tipos, o comportamento será indefinido: 63 (11.1) - o tipo dinâmico do objeto,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - uma versão qualificada para cv do tipo dinâmico do objeto,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - um tipo semelhante (conforme definido em 7.5) ao tipo dinâmico do objeto,

(11.4) - um tipo que é o tipo assinado ou não assinado correspondente ao tipo dinâmico do objeto,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - um tipo que é o tipo assinado ou não assinado, correspondente a uma versão qualificada para cv do tipo dinâmico do objeto,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - um tipo agregado ou de união que inclui um dos tipos mencionados acima entre seus elementos ou membros de dados não estáticos (incluindo, recursivamente, um elemento ou membro de dados não estático de uma união subagregada ou contida),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - um tipo que é um tipo de classe base (possivelmente qualificado para cv) do tipo dinâmico do objeto,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - um tipo de caractere, caractere não assinado ou std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Vale ressaltar que o caractere assinado não está incluído na lista acima. Essa é uma diferença notável de C, que indica um tipo de caractere .

O que é Type Punning

Chegamos a esse ponto e podemos estar se perguntando: por que queremos usar o apelido? A resposta normalmente é digitar trocadilho , geralmente os métodos usados ​​violam regras estritas de alias.

Às vezes, queremos contornar o sistema de tipos e interpretar um objeto como um tipo diferente. Isso é chamado de punção de tipo , para reinterpretar um segmento de memória como outro tipo. A punção de tipo é útil para tarefas que desejam acessar a representação subjacente de um objeto para visualizar, transportar ou manipular. Áreas típicas que encontramos como punição de tipo sendo usadas são compiladores, serialização, código de rede, etc.

Tradicionalmente, isso é conseguido pegando o endereço do objeto, convertendo-o em um ponteiro do tipo que queremos reinterpretá-lo como e depois acessando o valor, ou seja, usando o alias. Por exemplo:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Como vimos anteriormente, este não é um alias válido, por isso estamos invocando um comportamento indefinido. Mas tradicionalmente os compiladores não tiravam vantagem das regras estritas de alias e esse tipo de código geralmente funcionava; infelizmente, os desenvolvedores se acostumaram a fazer as coisas dessa maneira. Um método alternativo comum para punição de tipo é por meio de uniões, que são válidas em C, mas com comportamento indefinido em C ++ ( veja o exemplo ao vivo ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Isso não é válido em C ++ e alguns consideram que o objetivo das uniões é unicamente para implementar tipos de variantes e consideram que o uso de uniões para punção de tipo é um abuso.

Como digitamos o trocadilho corretamente?

O método padrão para punção de tipo em C e C ++ é memcpy . Isso pode parecer um pouco pesado, mas o otimizador deve reconhecer o uso do memcpy para punções de tipo, otimizá-lo e gerar um registro para registrar a movimentação. Por exemplo, se sabemos que int64_t tem o mesmo tamanho que o dobro :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

podemos usar o memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

Em um nível de otimização suficiente, qualquer compilador moderno decente gera código idêntico ao método reinterpret_cast ou método de união mencionado anteriormente para punção de tipo . Examinando o código gerado, vemos que ele usa apenas registre mov (exemplo ao vivo do Compiler Explorer ).

C ++ 20 e bit_cast

No C ++ 20, podemos obter bit_cast ( implementação disponível no link da proposta ), que fornece uma maneira simples e segura de digitar trocadilhos, além de ser utilizável em um contexto constexpr.

A seguir, é apresentado um exemplo de como usar bit_cast para digitar pun um int não assinado a flutuar ( veja ao vivo ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

No caso em que os tipos Para e De não têm o mesmo tamanho, é necessário usar uma estrutura intermediária15. Usaremos uma estrutura que contém uma matriz de caracteres sizeof (int não assinada) ( assume int não assinada de 4 bytes ) como o tipo From e int sem assinatura como o tipo To . :

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

É lamentável precisarmos desse tipo intermediário, mas essa é a restrição atual do bit_cast .

Capturando violações estritas de aliasing

Não temos muitas ferramentas boas para capturar aliasing estrito em C ++, as ferramentas que possuímos capturam alguns casos de violações estritas de aliasing e alguns casos de cargas e armazenamentos desalinhados.

O gcc usando o sinalizador -fstrict-aliasing e -Wstrict-aliasing pode capturar alguns casos, embora não sem falsos positivos / negativos. Por exemplo, os seguintes casos gerarão um aviso no gcc ( veja ao vivo ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

embora não capte este caso adicional ( veja ao vivo ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Embora o clang permita esses sinalizadores, aparentemente não implementa os avisos.

Outra ferramenta que temos à nossa disposição é o ASan, que pode capturar cargas e lojas desalinhadas. Embora essas não sejam violações estritamente diretas de alias, elas são um resultado comum de violações estritas de alias. Por exemplo, os seguintes casos gerarão erros de tempo de execução quando criados com clang usando -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

A última ferramenta que vou recomendar é específica para C ++ e não é estritamente uma ferramenta, mas uma prática de codificação, não permite transmissões no estilo C. Tanto o gcc quanto o clang produzirão um diagnóstico para os elencos no estilo C usando o elenco -Wold-style . Isso forçará qualquer trocadilho de tipo indefinido a usar reinterpret_cast; em geral, reinterpret_cast deve ser um sinalizador para uma revisão mais detalhada do código. Também é mais fácil pesquisar em sua base de códigos por reinterpret_cast para realizar uma auditoria.

Para C, já temos todas as ferramentas abordadas e também temos o tis-intérprete, um analisador estático que analisa exaustivamente um programa para um grande subconjunto da linguagem C. Dadas as versões C do exemplo anterior, em que o uso de -fstrict-aliasing perde um caso ( veja ao vivo )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter é capaz de capturar todos os três, o exemplo a seguir chama tis-kernal como tis-intérprete (a saída é editada por questões de brevidade):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Finalmente, há o TySan, que está atualmente em desenvolvimento. Este desinfetante adiciona informações de verificação de tipo em um segmento de memória de sombra e verifica os acessos para ver se eles violam as regras de alias. A ferramenta deve ser capaz de detectar todas as violações de aliasing, mas pode ter uma grande sobrecarga de tempo de execução.

Shafik Yaghmour
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Bhargav Rao
3
Se eu pudesse, +10, bem escrito e explicado, também de ambos os lados, escritores de compiladores e programadores ... a única crítica: seria bom ter exemplos contrários acima, ver o que é proibido pelo padrão, não é óbvio tipo :-)
Gabriel
2
Resposta muito boa. Lamento apenas que os exemplos iniciais sejam dados em C ++, o que dificulta o acompanhamento de pessoas como eu que apenas conhecem ou se preocupam com C e não têm idéia do que reinterpret_castpodem fazer ou o que coutpode significar. (Está tudo bem mencionar C ++, mas a questão original sobre C e IIUC estes exemplos poderia apenas como validamente ser escrito em C.)
Gro-Tsen
Com relação à punição de tipo: então, se eu escrever uma matriz de algum tipo X no arquivo, depois ler esse arquivo na memória apontada com void *, lançarei esse ponteiro para o tipo real dos dados para usá-lo - isso é comportamento indefinido?
Michael IV
44

O aliasing estrito não se refere apenas a ponteiros, mas também a referências; escrevi um artigo sobre o wiki do desenvolvedor do impulso e foi tão bem recebido que o transformei em uma página no meu site de consultoria. Explica completamente o que é, por que confunde tanto as pessoas e o que fazer sobre isso. White paper estrito sobre alias . Em particular, explica por que as uniões são um comportamento arriscado para C ++ e por que usar memcpy é a única correção portátil em C e C ++. Espero que isso seja útil.

phorgan1
fonte
3
" Aliasing estrito não se refere apenas a ponteiros, mas também a referências " Na verdade, refere-se a lvalues . " usar o memcpy é a única correção portátil " Hear!
precisa
5
Bom papel. Minha opinião: (1) esse problema de apelido é uma reação exagerada a uma programação ruim - tentando proteger o programador ruim de seus maus hábitos. Se o programador tem bons hábitos, esse alias é apenas um incômodo e as verificações podem ser desativadas com segurança. (2) A otimização do lado do compilador deve ser feita apenas em casos conhecidos e, quando houver dúvida, seguir rigorosamente o código-fonte; forçar o programador a escrever um código para atender às idiossincrasias do compilador é, simplesmente, errado. Pior ainda é fazer parte do padrão.
Slashmais 02/02
4
@slashmais (1) " é uma reação exagerada a uma programação ruim " Bobagem. É uma rejeição dos maus hábitos. Faça isso? Você paga o preço: não há garantia para você! (2) Casos conhecidos? Quais? A regra estrita de alias deve ser "bem conhecida"!
curiousguy
5
@curiousguy: Depois de esclarecer alguns pontos de confusão, fica claro que a linguagem C com as regras de aliasing torna impossível para os programas implementar pools de memória independentes de tipo. Alguns tipos de programa podem sobreviver com malloc / free, mas outros precisam de uma lógica de gerenciamento de memória melhor adaptada às tarefas em questão. Eu me pergunto por que a lógica do C89 usou um exemplo tão ruim da razão da regra de aliasing, já que o exemplo deles faz parecer que a regra não apresentará nenhuma dificuldade maior na execução de qualquer tarefa razoável.
Supercat 21/15 /
5
@curiousguy, a maioria dos pacotes de compiladores disponíveis inclui o -fstrict-aliasing como padrão no -O3 e esse contrato oculto é imposto aos usuários que nunca ouviram falar do TBAA e escreveram códigos como um programador de sistema. Não pretendo parecer falso para os programadores de sistema, mas esse tipo de otimização deve ser deixado de fora da opção padrão de -O3 e deve ser uma otimização de opção para aqueles que sabem o que é TBAA. Não é divertido olhar para o 'bug' do compilador que acaba sendo um código de usuário que viola o TBAA, especialmente rastreando a violação no nível de origem no código do usuário.
kchoi
34

Como adendo ao que Doug T. já escreveu, aqui está um caso de teste simples que provavelmente o aciona com o gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Compile com gcc -O2 -o check check.c. Normalmente (com a maioria das versões do gcc que eu tentei), isso gera um "problema estrito de alias", porque o compilador assume que "h" não pode ser o mesmo endereço que "k" na função "check". Por isso, o compilador otimiza a if (*h == 5)distância e sempre chama o printf.

Para quem está interessado aqui é o código do assembler x64, produzido pelo gcc 4.6.3, rodando no ubuntu 12.04.2 para x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Portanto, a condição if desapareceu completamente do código do assembler.

Ingo Blackman
fonte
se você adicionar um segundo curto * j para marcar () e usá-lo (* j = 7), a otimização desaparecerá, pois o ggc não o fará se hej não apontarem para o mesmo valor. Sim, a otimização é realmente inteligente.
Lhardy Philippe
2
Para tornar as coisas mais divertidas, use ponteiros para tipos que não são compatíveis, mas têm o mesmo tamanho e representação (em alguns sistemas que são verdadeiros, por exemplo, long long*e int64_t*). Pode-se esperar que um compilador sadio reconheça isso long long*e int64_t*possa acessar o mesmo armazenamento se eles forem armazenados de forma idêntica, mas esse tratamento não está mais na moda.
Supercat 22/03
Grr ... x64 é uma convenção da Microsoft. Use amd64 ou x86_64.
SS Anne
Grr ... x64 é uma convenção da Microsoft. Use amd64 ou x86_64.
SS Anne
17

A punção de tipo por meio de projeções de ponteiro (ao contrário de usar uma união) é um exemplo importante de quebrar o aliasing estrito.

Chris Jester-Young
fonte
1
Veja minha resposta aqui para obter as citações relevantes, especialmente as notas de rodapé, mas a punição através de sindicatos sempre foi permitida em C, embora tenha sido mal formulada no início. Você quer esclarecer sua resposta.
Shafik Yaghmour
@ ShafikYaghmour: O C89 claramente permitiu que os implementadores selecionassem os casos em que eles reconheceriam ou não utilmente a punição de tipo através dos sindicatos. Uma implementação pode, por exemplo, especificar que uma gravação em um tipo seguida por uma leitura de outro seja reconhecida como punção de tipo, se o programador fizer um dos seguintes procedimentos entre a gravação e a leitura : (1) avalie um valor l contendo o tipo de união [tomar o endereço de um membro se qualificaria, se feito no ponto certo na sequência]; (2) converte um ponteiro para um tipo em um ponteiro para o outro e acesse via esse ptr.
Supercat 22/03
@ ShafikYaghmour: Uma implementação também pode especificar, por exemplo, que a punção de tipo entre valores inteiros e de ponto flutuante funcionaria apenas de forma confiável se o código executasse uma fpsync()diretiva entre escrever como fp e ler como int ou vice-versa [em implementações com pipelines e caches inteiros e FPU separados , essa diretiva pode ser cara, mas não tão cara quanto o compilador executar essa sincronização em todos os acessos à união]. Ou uma implementação pode especificar que o valor resultante nunca será utilizável, exceto em circunstâncias usando Sequências Iniciais Comuns.
Supercat 22/03
@ ShafikYaghmour: No C89, as implementações podiam proibir a maioria das formas de punição de tipo, inclusive via uniões, mas a equivalência entre ponteiros para uniões e ponteiros para seus membros implicava que a punção de tipo era permitida em implementações que não a proibiam expressamente .
Supercat
17

De acordo com a lógica C89, os autores da Norma não queriam exigir que os compiladores recebessem código como:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

deve ser necessário recarregar o valor de x entre a atribuição e a declaração de retorno, a fim de permitir a possibilidade que ppossa apontar para x, e a atribuição para, *pconsequentemente, alterar o valor de x. A noção de que um compilador deve ter o direito de presumir que não haverá apelidos em situações como a acima não é controversa.

Infelizmente, os autores do C89 escreveram sua regra de uma maneira que, se lida literalmente, faria até mesmo a seguinte função chamar Undefined Behavior:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

porque usa um lvalue do tipo int para acessar um objeto do tipo struct Se intnão está entre os tipos que podem ser usados ​​acessando a struct S. Como seria absurdo tratar todo uso de membros de estruturas e uniões sem caráter de caractere como comportamento indefinido, quase todo mundo reconhece que há pelo menos algumas circunstâncias em que um valor l de um tipo pode ser usado para acessar um objeto de outro tipo . Infelizmente, o Comitê de Padrões C não conseguiu definir quais são essas circunstâncias.

Grande parte do problema é resultado do Relatório de Defeitos # 028, que perguntou sobre o comportamento de um programa como:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

O Relatório de Defeitos # 28 declara que o programa chama Comportamento indefinido porque a ação de escrever um membro da união do tipo "double" e ler um do tipo "int" chama o comportamento definido pela implementação. Esse raciocínio é absurdo, mas forma a base para as regras do Tipo Efetivo que desnecessariamente complicam a linguagem sem fazer nada para resolver o problema original.

A melhor maneira de resolver o problema original provavelmente seria tratar a nota de rodapé sobre o objetivo da regra como se fosse normativa e tornar a regra inaplicável, exceto nos casos que realmente envolvem acessos conflitantes usando aliases. Dado algo como:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Não há conflito interno inc_intporque todos os acessos ao armazenamento acessado *psão feitos com um valor de tipo inte não há conflito testporque pé derivado visivelmente de um struct Se, na próxima vez em que sfor usado, todos os acessos ao armazenamento que serão feitos atravésp já terá acontecido.

Se o código fosse ligeiramente alterado ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Aqui, existe um conflito de aliasing entre pe o acesso à s.xlinha marcada, porque nesse ponto da execução existe outra referência que será usada para acessar o mesmo armazenamento .

Se o Relatório de Defeitos 028 dissesse que o exemplo original invocava o UB por causa da sobreposição entre a criação e o uso dos dois ponteiros, isso tornaria as coisas muito mais claras sem a necessidade de adicionar "Tipos eficazes" ou outra complexidade.

supercat
fonte
Bem dito, seria interessante ler uma proposta do tipo que era mais ou menos "o que o comitê de normas poderia ter feito" que atingisse seus objetivos sem introduzir tanta complexidade.
JRH
1
@jrh: Eu acho que seria bem simples. Reconheça que 1. Para que o alias ocorra durante uma execução específica de uma função ou loop, dois indicadores ou valores diferentes devem ser usados durante a execução para endereçar o mesmo armazenamento em fashon conflitante; 2. Reconhecer que, em contextos em que um ponteiro ou valor é derivado visivelmente de outro, um acesso ao segundo é um acesso ao primeiro; 3. Reconheça que a regra não se aplica a casos que realmente não envolvem alias.
Supercat 31/07
1
As circunstâncias exatas em que um compilador reconhece um lvalue derivado recentemente podem ser um problema de qualidade de implementação, mas qualquer compilador remotamente decente deve ser capaz de reconhecer formulários que o gcc e o clang ignoram deliberadamente.
Supercat 31/07
11

Depois de ler muitas das respostas, sinto a necessidade de adicionar algo:

O aliasing estrito (que descreverei um pouco) é importante porque :

  1. O acesso à memória pode ser caro (desempenho), e é por isso que os dados são manipulados nos registros da CPU antes de serem gravados de volta na memória física.

  2. Se dados em dois registros diferentes de CPU forem gravados no mesmo espaço de memória, não podemos prever quais dados "sobreviverão" quando codificarmos em C.

    Na montagem, onde codificamos o carregamento e descarregamento dos registros da CPU manualmente, saberemos quais dados permanecem intactos. Mas C (felizmente) abstrai esse detalhe.

Como dois ponteiros podem apontar para o mesmo local na memória, isso pode resultar em código complexo que lida com possíveis colisões .

Esse código extra é lento e prejudica o desempenho, pois realiza operações extras de leitura / gravação de memória, que são mais lentas e (possivelmente) desnecessárias.

A regra de aliasing estrita nos permite evitar código de máquina redundante nos casos em que deve ser seguro assumir que dois ponteiros não apontam para o mesmo bloco de memória (consulte também orestrict palavra chave).

O aliasing estrito afirma que é seguro assumir que ponteiros para tipos diferentes apontam para locais diferentes na memória.

Se um compilador perceber que dois ponteiros apontam para tipos diferentes (por exemplo, an int *e a float *), ele assumirá que o endereço de memória é diferente e não protegerá contra colisões de endereços de memória, resultando em um código de máquina mais rápido.

Por exemplo :

Vamos assumir a seguinte função:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Para lidar com o caso em que a == b(ambos os ponteiros apontam para a mesma memória), precisamos solicitar e testar a maneira como carregamos dados da memória nos registros da CPU, para que o código possa acabar assim:

  1. carregar ae bda memória.

  2. adicionar aa b.

  3. salve b e recarregue a .

    (salve do registro da CPU na memória e carregue da memória no registro da CPU).

  4. adicionar ba a.

  5. salve a(do registro da CPU) na memória.

A etapa 3 é muito lenta porque precisa acessar a memória física. No entanto, é necessário proteger contra instâncias em que ae bapontar para o mesmo endereço de memória.

O aliasing estrito nos permitiria evitar isso, informando ao compilador que esses endereços de memória são distintamente diferentes (o que, nesse caso, permitirá uma otimização ainda maior que não pode ser realizada se os ponteiros compartilharem um endereço de memória).

  1. Isso pode ser informado ao compilador de duas maneiras, usando tipos diferentes para apontar. ou seja:

    void merge_two_numbers(int *a, long *b) {...}
  2. Usando a restrictpalavra - chave ou seja:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Agora, satisfazendo a regra Strict Aliasing, a etapa 3 pode ser evitada e o código será executado significativamente mais rápido.

De fato, adicionando a restrictpalavra - chave, toda a função pode ser otimizada para:

  1. carregar ae bda memória.

  2. adicionar aa b.

  3. salvar o resultado para ae para b.

Essa otimização não poderia ter sido feita antes, devido à possível colisão (onde ae bseria triplicada em vez de dobrada).

Myst
fonte
com a palavra-chave restrita, na etapa 3, não deve salvar o resultado apenas em 'b'? Parece que o resultado do somatório também será armazenado em 'a'. 'B' precisa ser recarregado novamente?
NeilB
1
@ NeilB - Sim, você está certo. Estamos apenas salvando b(não recarregando) e recarregando a. Espero que esteja mais claro agora.
Myst
O aliasing baseado em tipo pode ter oferecido alguns benefícios antes restrict, mas eu acho que o último seria, na maioria das circunstâncias, mais eficaz, e o relaxamento de algumas restrições registerpermitiria preencher alguns dos casos em restrictque não ajudaria. Não tenho certeza de que tenha sido "importante" tratar o Padrão como uma descrição completa de todos os casos em que os programadores devem esperar que os compiladores reconheçam evidências de alias, em vez de apenas descrever locais onde os compiladores devem presumir alias, mesmo quando não exista nenhuma evidência específica .
Supercat 23/01
Observe que, embora o carregamento da RAM principal seja muito lento (e possa travar o núcleo da CPU por um longo tempo se as operações a seguir dependerem do resultado), o carregamento do cache L1 é muito rápido e a gravação em uma linha de cache que estava gravando recentemente pelo mesmo núcleo. Portanto, todos, exceto a primeira leitura ou gravação em um endereço, geralmente são razoavelmente rápidos: a diferença entre o acesso reg / mem addr é menor do que a diferença entre o mem addr em cache / não em cache.
arquivo
@curiousguy - embora você esteja correto, "rápido" neste caso é relativo. O cache L1 provavelmente ainda é uma ordem de magnitude mais lenta que a CPU (acho que mais de 10 vezes mais lenta). Além disso, a restrictpalavra - chave minimiza não apenas a velocidade das operações, mas também o número delas, o que pode ser significativo ... quero dizer, afinal, a operação mais rápida não é nenhuma operação :)
Myst
6

O aliasing estrito não está permitindo diferentes tipos de ponteiros para os mesmos dados.

Este artigo deve ajudar você a entender o problema em detalhes.

Jason Dagit
fonte
4
Você pode alternar entre referências e entre uma referência e um ponteiro. Veja meu tutorial dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1
4
É permitido ter diferentes tipos de ponteiros para os mesmos dados. O aliasing estrito é quando o mesmo local de memória é gravado através de um tipo de ponteiro e lido através de outro. Além disso, alguns tipos diferentes são permitidos (por exemplo, intuma estrutura que contém um int).
MM
-3

Tecnicamente, em C ++, a regra estrita de aliasing provavelmente nunca é aplicável.

Observe a definição de indireção ( * operador ):

O operador unary * executa a indireção: a expressão à qual é aplicada deve ser um ponteiro para um tipo de objeto ou um ponteiro para um tipo de função e o resultado é um valor l que se refere ao objeto ou função para a qual a expressão aponta .

Também da definição de glvalue

Um glvalue é uma expressão cuja avaliação determina a identidade de um objeto, (... snip)

Portanto, em qualquer rastreamento de programa bem definido, um glvalue se refere a um objeto. Portanto, a chamada regra estrita de aliasing nunca se aplica. Pode não ser o que os designers queriam.

curiousguy
fonte
4
O Padrão C usa o termo "objeto" para se referir a vários conceitos diferentes. Entre eles, uma sequência de bytes que são alocados exclusivamente para algum propósito, uma referência não necessariamente exclusiva a uma sequência de bytes para / da qual um valor de um tipo específico possa ser gravado ou lido, ou uma referência que realmente tenha foi ou será acessado em algum contexto. Eu não acho que exista uma maneira sensata de definir o termo "Objeto" que seja consistente com toda a maneira como o Padrão o usa.
Supercat
@supercat Incorreto. Apesar da sua imaginação, é realmente bastante consistente. Na ISO C, é definida como "região de armazenamento de dados no ambiente de execução, cujo conteúdo pode representar valores". No ISO C ++, há uma definição semelhante. Seu comentário é ainda mais irrelevante que a resposta, porque tudo o que você mencionou são formas de representação para referenciar o conteúdo de objetos , enquanto a resposta ilustra o conceito C ++ (glvalue) de um tipo de expressão que se relaciona fortemente à identidade dos objetos. E todas as regras de alias são basicamente relevantes para a identidade, mas não para o conteúdo.
FrankHB 13/03
1
@FrankHB: Se alguém declara int foo;, o que é acessado pela expressão lvalue *(char*)&foo? Isso é um objeto do tipo char? Esse objeto passa a existir ao mesmo tempo que foo? Escrever para fooalterar o valor armazenado desse objeto do tipo acima mencionado char? Em caso afirmativo, existe alguma regra que permita que o valor armazenado de um objeto do tipo charseja acessado usando um lvalue do tipo int?
supercat 13/03
@FrankHB: Na ausência do 6.5p7, pode-se dizer simplesmente que toda região de armazenamento contém simultaneamente todos os objetos de todos os tipos que poderiam caber nessa região de armazenamento e que acessar essa região de armazenamento simultaneamente acessa todos eles. Interpretar dessa maneira, o uso do termo "objeto" em 6.5p7, no entanto, proibiria fazer qualquer coisa com valores não do tipo caractere, o que claramente seria um resultado absurdo e derrotaria totalmente o objetivo da regra. Além disso, o conceito de "objeto" usado em todos os lugares que não 6.5p6 tem um tipo de tempo de compilação estática, mas ...
supercat
1
sizeof (int) é 4, a declaração int i;cria quatro objetos de cada tipo de caractere in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` e i. Finalmente, não há nada no Standard que permita que mesmo um volatileponteiro qualificado acesse registros de hardware que não atendem à definição de "objeto".
supercat 15/03