Perché questo preteso dereferenziamento di tipo puntatore puntato sul compilatore è specifico del compilatore?


38

Ho letto vari post su Stack Overflow RE: l'errore del puntatore tipo-derefercing puntato. La mia comprensione è che l'errore è essenzialmente l'avvertimento del compilatore del pericolo di accedere a un oggetto attraverso un puntatore di tipo diverso (sebbene un'eccezione sembri essere fatta per char*), che è un avvertimento comprensibile e ragionevole.

La mia domanda è specifica per il codice seguente: perché il casting dell'indirizzo di un puntatore si void**qualifica per questo avviso (promosso a errore tramite -Werror)?

Inoltre, questo codice viene compilato per più architetture di destinazione, solo una delle quali genera l'avviso / errore - ciò potrebbe implicare che si tratti legittimamente di un difetto specifico della versione del compilatore?

// 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 la mia comprensione, dichiarata sopra, è corretta, a void**, essendo ancora solo un puntatore, questo dovrebbe essere un casting sicuro.

Esiste una soluzione alternativa che non utilizza i valori che pacificherebbe questo avviso / errore specifico del compilatore? Vale a dire che lo capisco e perché questo risolverà il problema, ma vorrei evitare questo approccio perché voglio trarre vantaggio da freeFunc() NULL ing out-arg previsto:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Compilatore di problemi (uno di uno):

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$

Compilatore non lagnante (uno dei tanti):

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$

Aggiornamento: ho scoperto inoltre che l'avviso sembra essere generato specificamente quando compilato -O2(sempre con il solo "compilatore di problemi")



1
"a void**, essendo ancora solo un puntatore, questo dovrebbe essere un casting sicuro." Woah lì skippy! Sembra che tu abbia alcune assunzioni fondamentali in corso. Cerca di pensare meno in termini di byte e leve e di più in termini di astrazioni, perché questo è ciò con cui stai effettivamente programmando
Lightness Races in Orbit

7
Tangenzialmente, i compilatori che stai utilizzando hanno 15 e 17 anni! Non farei affidamento su nessuno dei due.
Tavian Barnes,

4
@TavianBarnes Inoltre, se devi affidarti a GCC 3 per qualsiasi motivo, è meglio usare la versione di fine vita, che era 3.4.6, credo. Perché non sfruttare tutte le correzioni disponibili per quella serie prima che venisse messa a riposo.
Kaz,

Che tipo di standard di codifica C ++ prescrive tutti quegli spazi?
Peter Mortensen,

Risposte:


33

Un valore di tipo void**è un puntatore a un oggetto di tipo void*. Un oggetto di tipo Foo*non è un oggetto di tipo void*.

Esiste una conversione implicita tra valori di tipo Foo*e void*. Questa conversione può cambiare la rappresentazione del valore. Allo stesso modo, puoi scrivere int n = 3; double x = n;e questo ha il comportamento ben definito dell'impostazione xdel valore 3.0, ma double *p = (double*)&n;ha un comportamento indefinito (e in pratica non sarà impostato psu un "puntatore a 3.0" su qualsiasi architettura comune).

Le architetture in cui diversi tipi di puntatori a oggetti hanno rappresentazioni diverse sono rare al giorno d'oggi, ma sono consentite dallo standard C. Esistono (rare) vecchie macchine con puntatori di parole che sono indirizzi di una parola in memoria e puntatori di byte che sono indirizzi di una parola insieme a un offset di byte in questa parola; Foo*sarebbe un puntatore a parola e void*sarebbe un puntatore a byte su tali architetture. Esistono macchine (rare) con puntatori grassi che contengono informazioni non solo sull'indirizzo dell'oggetto, ma anche sul suo tipo, dimensione e elenco di controllo accessi; un puntatore a un tipo definito potrebbe avere una rappresentazione diversa da una void*che necessita di informazioni aggiuntive sul tipo in fase di esecuzione.

Tali macchine sono rare, ma consentite dalla norma C. E alcuni compilatori C sfruttano l'autorizzazione a trattare i puntatori punzonati come distinti per ottimizzare il codice. Il rischio di alias dei puntatori è una delle principali limitazioni alla capacità di un compilatore di ottimizzare il codice, quindi i compilatori tendono a trarre vantaggio da tali autorizzazioni.

Un compilatore è libero di dirti che stai facendo qualcosa di sbagliato, o di fare in silenzio ciò che non volevi, o di fare in silenzio ciò che volevi. Il comportamento indefinito consente uno di questi.

Puoi creare freefuncuna macro:

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

Ciò comporta i consueti limiti delle macro: la mancanza di sicurezza del tipo, pviene valutata due volte. Nota che questo ti dà la sicurezza di non lasciare in giro i puntatori penzolanti se pfosse il singolo puntatore all'oggetto liberato.


1
Ed è bello sapere che anche se Foo*e void*hanno la stessa rappresentazione sulla tua architettura, è ancora indefinito digitarli.
Tavian Barnes,

