(OU digitação de quibble, comportamento vago e alinhamento, oh meu Deus!)Olá pessoal, em poucas semanas estamos lançando um novo tópico no curso
"C ++ Developer" . Este evento será dedicado ao material de hoje.
O que é aliasing estrito? Primeiro, descrevemos o que é alias e depois descobrimos para que serve o rigor.
Em C e C ++, o alias está relacionado a quais tipos de expressões temos permissão para acessar valores armazenados. Em C e C ++, o padrão define quais expressões de nomenclatura são válidas para quais tipos. O compilador e o otimizador podem assumir que seguimos estritamente as regras do alias, daí o termo - a regra do estrito alias (regra estrita do alias). Se tentarmos acessar um valor usando um tipo inválido, ele será classificado como comportamento indefinido (UB). Quando temos um comportamento incerto, todas as apostas são feitas, e os resultados do nosso programa deixam de ser confiáveis.
Infelizmente, com violações estritas de alias, geralmente obtemos os resultados esperados, deixando a possibilidade de que uma versão futura do compilador com nova otimização viole o código que consideramos válido. Isso é indesejável, vale a pena entender as regras estritas do alias e evitar violá-las.

Para entender melhor por que devemos nos preocupar com isso, discutiremos os problemas que surgem ao violar as regras estritas de alias, digite punição, como é frequentemente usado em regras estritas de alias, além de como criar corretamente um trocadilho, juntamente com alguma ajuda possível com o C ++ 20 para simplificar o trocadilho e reduzir a chance de erros. Resumiremos a discussão considerando alguns métodos para detectar violações de regras estritas de apelido.
Exemplos preliminaresVamos dar uma olhada em alguns exemplos e, em seguida, discutir o que exatamente é declarado no (s) padrão (s), considerar alguns exemplos adicionais e ver como evitar aliases estritos e identificar violações que perdemos. Aqui está
um exemplo que não deve surpreendê-lo:
int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n";
Temos int * apontando para a memória ocupada por int, e isso é um alias válido. O otimizador deve assumir que as atribuições via ip podem atualizar o valor ocupado por x.
O
exemplo a seguir mostra alias, o que leva a um comportamento indefinido:
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 int * e 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
reinterpret_cast diz ao compilador para tratar a expressão como se ela tivesse o tipo especificado pelo parâmetro de modelo. Nesse caso, dizemos a ele para processar a expressão & x como se fosse do tipo float *. Ingenuamente, podemos esperar que o resultado do segundo corte seja 0, mas quando a otimização estiver ativada usando -O2 e gcc, e clang obterá o seguinte resultado:
0 0
1
O que pode ser inesperado, mas completamente correto, pois causamos um comportamento indefinido. Float não pode ser um alias válido de um objeto int. Portanto, o otimizador pode assumir que a constante 1 armazenada durante a desreferenciação i será o valor de retorno, pois salvar através de f não pode afetar corretamente o objeto int. Conectar o código no Compiler Explorer mostra que é exatamente isso que acontece (
exemplo ):
foo(float*, int*):
Um otimizador usando a Análise de alias baseada em tipo (TBAA) assume que 1 será retornado e move diretamente o valor constante para o registro eax, que armazena o valor de retorno. O TBAA usa regras de idioma sobre quais tipos são permitidos para aliasing para otimizar o carregamento e o armazenamento. Nesse caso, o TBAA sabe que float não pode ser um alias de int e otimiza o carregamento até a morte.
Agora para a referênciaO que exatamente o padrão diz sobre o que nos é permitido ou não? A linguagem padrão não é simples, portanto, para cada elemento, tentarei fornecer exemplos de código que demonstram significado.
O que o padrão C11 diz?
O padrão C11 diz o seguinte na seção "6.5 Expressões" do parágrafo 7:
O objeto deve ter seu próprio valor armazenado, cujo acesso é realizado apenas com a ajuda da 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 lvalue- int, int
- uma versão qualificada de um tipo compatível com o tipo de objeto atual,
int x = 1; const int *p = &x; printf("%d\n", *p); // * p lvalue- const int, int
- um tipo com ou sem um sinal correspondente a um tipo de objeto qualificado,
int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- unsigned int,
Consulte a nota de rodapé 12 para obter a extensão gcc / clang , que permite atribuir int * int * não assinado, mesmo que não sejam tipos compatíveis.
- um tipo com ou sem sinal correspondente a uma versão qualificada do tipo de objeto atual,
int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p lvalue- const unsigned int, ,
- um tipo agregado ou combinado que inclui um dos tipos acima entre seus membros (incluindo, recursivamente, um membro de uma associação subagregada ou contida), ou
struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo - , int , *ip // foo f; foobar( &f, &f.x );
- tipo de caractere.
int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p lvalue- char, . // - .
O que o C ++ 17 Draft Standard dizO padrão de projeto do C ++ 17 na seção 11 [basic.lval] declara: se um programa tentar acessar um valor armazenado de um objeto por meio de um glvalue diferente de um dos seguintes tipos, o comportamento será indefinido: 63 (11.1) é um tipo de objeto dinâmico,
void *p = malloc( sizeof(int) ); // , int *ip = new (p) int{0}; // placement new int std::cout << *ip << "\n"; // * ip glvalue- int,
(11.2) - versão qualificada para cv (cv - const e volátil) do tipo dinâmico de um objeto,
int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip glvalue const int, cv- x
(11.3) - um tipo semelhante (conforme definido em 7.5) ao tipo dinâmico de um objeto,
//
(11.4) - um tipo com ou sem sinal correspondente ao tipo dinâmico de um objeto,
// si ui ,
// godbolt (https://godbolt.org/g/KowGXB) , .
signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; }
(11.5) - um tipo com ou sem sinal, correspondente à versão qualificada para cv do tipo dinâmico de um objeto,
signed int foo( const signed int &si1, int &si2); // ,
(11.6) - um tipo agregado ou combinado que inclui um dos tipos acima entre seus elementos ou elementos de dados não estáticos (incluindo, recursivamente, um elemento ou elemento de dados não estático de uma associação subagregada ou contendo agregados),
struct foo { int x; };
// Compiler Explorer (https://godbolt.org/g/z2wJTC)
int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx );
(11.7) - um tipo que é (possivelmente qualificado para cv) um tipo de classe base de um tipo de objeto dinâmico,
struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; }
(11.8) - digite char, char 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 glvalue- std::byte, uint32_t }
Vale ressaltar que o
signed char
não
signed char
incluído na lista acima. Essa é uma diferença notável de C, que fala sobre o tipo de caractere.
Diferenças sutisPortanto, embora possamos ver que C e C ++ dizem coisas semelhantes sobre alias, existem algumas diferenças que devemos estar cientes. C ++ não possui um conceito C de um tipo
válido ou
compatível e C não possui um conceito C ++ de um tipo
dinâmico ou similar. Embora ambos tenham expressões lvalue e rvalue, o C ++ também possui expressões glvalue, prvalue e xvalue. Essas diferenças estão amplamente fora do escopo deste artigo, mas um exemplo interessante é como criar um objeto a partir da memória usada pelo malloc. Em C, podemos definir um tipo válido, por exemplo, gravando na memória via lvalue ou memcpy.
// C, C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); // *p - float C // float *fp = p; *fp = 1.0f; // *p - float C
Nenhum desses métodos é suficiente em C ++, o que requer a colocação de novos:
float *fp = new (p) float{1.0f} ; // *p float
Os tipos de caracteres int8_t e uint8_t?Teoricamente, nem int8_t nem uint8_t devem ser do tipo char, mas na prática eles são implementados dessa maneira. Isso é importante porque se eles são realmente tipos de caracteres, também são aliases como tipos de caracteres. Se você não está ciente disso, isso pode
levar a uma degradação inesperada do desempenho . Vemos que
glibc typedef
int8_t
e
uint8_t
para
signed char
e
unsigned char
respectivamente.
Isso seria difícil de mudar, pois para C ++ seria uma lacuna ABI. Isso alteraria a distorção do nome e quebraria qualquer API usando qualquer um desses tipos em sua interface.
O fim da primeira parte. E falaremos sobre o trocadilho de digitação e alinhamento em alguns dias.
Escreva seus comentários e não perca o
seminário on-line aberto , que será realizado em 6 de março pelo chefe de desenvolvimento de tecnologia da Rambler & Co,
Dmitry Shebordaev .