Ich habe verschiedene Beiträge zu Stack Overflow RE gelesen : Der dereferenzierende typgesteuerte Zeigerfehler. Nach meinem Verständnis ist der Fehler im Wesentlichen die Compiler-Warnung vor der Gefahr des Zugriffs auf ein Objekt über einen Zeiger eines anderen Typs (obwohl eine Ausnahme gemacht zu sein scheint char*
), was eine verständliche und vernünftige Warnung ist.
Meine Frage bezieht sich speziell auf den folgenden Code: Warum qualifiziert sich das Umwandeln der Adresse eines Zeigers auf eine void**
für diese Warnung (über Fehler zum Fehler befördert -Werror
)?
Darüber hinaus wird dieser Code für mehrere Zielarchitekturen kompiliert, von denen nur eine die Warnung / den Fehler generiert. Könnte dies bedeuten, dass es sich zu Recht um einen compilerspezifischen Mangel des Compilers handelt?
// 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;
}
Wenn mein oben angegebenes Verständnis richtig ist und a void**
immer noch nur ein Zeiger ist, sollte dies ein sicheres Casting sein.
Gibt es eine Problemumgehung , bei der keine l-Werte verwendet werden , um diese compilerspezifische Warnung / diesen Fehler zu beruhigen? Dh ich verstehe das und warum dies das Problem lösen wird, aber ich möchte diesen Ansatz vermeiden, weil ich die freeFunc()
NULL eines beabsichtigten Out-Arg nutzen möchte :
void* tmp = f;
freeFunc( &tmp );
f = NULL;
Problem Compiler (einer von einem):
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$
Nicht beschwerender Compiler (einer von vielen):
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$
Update: Ich habe außerdem festgestellt, dass die Warnung beim Kompilieren speziell generiert zu werden scheint -O2
(immer noch nur mit dem angegebenen "Problem-Compiler").
void**
, da dies immer noch nur ein Zeiger ist, sollte dies ein sicheres Casting sein." Woah da skippy! Klingt so, als hätten Sie einige grundlegende Annahmen. Versuchen Sie, weniger in Bytes und Hebeln als in Abstraktionen zu denken, denn damit programmieren Sie tatsächlichAntworten:
Ein Wert vom Typ
void**
ist ein Zeiger auf ein Objekt vom Typvoid*
. Ein Objekt vom TypFoo*
ist kein Objekt vom Typvoid*
.Es gibt eine implizite Konvertierung zwischen Werten vom Typ
Foo*
undvoid*
. Diese Konvertierung kann die Darstellung des Werts ändern. In ähnlicher Weise können Sie schreiben,int n = 3; double x = n;
und dies hat das genau definierte Verhaltenx
, den Wert festzulegen3.0
,double *p = (double*)&n;
hat jedoch ein undefiniertes Verhalten (und wird in der Praxis in keiner gängigen Architekturp
auf einen „Zeiger auf3.0
“ gesetzt).Architekturen, bei denen verschiedene Arten von Zeigern auf Objekte unterschiedliche Darstellungen haben, sind heutzutage selten, aber nach dem C-Standard zulässig. Es gibt (seltene) alte Maschinen mit Wortzeigern, die Adressen eines Wortes im Speicher sind, und Bytezeigern, die Adressen eines Wortes zusammen mit einem Byte-Offset in diesem Wort sind.
Foo*
wäre ein Wortzeiger undvoid*
wäre ein Bytezeiger auf solchen Architekturen. Es gibt (seltene) Maschinen mit fetten Zeigern, die nicht nur Informationen über die Adresse des Objekts enthalten, sondern auch über seinen Typ, seine Größe und seine Zugriffssteuerungslisten. Ein Zeiger auf einen bestimmten Typ hat möglicherweise eine andere Darstellung als ein Zeiger, der zurvoid*
Laufzeit zusätzliche Typinformationen benötigt.Solche Maschinen sind selten, aber nach dem C-Standard zulässig. Einige C-Compiler nutzen die Berechtigung, typgesteuerte Zeiger als unterschiedlich zu behandeln, um den Code zu optimieren. Das Risiko von Zeiger-Aliasing ist eine wesentliche Einschränkung für die Fähigkeit eines Compilers, Code zu optimieren. Daher nutzen Compiler solche Berechtigungen in der Regel.
Ein Compiler kann Ihnen sagen, dass Sie etwas falsch machen oder leise tun, was Sie nicht wollten, oder leise tun, was Sie wollten. Undefiniertes Verhalten erlaubt all dies.
Sie können
freefunc
ein Makro erstellen:Dies ist mit den üblichen Einschränkungen von Makros verbunden: Mangelnde Typensicherheit
p
wird zweimal bewertet. Beachten Sie, dass dies Ihnen nur die Sicherheit gibt, keine baumelnden Zeiger herumzulassen, wenn diesp
der einzige Zeiger auf das freigegebene Objekt war.quelle
Foo*
undvoid*
haben die gleiche Darstellung von der Architektur, ist es noch nicht definiert , um Typ-pun sie.A
void *
wird teilweise vom C-Standard speziell behandelt, weil es auf einen unvollständigen Typ verweist. Diese Behandlung ist nicht zu erweitern , um ,void **
wie es tut Punkt zu einer vollständigen Art, speziellvoid *
.Die strengen Aliasing-Regeln besagen, dass Sie einen Zeiger eines Typs nicht in einen Zeiger eines anderen Typs konvertieren und diesen Zeiger anschließend dereferenzieren können, da dies bedeutet, dass die Bytes eines Typs als andere neu interpretiert werden. Die einzige Ausnahme ist die Konvertierung in einen Zeichentyp, mit dem Sie die Darstellung eines Objekts lesen können.
Sie können diese Einschränkung umgehen, indem Sie anstelle einer Funktion ein funktionsähnliches Makro verwenden:
Was Sie so nennen können:
Dies hat jedoch eine Einschränkung, da das obige Makro
obj
zweimal ausgewertet wird. Wenn Sie GCC verwenden, kann dies mit einigen Erweiterungen vermieden werden, insbesondere mit dentypeof
Schlüsselwort- und Anweisungsausdrücken:quelle
#define
ist, dass esobj
zweimal ausgewertet wird. Ich kenne jedoch keinen guten Weg, um diese zweite Bewertung zu vermeiden. Selbst ein Anweisungsausdruck (GNU-Erweiterung) reicht nicht aus, da Sie ihn zuweisen müssen,obj
nachdem Sie seinen Wert verwendet haben.typeof
vermeiden,obj
zweimal auszuwerten :#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.Das Dereferenzieren eines Typ-Punned-Zeigers ist UB und Sie können nicht damit rechnen, was passieren wird.
Unterschiedliche Compiler generieren unterschiedliche Warnungen, und zu diesem Zweck können unterschiedliche Versionen desselben Compilers als unterschiedliche Compiler betrachtet werden. Dies scheint eine bessere Erklärung für die Varianz zu sein, die Sie sehen, als eine Abhängigkeit von der Architektur.
Ein Fall, der Ihnen helfen kann zu verstehen, warum Typ Punning in diesem Fall schlecht sein kann, ist, dass Ihre Funktion auf einer Architektur, für die, nicht funktioniert
sizeof(Foo*) != sizeof(void*)
. Das ist durch den Standard autorisiert, obwohl ich keinen aktuellen kenne, für den dies zutrifft.Eine Problemumgehung wäre die Verwendung eines Makros anstelle einer Funktion.
Beachten Sie, dass
free
Nullzeiger akzeptiert werden.quelle
sizeof Foo* != sizeof void*
. Ich habe noch nie festgestellt, dass Zeigergrößen "in the wild" typabhängig sind. Im Laufe der Jahre habe ich es als selbstverständlich angesehen, dass Zeigergrößen in einer bestimmten Architektur alle gleich sind.float*
einint32_t
Objekt nicht geändert wird. Daherint32_t*
mussint32_t *restrict ptr
der Compiler beispielsweise nicht annehmen, dass er nicht auf denselben Speicher verweist. Gleiches gilt für Geschäftevoid**
, bei denen davon ausgegangen wird, dass einFoo*
Objekt nicht geändert wird.Dieser Code ist gemäß C-Standard ungültig, sodass er in einigen Fällen möglicherweise funktioniert, jedoch nicht unbedingt portierbar ist.
Die "strenge Aliasing-Regel" für den Zugriff auf einen Wert über einen Zeiger, der in einen anderen Zeigertyp umgewandelt wurde, finden Sie in 6.5 Absatz 7:
In Ihrer
*obj = NULL;
Anweisung hat das Objekt einen effektiven Typ, auf denFoo*
jedoch der lvalue-Ausdruck*obj
mit type zugreiftvoid*
.In 6.7.5.1 Absatz 2 haben wir
So
void*
undFoo*
nicht kompatible Typen oder kompatible Typen mit Qualifier hinzugefügt, und schon gar nicht eine der anderen Optionen der strengen Aliasing - Regel passen.Obwohl dies nicht der technische Grund ist, warum der Code ungültig ist, ist es auch relevant, Abschnitt 6.2.5 Absatz 26 zu beachten:
Was die Unterschiede bei den Warnungen betrifft, so ist dies kein Fall, in dem der Standard eine Diagnosemeldung erfordert. Es geht also nur darum, wie gut der Compiler oder seine Version potenzielle Probleme erkennt und auf hilfreiche Weise darauf hinweist. Sie haben festgestellt, dass Optimierungseinstellungen einen Unterschied machen können. Dies liegt häufig daran, dass intern mehr Informationen darüber generiert werden, wie verschiedene Teile des Programms in der Praxis tatsächlich zusammenpassen, und dass zusätzliche Informationen daher auch für Warnprüfungen verfügbar sind.
quelle
Zusätzlich zu den anderen Antworten ist dies ein klassisches Anti-Muster in C, das mit Feuer verbrannt werden sollte. Es erscheint in:
void *
(das nicht unter diesem Problem leidet, da es eine Wertkonvertierung anstelle von Typ Punning beinhaltet ), stattdessen ein Fehlerflag zurückgeben und das Ergebnis über einen Zeiger auf Zeiger speichern.Für ein anderes Beispiel von (1) gab es einen langjährigen berüchtigten Fall in der
av_free
Funktion von ffmpeg / libavcodec . Ich glaube, es wurde irgendwann mit einem Makro oder einem anderen Trick behoben, aber ich bin mir nicht sicher.Für (2) sind beide
cudaMalloc
undposix_memalign
Beispiele.In keinem Fall erfordert die Schnittstelle von Natur aus eine ungültige Verwendung, sie empfiehlt dies jedoch nachdrücklich und lässt die korrekte Verwendung nur mit einem zusätzlichen temporären Objekt vom Typ zu
void *
, das den Zweck der Free-and-Null-Out-Funktionalität zunichte macht und die Zuweisung umständlich macht.quelle
void *
und gieße / konvertiere es jedes Mal, wenn es dereferenziert werden soll. Dies ist sehr unwahrscheinlich. Wenn der Aufrufer einen anderen Zeigertyp speichert, besteht die einzige Möglichkeit, die Funktion aufzurufen, ohne UB aufzurufen, darin, den Zeiger auf ein temporäres Objekt vom Typ zu kopierenvoid *
und die Adresse davon an die Freigabefunktion zu übergeben, und dann einfach ...(void **)
Besetzung durchführen und undefiniertes Verhalten erzeugen.Obwohl C für Maschinen entwickelt wurde, die für alle Zeiger dieselbe Darstellung verwenden, wollten die Autoren des Standards die Sprache auf Maschinen verwenden, die unterschiedliche Darstellungen für Zeiger auf verschiedene Objekttypen verwenden. Daher war es nicht erforderlich, dass Maschinen, die unterschiedliche Zeigerdarstellungen für verschiedene Arten von Zeigern verwenden, einen Typ "Zeiger auf jede Art von Zeiger" unterstützen, obwohl viele Maschinen dies ohne Kosten tun könnten.
Bevor der Standard geschrieben wurde, erlaubten Implementierungen für Plattformen, die für alle Zeigertypen dieselbe Darstellung
void**
verwendeten, einstimmig die Verwendung von a, zumindest mit geeignetem Casting, als "Zeiger auf einen beliebigen Zeiger". Die Autoren des Standards erkannten mit ziemlicher Sicherheit, dass dies auf Plattformen nützlich sein würde, die ihn unterstützten, aber da er nicht allgemein unterstützt werden konnte, lehnten sie es ab, ihn zu beauftragen. Stattdessen erwarteten sie, dass die Qualitätsimplementierung solche Konstrukte verarbeiten würde, wie sie in der Begründung als "populäre Erweiterung" beschrieben würden, in Fällen, in denen dies sinnvoll wäre.quelle