O que é a strict aliasing rule e por que seu código C++ pode quebrar de repente?
\nVocê já escreveu um código C++ aparentemente correto, e o compilador, em níveis altos de otimização (-O2 ou -O3), o transformou em algo que não funciona? Bem-vindo ao mundo do aliasing estrito (strict aliasing rule). Esta é uma das regras mais traiçoeiras da linguagem, que pode transformar a depuração em uma verdadeira aventura. No PythonLib, adoramos explicar tópicos complexos de forma simples, e o artigo de hoje não é exceção.
Vamos imaginar: você tem dois ponteiros de tipos diferentes apontando para a mesma área de memória. O compilador, seguindo a strict aliasing rule, tem todo o direito de decidir que eles não podem se referir ao mesmo objeto. E se você violar essa regra — espere comportamento indefinido (UB). O programa pode funcionar, pode travar ou pode mostrar resultados incorretos. Parece assustador? Vamos entender.
\n\nA essência da regra: por que o compilador é tão "rigoroso"?
\nA strict aliasing rule em C++ (e C) afirma: acessar um objeto através de um ponteiro ou referência de um tipo incompatível é UB. As únicas exceções são para char, unsigned char e std::byte. Por que isso é necessário? A resposta é simples: otimização. Se o compilador tem certeza de que dois ponteiros de tipos diferentes não podem apontar para a mesma memória, ele pode reordenar operações, armazenar valores em cache nos registradores e otimizar o código agressivamente. Sem essa regra, o desempenho de muitos programas cairia drasticamente.
Mas há um lado negativo: desenvolvedores que tentam fazer type punning (interpretar os mesmos bytes como tipos diferentes) frequentemente encontram problemas. O exemplo clássico é tentar verificar rapidamente se um número de ponto flutuante é NaN, usando reinterpret_cast para int. O compilador pode "quebrar" esse código.
\n\nComo é uma violação do strict aliasing? Exemplos de código
\nVamos considerar o exemplo mais popular, que aparece em vários Stack Overflow:
\n#include <iostream>\n#include <cstdint>\n\nint main() {\n float f = 3.14f;\n // Violação do strict aliasing: lendo float como int\n int* pi = reinterpret_cast<int*>(&f);\n std::cout << *pi; // UB!\n return 0;\n}\n\nÀ primeira vista, é um truque comum para visualizar a representação binária de um float. Mas o padrão C++ diz: float e int são tipos incompatíveis. O compilador pode otimizar esse código de forma que você obtenha lixo. Com otimizações ativadas, o GCC, por exemplo, pode simplesmente retornar algum valor conhecido ou até remover o código.
Outro exemplo é trabalhar com arrays de bytes:
\nstruct Point { int x, y; };\nchar buffer[sizeof(Point)];\n// Preenche buffer...\nPoint* p = reinterpret_cast<Point*>(buffer);\np->x = 42; // UB, se buffer não foi criado como Point\n\nAqui a violação ocorre porque buffer é um array de char, e nós o acessamos como Point. Embora char* possa apontar para qualquer objeto, o inverso não é verdadeiro: Point* não pode apontar para char[], a menos que a memória tenha sido alocada como Point.
Comparação de abordagens: como contornar o strict aliasing com segurança?
\nExistem várias maneiras legais de realizar type punning em C++. Vamos comparar três abordagens principais: memcpy, union e std::bit_cast (C++20). Cada uma tem seus prós e contras.
1. memcpy — o bom e velho método
\nO método mais seguro e portátil é copiar os bytes usando memcpy. Os compiladores otimizam muito bem essa chamada, muitas vezes transformando-a em um simples carregamento de registrador, sem custo adicional:
#inc