O padrão afirma claramente que tentar acessar um objeto const por meio de um ponteiro para um tipo não const desse objeto causará Comportamento Indefinido. O mesmo se aplica a objetos qualificados com a palavra-chave volátil e ponteiros para tais objetos.
Mas o que significa acesso? Por exemplo, é uma sequência arbitrariamente longa de precasts de ponteiros contendo um cast "incorreto", mas finalmente conversível para o tipo correto, uma expressão
int const i = 0;
int const *p = &i; // ok
int const *t = (int const *)(float *)(int *)&i; // is this OK or UB?
Sempre fui atormentado por esse ponto de acordo com a rigidez do padrão da linguagem C. Em particular, tenho muitos dos seguintes casos especiais de passagem de argumentos para funções, onde eles devem ser explicitamente convertidos:
void send(unsigned int const buf[], unsigned int size);
typedef struct {
unsigned int hdr;
float temp;
unsigned int data[5];
} Str;
void func(void) {
Str const s = {
0, 1.5f, {0, 1, 2, 3, 4}
};
...
send((unsigned int const *)&s, sizeof(s));
}
De acordo com as regras do padrão, posso converter com segurança um ponteiro para uma estrutura em um ponteiro para seu primeiro elemento (com o tipo apropriado), mas nada é dito sobre obrigações adicionais com qualificadores (ou eu não vi). Embora eu entenda formalmente que é correto deixar const em uma conversão explícita, isso geralmente confunde muito o código, e eu gostaria de omitir o uso de const nesta expressão. Quero ter certeza de que chamar
send((unsigned int *)&s, sizeof(s));
é tão seguro quanto ligar
send((unsigned int const *)&s, sizeof(s));
já que o protótipo da função send() forçará explicitamente o compilador a converter implicitamente (unsigned int *) para (unsigned int const *) para mim.
Mas se qualquer perda intermediária do qualificador const em conversões de tipo (em conversões de tipo), apesar do fato de que até mesmo o tipo final terá o qualificador correto (uma conversão do formato (type const *) -> (type *) -> ... -> (type const *)), for proibida pelo padrão, então uma chamada no estilo de send((unsigned int *)&s, sizeof(s)); será incorreta.
Por favor, confirme ou negue os pontos do padrão C.
C 2024 3.1 define acesso :
(Versões anteriores do padrão diziam “ ler ou modificar”.)
A conversão de ponteiros é apenas uma operação em valores e não acessa os objetos apontados.
Elas envolvem regras sobre conversões de ponteiros, o que não acredito ser o cerne da sua pergunta. Conforme C 2024 6.3.3.3, conversões entre ponteiros para tipos de objeto são geralmente definidas desde que o alinhamento seja adequado para o tipo de destino.
&i
é necessariamente alinhado adequadamente para umint
, porque foi definido comint const i
, então as conversões paraint *
eint const *
são definidas. A conversão parafloat *
é definida sei
for alinhado adequadamente para umfloat
. (Umint
é sempre alinhado adequadamente parafloat
na maioria das implementações comuns de C.)Dado alinhamento satisfatório, a conversão para um ponteiro diferente para um tipo de objeto e de volta para o tipo original é garantida para produzir um valor igual ao original. Cadeias de conversões não são explicitamente especificadas, mas pode-se presumir razoavelmente que restauram o valor original.
Não diz isso. Como vemos acima, “acesso” inclui leitura, e ler um objeto definido com
const
é definido. O que o padrão diz que é indefinido a esse respeito é, por 6.7.4.1, modificar um objeto const-qualificado:Observe também que “comportamento indefinido” não é um substantivo próprio e não deve ser escrito com letra maiúscula.
Ambos são equivalentes em termos do que o padrão C especifica para execução em sua máquina abstrata; dois programas que diferem apenas neste código terão o mesmo comportamento de execução (exceto fatores de composição externos a isso, como alguém examinando o código-fonte e inserindo entradas diferentes em programas diferentes).
No entanto, um compilador poderia avisar que o primeiro rejeita
const
enquanto não avisaria para o último. Isso é permitido pelo padrão (5.2.1.3: “É claro que uma implementação é livre para produzir qualquer número de mensagens de diagnóstico, frequentemente chamadas de avisos, desde que um programa válido ainda esteja corretamente traduzido.”), então não é garantido que os dois programas se comportarão de forma idêntica a esse respeito.Além disso, uma grande parte da programação não é meramente como os programas interagem com compiladores e computadores, mas como eles interagem com humanos. Um pedaço de código pode confundir futuros leitores humanos mais do que o outro.
Não é acesso, é uma tentativa de modificar um objeto const que causa comportamento indefinido.
Você pode ter implementações onde casts entre ponteiros para pontos diferentes são problemáticos, mas eu não conheço nenhuma implementação assim agora. Mas conversões entre const / non const ou volátil / não volátil são sempre boas. Então 99,999% só importa que você tenha um objeto T const e o modifique por meio de um ponteiro T*. Se você cast o ponteiro mais adiante para um ponteiro T* const, o compilador não permite nenhuma modificação. Então, para casts múltiplos, apenas o resultado final importa.
(Observação: converter T* para um tipo inteiro muito pequeno e vice-versa causará problemas).
IMO é um UB, pois o padrão apenas garante que qualquer ponteiro pode ser convertido para ponteiro para
void
evoid *
pode ser convertido para qualquer tipo de ponteiro. Mas não garante a conversão de ou para quaisquer outros tipos de ponteiro. Também inclui conversão "indireta" viavoid
ponteiro