Eu li várias postagens no Stack Overflow RE: o erro de ponteiro com punção de tipo drefercing. Entendo que o erro é essencialmente o aviso do compilador sobre o perigo de acessar um objeto através de um ponteiro de um tipo diferente (embora uma exceção pareça ter sido feita char*
), que é um aviso compreensível e razoável.
Minha pergunta é específica para o código abaixo: por que converter o endereço de um ponteiro para uma void**
qualificação para esse aviso (promovido a erro via -Werror
)?
Além disso, esse código é compilado para várias arquiteturas de destino, apenas uma delas gera o aviso / erro - isso pode significar que é legitimamente uma deficiência específica da versão do compilador?
// main.c
#include <stdlib.h>
typedef struct Foo
{
int i;
} Foo;
void freeFunc( void** obj )
{
if ( obj && * obj )
{
free( *obj );
*obj = NULL;
}
}
int main( int argc, char* argv[] )
{
Foo* f = calloc( 1, sizeof( Foo ) );
freeFunc( (void**)(&f) );
return 0;
}
Se meu entendimento, exposto acima, estiver correto, a void**
, ainda sendo apenas um indicador, isso deve ser uma transmissão segura.
Existe uma solução alternativa que não use lvalues que pacifique esse aviso / erro específico do compilador? Ou seja, eu entendo isso e por que isso resolverá o problema, mas gostaria de evitar essa abordagem porque quero tirar proveito do freeFunc()
NULL de um argumento pretendido:
void* tmp = f;
freeFunc( &tmp );
f = NULL;
Compilador de problemas (um de um):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules
user@8d63f499ed92:/build$
Compilador que não reclama (um de muitos):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
user@8d63f499ed92:/build$
Atualização: Descobri ainda que o aviso parece ser gerado especificamente quando compilado com -O2
(ainda com o "compilador de problemas" observado apenas)
void**
, ainda sendo apenas um ponteiro, esse deve ser um lançamento seguro". Woah lá skippy! Parece que você tem algumas suposições fundamentais em andamento. Tente pensar menos em termos de bytes e alavancas e mais em termos de abstrações, porque é isso que você está realmente a programação comRespostas:
Um valor do tipo
void**
é um ponteiro para um objeto do tipovoid*
. Um objeto do tipoFoo*
não é um objeto do tipovoid*
.Há uma conversão implícita entre valores do tipo
Foo*
evoid*
. Essa conversão pode alterar a representação do valor. Da mesma forma, você pode escreverint n = 3; double x = n;
e isso tem um comportamento bem definido de definirx
o valor3.0
, masdouble *p = (double*)&n;
tem um comportamento indefinido (e na prática não será definidop
como um "ponteiro para3.0
" em nenhuma arquitetura comum).As arquiteturas em que diferentes tipos de ponteiros para objetos têm representações diferentes são raras hoje em dia, mas são permitidas pelo padrão C. Existem máquinas antigas (raras) com ponteiros de palavras que são endereços de uma palavra na memória e ponteiros de bytes que são endereços de uma palavra, juntamente com um deslocamento de bytes nessa palavra;
Foo*
seria um ponteiro de palavras evoid*
um ponteiro de bytes em tais arquiteturas. Existem máquinas (raras) com indicadores de gordura que contêm informações não apenas sobre o endereço do objeto, mas também sobre seu tipo, tamanho e lista de controle de acesso; um ponteiro para um tipo definido pode ter uma representação diferente daquelavoid*
que precisa de informações adicionais sobre o tipo em tempo de execução.Tais máquinas são raras, mas permitidas pelo padrão C. E alguns compiladores C aproveitam a permissão para tratar ponteiros com punção de tipo como distintos para otimizar o código. O risco de aliasing de ponteiros é uma grande limitação à capacidade de um compilador de otimizar o código; portanto, os compiladores tendem a tirar vantagem dessas permissões.
Um compilador é livre para dizer que você está fazendo algo errado, ou fazer silenciosamente o que não queria ou fazer silenciosamente o que queria. O comportamento indefinido permite qualquer um desses.
Você pode fazer
freefunc
uma macro:Isso vem com as limitações usuais das macros: a falta de segurança do tipo
p
é avaliada duas vezes. Observe que isso só oferece a segurança de não deixar ponteiros pendurados ao redor, sep
foi o ponteiro único para o objeto liberado.fonte
Foo*
evoid*
tiver a mesma representação em sua arquitetura, ainda não for definido digitá-los.A
void *
é tratado especialmente pelo padrão C em parte porque faz referência a um tipo incompleto. Este tratamento que não se estendem paravoid **
, uma vez que faz aponte para um tipo completo, especificamentevoid *
.As regras estritas de alias dizem que você não pode converter um ponteiro de um tipo em um ponteiro de outro tipo e subsequentemente desreferenciar esse ponteiro, pois isso significa reinterpretar os bytes de um tipo como outro. A única exceção é a conversão para um tipo de caractere que permite ler a representação de um objeto.
Você pode contornar essa limitação usando uma macro de função em vez de uma função:
Que você pode chamar assim:
No entanto, isso tem uma limitação, porque a macro acima será avaliada
obj
duas vezes. Se você estiver usando o GCC, isso pode ser evitado com algumas extensões, especificamente astypeof
expressões de palavra - chave e instrução:fonte
#define
é que ele avaliaráobj
duas vezes. Mas não conheço uma boa maneira de evitar essa segunda avaliação. Mesmo uma expressão de declaração (extensão GNU) não funciona como você precisa atribuirobj
depois de usar seu valor.typeof
para evitar avaliandoobj
duas vezes:#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.A desreferenciação de um ponteiro com tipo de punção é UB e você não pode contar com o que acontecerá.
Compiladores diferentes geram avisos diferentes e, para esse fim, versões diferentes do mesmo compilador podem ser consideradas como compiladores diferentes. Essa parece uma explicação melhor para a variação que você vê do que uma dependência da arquitetura.
Um caso que pode ajudá-lo a entender por que a punição de tipo nesse caso pode ser ruim é que sua função não funcionará em uma arquitetura para a qual
sizeof(Foo*) != sizeof(void*)
. Isso é autorizado pelo padrão, embora eu não conheça nenhum atual para o qual isso seja verdade.Uma solução alternativa seria usar uma macro em vez de uma função.
Observe que
free
aceita ponteiros nulos.fonte
sizeof Foo* != sizeof void*
. Eu nunca encontrei tamanhos de ponteiro "em estado selvagem" que dependessem do tipo; portanto, ao longo dos anos, passei a considerar axiomático que os tamanhos de ponteiro sejam todos iguais em uma determinada arquitetura.float*
não modifica umint32_t
objeto; portanto, por exemplo,int32_t*
não é necessárioint32_t *restrict ptr
que o compilador assuma que não está apontando para a mesma memória. O mesmo para lojas através de umvoid**
pressuposto de não modificar umFoo*
objeto.Esse código é inválido de acordo com o padrão C, portanto, pode funcionar em alguns casos, mas não é necessariamente portátil.
A "regra estrita de aliasing" para acessar um valor por meio de um ponteiro que foi convertido para um tipo de ponteiro diferente é encontrada no parágrafo 6.5 do parágrafo 7:
Em sua
*obj = NULL;
declaração, o objeto tem o tipo eficazFoo*
, mas é acessado pela expressão lvalue*obj
com o tipovoid*
.No 6.7.5.1, parágrafo 2, temos
Portanto,
void*
eFoo*
não são tipos compatíveis ou compatíveis com qualificadores adicionados e certamente não se enquadram em nenhuma das outras opções da regra de alias estrita.Embora não seja o motivo técnico pelo qual o código é inválido, também é relevante observar a seção 6.2.5, parágrafo 26:
Quanto às diferenças nos avisos, esse não é um caso em que o Padrão exige uma mensagem de diagnóstico; portanto, é apenas uma questão de quão bom é o compilador ou sua versão em perceber possíveis problemas e apontá-los de uma maneira útil. Você notou que as configurações de otimização podem fazer a diferença. Isso geralmente ocorre porque mais informações são geradas internamente sobre como várias partes do programa realmente se encaixam na prática e, portanto, essas informações extras também estão disponíveis para verificações de aviso.
fonte
Além do que as outras respostas disseram, este é um anti-padrão clássico em C e que deve ser queimado com fogo. Aparece em:
void *
(que não sofre esse problema porque envolve uma conversão de valor em vez de punção de tipo ); em vez disso, retorna um sinalizador de erro e armazena o resultado por meio de ponteiro para ponteiro.Para outro exemplo de (1), houve um caso infame de longa data na
av_free
função do ffmpeg / libavcodec . Acredito que foi corrigido com uma macro ou algum outro truque, mas não tenho certeza.Para (2), ambos
cudaMalloc
eposix_memalign
são exemplos.Em nenhum dos casos, a interface exige inerentemente o uso inválido, mas a encoraja fortemente, e admite o uso correto apenas com um objeto temporário extra do tipo
void *
que derrota o objetivo da funcionalidade de saída livre e nula e torna a alocação desajeitada.fonte
void *
e faça a conversão / conversão toda vez que quiser desreferenciá-la. Isso é muito improvável. Se o chamador estiver armazenando algum outro tipo de ponteiro, a única maneira de chamar a função sem chamar UB é copiar o ponteiro para um objeto temporário do tipovoid *
e passar o endereço desse para a função de liberação, e então ...(void **)
cast, produzindo um comportamento indefinido.Embora C tenha sido projetado para máquinas que usam a mesma representação para todos os ponteiros, os autores da Norma desejavam tornar o idioma utilizável em máquinas que usam representações diferentes para ponteiros para diferentes tipos de objetos. Portanto, eles não exigiram que máquinas que usem diferentes representações de ponteiros para diferentes tipos de ponteiros suportem um tipo de "ponteiro para qualquer tipo de ponteiro", mesmo que muitas máquinas possam fazê-lo a custo zero.
Antes da redação do Padrão, implementações para plataformas que usavam a mesma representação para todos os tipos de ponteiros permitiam, por unanimidade
void**
, utilizar um, pelo menos com a projeção adequada, como um "ponteiro para qualquer ponteiro". Os autores do Padrão quase certamente reconheceram que isso seria útil nas plataformas que o apoiavam, mas, como não podia ser universalmente suportado, eles se recusaram a mandatá-lo. Em vez disso, eles esperavam que a implementação da qualidade processasse construções como o que o Fundamento descreveria como uma "extensão popular", nos casos em que isso faria sentido.fonte