12

A void *è trattato appositamente dallo standard C in parte perché fa riferimento a un tipo incompleto. Questo trattamento non non estendere a void **quanto fa scegliere un tipo completo, specificamente void *.

Le rigide regole di aliasing affermano che non è possibile convertire un puntatore di un tipo in un puntatore di un altro tipo e successivamente dereferenziare quel puntatore perché farlo significa reinterpretare i byte di un tipo come un altro. L'unica eccezione è quando si converte in un tipo di carattere che consente di leggere la rappresentazione di un oggetto.

È possibile aggirare questa limitazione utilizzando una macro simile a una funzione anziché una funzione:

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

Che puoi chiamare così:

freeFunc(f);

Ciò ha tuttavia una limitazione, poiché la macro sopra verrà valutata objdue volte. Se stai usando GCC, questo può essere evitato con alcune estensioni, in particolare le typeofespressioni di parole chiave e istruzioni:

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })

3
+1 per fornire una migliore attuazione del comportamento previsto. L'unico problema che vedo #defineè che valuterà objdue volte. Non conosco un buon modo per evitare questa seconda valutazione. Anche un'espressione dell'istruzione (estensione GNU) non farà il trucco come è necessario assegnare objdopo averne usato il valore.
cmaster - reintegra monica il

2
@cmaster: Se siete disposti a utilizzare le estensioni GNU, come espressioni di istruzione, quindi è possibile utilizzare typeofper evitare di valutare objdue volte: #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
Ruakh,

@ruakh Molto bello :-) Sarebbe bello se dbush lo modificasse nella risposta, quindi non verrà cancellato in massa con i commenti.
cmaster - reinstalla monica il

9

Dereferenziare un tipo puntatore puntato è UB e non puoi contare su ciò che accadrà.

Compilatori diversi generano avvisi diversi e, a tale scopo, diverse versioni dello stesso compilatore possono essere considerate come compilatori diversi. Questa sembra una spiegazione migliore per la varianza che vedi di una dipendenza dall'architettura.

Un caso che può aiutarti a capire perché la punzonatura di tipo in questo caso può essere negativo è che la tua funzione non funzionerà su un'architettura per la quale sizeof(Foo*) != sizeof(void*). Ciò è autorizzato dallo standard, anche se non conosco nessuno per il quale ciò sia vero.

Una soluzione alternativa sarebbe utilizzare una macro anziché una funzione.

Si noti che freeaccetta puntatori null.


2
Affascinante che sia possibile sizeof Foo* != sizeof void*. Non ho mai riscontrato che le dimensioni del puntatore "in the wild" dipendano dal tipo, quindi nel corso degli anni, sono giunto a considerare assiomatico che le dimensioni del puntatore sono tutte uguali su una data architettura.
StoneThrow

1
@Stonethrow l'esempio standard sono i puntatori fat usati per indirizzare i byte nell'architettura indirizzabile a parole. Ma penso che le macchine indirizzabili della parola corrente utilizzino l'alternativa sizeof char == sizeof word .
AProgrammer

2
Nota che il tipo deve essere tra parentesi per dimensione di ...
Antti Haapala,

@StoneThrow: indipendentemente dalle dimensioni del puntatore, l'analisi alias basata sul tipo lo rende non sicuro; questo aiuta i compilatori a ottimizzare supponendo che un negozio attraverso un float*non modifichi un int32_toggetto, quindi ad esempio int32_t*non è necessario int32_t *restrict ptrche il compilatore supponga che non stia puntando alla stessa memoria. Lo stesso vale per i negozi void**che presuppongono di non modificare un Foo*oggetto.
Peter Cordes,

4

Questo codice non è valido secondo lo standard C, quindi potrebbe funzionare in alcuni casi, ma non è necessariamente portatile.

