Por que esse aviso de ponteiro puncionado por cancelamento de referência alegado é específico do compilador?

38

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)

StoneThrow
fonte
11
"a 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 com
Leveza raças em Orbit
7
Tangencialmente, os compiladores que você está usando têm 15 e 17 anos! Eu não confiaria em nenhum deles.
Tavian Barnes 12/11/19
4
@TavianBarnes Além disso, se você precisar confiar no GCC 3 por qualquer motivo, é melhor usar a versão final em fim de vida útil, que era 3.4.6, eu acho. Por que não tirar proveito de todas as correções disponíveis para essa série antes de descansar.
Kaz
Que tipo de padrão de codificação C ++ prescreve todos esses espaços?
Peter Mortensen

Respostas:

33

Um valor do tipo void**é um ponteiro para um objeto do tipo void*. Um objeto do tipo Foo*não é um objeto do tipo void*.

Há uma conversão implícita entre valores do tipo Foo*e void*. Essa conversão pode alterar a representação do valor. Da mesma forma, você pode escrever int n = 3; double x = n;e isso tem um comportamento bem definido de definir xo valor 3.0, mas double *p = (double*)&n;tem um comportamento indefinido (e na prática não será definido pcomo um "ponteiro para 3.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 e void*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 daquela void*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 freefuncuma macro:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

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, se pfoi o ponteiro único para o objeto liberado.

Gilles 'SO- parar de ser mau'
fonte
11
E é bom saber que, mesmo se Foo*e void*tiver a mesma representação em sua arquitetura, ainda não for definido digitá-los.
Tavian Barnes 12/11/19
12

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 para void **, uma vez que faz aponte para um tipo completo, especificamente void *.

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:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Que você pode chamar assim:

freeFunc(f);

No entanto, isso tem uma limitação, porque a macro acima será avaliada objduas vezes. Se você estiver usando o GCC, isso pode ser evitado com algumas extensões, especificamente as typeofexpressões de palavra - chave e instrução:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
fonte
3
+1 por fornecer uma melhor implementação do comportamento pretendido. O único problema que vejo com o #defineé que ele avaliará objduas 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 atribuir objdepois de usar seu valor.
cmaster - restabelece monica 12/11/19
2
@cmaster: Se você estiver disposto a usar extensões GNU como expressões de instrução, então você pode usar typeofpara evitar avaliando objduas vezes: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
Ruakh
@ruakh Muito legal :-) Seria ótimo se o dbush editasse isso na resposta, para que não seja excluído em massa com os comentários.
cmaster - restabelece monica 13/11/19
9

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 freeaceita ponteiros nulos.

AProgrammer
fonte
2
Fascinante que é possível que 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.
StoneThrow 11/11/19
11
@Stonethrow, o exemplo padrão são indicadores de gordura usados ​​para endereçar bytes na arquitetura endereçável por palavras. Mas acho que as máquinas endereçáveis ​​de palavras atuais usam a alternativa sizeof char == sizeof word .
AProgrammer 11/11/19
2
Note-se que o tipo tem de ser entre parênteses para sizeof ...
Antti Haapala
@StoneThrow: Independentemente do tamanho do ponteiro, a análise de alias baseada em tipo a torna insegura; isso ajuda os compiladores a otimizarem, assumindo que uma loja através de um float*não modifica um int32_tobjeto; portanto, por exemplo, int32_t*não é necessário int32_t *restrict ptrque o compilador assuma que não está apontando para a mesma memória. O mesmo para lojas através de um void**pressuposto de não modificar um Foo*objeto.
Peter Cordes
4

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:

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

  • 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.

Em sua *obj = NULL;declaração, o objeto tem o tipo eficaz Foo*, mas é acessado pela expressão lvalue *objcom o tipo void*.

No 6.7.5.1, parágrafo 2, temos

Para que dois tipos de ponteiros sejam compatíveis, ambos devem ser identificados de forma idêntica e ambos devem ser ponteiros para tipos compatíveis.

Portanto, void*e Foo*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:

Um ponteiro para voiddeve ter os mesmos requisitos de representação e alinhamento que um ponteiro para um tipo de caractere. Da mesma forma, os indicadores para versões qualificadas ou não qualificadas de tipos compatíveis devem ter os mesmos requisitos de representação e alinhamento. Todos os ponteiros para tipos de estrutura devem ter os mesmos requisitos de representação e alinhamento que os outros. Todos os ponteiros para tipos de união devem ter os mesmos requisitos de representação e alinhamento que os outros. Ponteiros para outros tipos não precisam ter os mesmos requisitos de representação ou alinhamento.

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.

aschepler
fonte
2

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:

  1. Funções de saída livre e nula, como aquela em que você encontrou o aviso.
  2. Funções de alocação que evitam o idioma C padrão de retorno 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_freefunção do ffmpeg / libavcodec . Acredito que foi corrigido com uma macro ou algum outro truque, mas não tenho certeza.

Para (2), ambos cudaMalloce posix_memalignsã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.

R .. GitHub PARE DE AJUDAR O GELO
fonte
Você tem um link explicando mais sobre por que (1) é um antipadrão? Acho que não estou familiarizado com esta situação / argumento e gostaria de aprender mais.
StoneThrow 12/11/19
11
@StoneThrow: É realmente simples - a intenção é impedir o uso indevido, anulando o objeto que armazena o ponteiro na memória que está sendo liberada, mas a única maneira de fazer isso é se o chamador estiver realmente armazenando o ponteiro em um objeto digite 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 tipo void *e passar o endereço desse para a função de liberação, e então ...
R .. GitHub Pare de ajudar o gelo
11
... anula um objeto temporário em vez do armazenamento real onde o chamador estava com o ponteiro. É claro que o que realmente acontece é que os usuários da função acabam fazendo um (void **)cast, produzindo um comportamento indefinido.
R .. GitHub Pare de ajudar o gelo
2

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.

supercat
fonte