La "regola di aliasing rigorosa" per accedere a un valore tramite un puntatore che è stato lanciato su un diverso tipo di puntatore si trova nel paragrafo 7 6.5:

Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione lvalue che ha uno dei seguenti tipi:

  • un tipo compatibile con il tipo effettivo dell'oggetto,

  • una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,

  • un tipo di tipo con o senza segno corrispondente al tipo effettivo dell'oggetto,

  • un tipo di tipo con o senza segno corrispondente a una versione qualificata del tipo effettivo dell'oggetto,

  • un tipo di aggregato o unione che include uno dei tipi di cui sopra tra i suoi membri (incluso, ricorsivamente, un membro di un'unione subaggregata o contenuta), o

  • un tipo di carattere.

Nella tua *obj = NULL;dichiarazione, l'oggetto ha un tipo efficace Foo*ma vi si accede dall'espressione lvalue *objcon type void*.

Nel paragrafo 6.7.5.1, abbiamo

Affinché due tipi di puntatore siano compatibili, entrambi devono essere identicamente qualificati ed entrambi devono essere puntatori a tipi compatibili.

Quindi void*e Foo*non sono tipi compatibili o tipi compatibili con i qualificatori aggiunti e certamente non si adattano a nessuna delle altre opzioni della regola di aliasing rigoroso.

Sebbene non sia la ragione tecnica per cui il codice non è valido, è anche rilevante notare la sezione 6.2.5 paragrafo 26:

Un puntatore voiddeve avere gli stessi requisiti di rappresentazione e allineamento di un puntatore a un tipo di carattere. Allo stesso modo, i puntatori a versioni qualificate o non qualificate di tipi compatibili devono avere gli stessi requisiti di rappresentazione e allineamento. Tutti i puntatori ai tipi di struttura devono avere gli stessi requisiti di rappresentazione e allineamento reciproci. Tutti i puntatori ai tipi di unione devono avere gli stessi requisiti di rappresentazione e allineamento reciproci. I puntatori ad altri tipi non devono necessariamente avere gli stessi requisiti di rappresentazione o allineamento.

Per quanto riguarda le differenze nelle avvertenze, questo non è un caso in cui lo Standard richiede un messaggio diagnostico, quindi dipende solo dalla capacità del compilatore o della sua versione di rilevare potenziali problemi e di segnalarli in modo utile. Hai notato che le impostazioni di ottimizzazione possono fare la differenza. Ciò è spesso dovuto al fatto che vengono generate internamente maggiori informazioni su come i vari elementi del programma si adattano effettivamente nella pratica e che tali informazioni aggiuntive sono quindi disponibili anche per i controlli di avvertimento.


2

Oltre a ciò che hanno detto le altre risposte, questo è un classico anti-schema in C, che dovrebbe essere bruciato con il fuoco. Appare in:

  1. Funzioni free-and-null-out come quella in cui è stato trovato l'avviso.
  2. Funzioni di allocazione che evitano il linguaggio C standard di return void *(che non soffre di questo problema perché comporta una conversione del valore anziché la punzonatura del tipo ), restituendo invece un flag di errore e memorizzando il risultato tramite un puntatore a puntatore.

Per un altro esempio di (1), c'era un caso famigerato di vecchia data nella funzione di ffmpeg / libavcodec av_free. Credo che alla fine sia stato risolto con una macro o qualche altro trucco, ma non ne sono sicuro.

Per (2), entrambi cudaMalloce posix_memalignsono esempi.

In nessun caso l'interfaccia richiede intrinsecamente un utilizzo non valido, ma lo incoraggia fortemente e ammette un uso corretto solo con un oggetto temporaneo di tipo aggiuntivo void *che vanifica lo scopo della funzionalità di annullamento e annullamento e rende scomoda l'allocazione.


Hai un link che spiega di più sul perché (1) è un anti-pattern? Non credo di avere familiarità con questa situazione / argomento e vorrei saperne di più.
StoneThrow,

1
@StoneThrow: è davvero semplice: l'intento è prevenire l'abuso annullando l'oggetto che memorizza il puntatore nella memoria che viene liberata, ma l'unico modo in cui può effettivamente farlo è se il chiamante sta effettivamente memorizzando il puntatore in un oggetto di digitare void *e lanciare / convertire ogni volta che vuole dereferenziarlo. Questo è molto improbabile. Se il chiamante sta memorizzando qualche altro tipo di puntatore, l'unico modo per chiamare la funzione senza invocare UB è copiare il puntatore su un oggetto temporaneo di tipo void *e passare l'indirizzo di quello alla funzione di liberazione, e quindi solo ...
R .. GitHub smette di aiutare ICE il

1
... annulla un oggetto temporaneo anziché l'archiviazione effettiva in cui il chiamante aveva il puntatore. Naturalmente ciò che accade realmente è che gli utenti della funzione finiscono per fare un (void **)cast, producendo un comportamento indefinito.
R .. GitHub smette di aiutare ICE il

2

Sebbene C sia stato progettato per macchine che usano la stessa rappresentazione per tutti i puntatori, gli autori dello Standard volevano rendere il linguaggio utilizzabile su macchine che usano rappresentazioni diverse per i puntatori a diversi tipi di oggetti. Pertanto, non hanno richiesto che le macchine che utilizzano diverse rappresentazioni di puntatori per diversi tipi di puntatori supportino un tipo di "puntatore a qualsiasi tipo di puntatore", anche se molte macchine potrebbero farlo a costo zero.

Prima della stesura dello standard, le implementazioni per piattaforme che utilizzavano la stessa rappresentazione per tutti i tipi di puntatore consentirebbero all'unanimità void**di essere utilizzate, almeno con il cast appropriato, come "puntatore a qualsiasi puntatore". Gli autori dello Standard hanno quasi sicuramente riconosciuto che ciò sarebbe utile su piattaforme che lo supportavano, ma poiché non poteva essere universalmente supportato, hanno rifiutato di imporlo. Invece, si aspettavano che l'implementazione di qualità avrebbe elaborato tali costrutti come ciò che la Rationale avrebbe descritto come una "estensione popolare", nei casi in cui ciò avrebbe senso.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.