Unabhängig davon, wie "schlecht" der Code ist und ob die Ausrichtung usw. auf dem Compiler / der Plattform kein Problem darstellt, ist dieses Verhalten undefiniert oder fehlerhaft?
Wenn ich eine Struktur wie diese habe: -
struct data
{
int a, b, c;
};
struct data thing;
Ist es legal , den Zugang a
, b
und c
wie (&thing.a)[0]
, (&thing.a)[1]
und (&thing.a)[2]
?
In jedem Fall hat es auf jedem Compiler und jeder Plattform, auf der ich es ausprobiert habe, mit jeder Einstellung, auf der ich es ausprobiert habe, "funktioniert". Ich mache mir nur Sorgen, dass der Compiler möglicherweise nicht erkennt, dass b und thing [1] dasselbe sind, und dass Speicher für 'b' in ein Register gestellt werden und thing [1] beispielsweise den falschen Wert aus dem Speicher liest. In jedem Fall habe ich versucht, es hat das Richtige getan. (Mir ist natürlich klar, dass das nicht viel beweist)
Dies ist nicht mein Code; es ist Code , den ich an der Arbeit habe mit, ich bin daran interessiert , ob dies ist schlecht Code oder gebrochen Code als die anderen meine Prioritäten wirkt es sehr viel für die Änderung :)
Verschlagwortet mit C und C ++. Ich interessiere mich hauptsächlich für C ++, aber auch für C, wenn es anders ist, nur aus Interesse.
Antworten:
Es ist illegal 1 . Das ist ein undefiniertes Verhalten in C ++.
Sie nehmen die Mitglieder auf Array-Weise auf, aber der C ++ - Standard sagt Folgendes (Hervorhebung von mir):
Für Mitglieder gibt es jedoch keine solche zusammenhängende Anforderung:
Während die beiden obigen Anführungszeichen ausreichen sollten, um darauf hinzuweisen, warum die Indizierung in a
struct
wie Sie kein vom C ++ - Standard definiertes Verhalten ist, wählen wir ein Beispiel: Schauen Sie sich den Ausdruck an(&thing.a)[2]
- Bezüglich des Indexoperators:In den fetten Text des obigen Zitats eintauchen: Bezüglich des Hinzufügens eines integralen Typs zu einem Zeigertyp (beachten Sie die Betonung hier).
Beachten Sie die Array- Anforderung für die if- Klausel. sonst das anders im obigen Zitat. Der Ausdruck ist
(&thing.a)[2]
offensichtlich nicht für die if- Klausel geeignet. Daher undefiniertes Verhalten.Nebenbei bemerkt: Obwohl ich den Code und seine Variationen auf verschiedenen Compilern ausgiebig experimentiert habe und sie hier keine Auffüllung einführen (es funktioniert ); Aus Sicht der Wartung ist der Code äußerst fragil. Sie sollten dennoch behaupten, dass die Implementierung die Mitglieder zusammenhängend zugewiesen hat, bevor Sie dies tun. Und bleib in Grenzen :-). Aber es ist immer noch undefiniertes Verhalten ....
Einige praktikable Problemumgehungen (mit definiertem Verhalten) wurden durch andere Antworten bereitgestellt.
Wie in den Kommentaren zu Recht erwähnt, gilt [basic.lval / 8] , das in meiner vorherigen Bearbeitung enthalten war, nicht. Danke @ 2501 und @MM
1 : Siehe @ Barrys Antwort auf diese Frage für den einzigen Rechtsfall, in dem Sie
thing.a
über diesen Partner auf ein Mitglied der Struktur zugreifen können.quelle
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Nein. In C ist dies ein undefiniertes Verhalten, auch wenn keine Auffüllung vorhanden ist.
Die Ursache für undefiniertes Verhalten ist der Zugriff außerhalb der Grenzen 1 . Wenn Sie einen Skalar haben ( Elemente a, b, c in der Struktur) und versuchen, ihn als Array 2 zu verwenden, um auf das nächste hypothetische Element zuzugreifen, verursachen Sie undefiniertes Verhalten, selbst wenn sich zufällig ein anderes Objekt desselben Typs bei befindet diese Adresse.
Sie können jedoch die Adresse des Strukturobjekts verwenden und den Versatz in ein bestimmtes Element berechnen:
Dies muss für jedes Mitglied einzeln durchgeführt werden, kann jedoch in eine Funktion eingefügt werden, die einem Array-Zugriff ähnelt.
1 (Zitiert aus: ISO / IEC 9899: 201x 6.5.6 Additive Operatoren 8)
Wenn das Ergebnis eins nach dem letzten Element des Array-Objekts zeigt, darf es nicht als Operand eines unären * Operators verwendet werden, der ausgewertet wird.
2 (Zitiert aus: ISO / IEC 9899: 201x 6.5.6 Additive Operatoren 7)
Für die Zwecke dieser Operatoren verhält sich ein Zeiger auf ein Objekt, das kein Element eines Arrays ist, genauso wie ein Zeiger auf das erste Element eines Array der Länge eins mit dem Objekttyp als Elementtyp.
quelle
char* p = ( char* )&thing.a + offsetof( thing , b );
zu undefiniertem Verhalten führt?Wenn Sie es in C ++ wirklich brauchen, erstellen Sie den Operator []:
Es funktioniert nicht nur garantiert, sondern die Verwendung ist auch einfacher. Sie müssen keinen unlesbaren Ausdruck schreiben
(&thing.a)[0]
Hinweis: Diese Antwort wird unter der Annahme gegeben, dass Sie bereits eine Struktur mit Feldern haben und den Zugriff über den Index hinzufügen müssen. Wenn Geschwindigkeit ein Problem ist und Sie die Struktur ändern können, kann dies effektiver sein:
Diese Lösung würde die Strukturgröße ändern, sodass Sie auch Methoden verwenden können:
quelle
thing.a()
.Für c ++: Wenn Sie auf ein Mitglied zugreifen müssen, ohne dessen Namen zu kennen, können Sie einen Zeiger auf die Mitgliedsvariable verwenden.
quelle
offsetoff
in C entspricht.In ISO C99 / C11 ist gewerkschaftsbasiertes Typ-Punning zulässig. Sie können dies also verwenden, anstatt Zeiger auf Nicht-Arrays zu indizieren (siehe verschiedene andere Antworten).
ISO C ++ erlaubt kein gewerkschaftsbasiertes Typ-Punning. GNU C ++ funktioniert als Erweiterung , und ich denke, einige andere Compiler, die GNU-Erweiterungen im Allgemeinen nicht unterstützen, unterstützen Union-Type-Punning. Das hilft Ihnen jedoch nicht dabei, streng portablen Code zu schreiben.
In aktuellen Versionen von gcc und clang wird durch das Schreiben einer C ++ -
switch(idx)
Elementfunktion mit a zum Auswählen eines Elements für konstante Indizes zur Kompilierungszeit optimiert, für Laufzeitindizes wird jedoch ein schrecklicher Verzweigungsasmus erzeugt. Daran ist an sich nichts auszusetzenswitch()
. Dies ist einfach ein Fehler bei der fehlenden Optimierung in aktuellen Compilern. Sie könnten Slava 'switch () effizient funktionieren.Die Lösung / Problemumgehung besteht darin, es andersherum zu machen: Geben Sie Ihrer Klasse / Struktur ein Array-Mitglied und schreiben Sie Accessor-Funktionen, um Namen an bestimmte Elemente anzuhängen.
Wir können uns die asm-Ausgabe für verschiedene Anwendungsfälle im Godbolt-Compiler-Explorer ansehen . Hierbei handelt es sich um vollständige x86-64-System V-Funktionen, wobei der nachfolgende RET-Befehl weggelassen wird, um besser zu zeigen, was Sie erhalten würden, wenn sie inline sind. ARM / MIPS / was auch immer ähnlich wäre.
Im Vergleich dazu ergibt die Antwort von @ Slava unter Verwendung eines
switch()
für C ++ einen solchen Asm für einen Index mit Laufzeitvariablen. (Code im vorherigen Godbolt-Link).Dies ist offensichtlich schrecklich im Vergleich zur gewerkschaftsbasierten C-Version (oder GNU C ++):
quelle
[]
Operator direkt auf einem Gewerkschaftsmitglied verwendet wird, definiert der Standard diesarray[index]
als äquivalent zu*((array)+(index))
, und weder gcc noch clang erkennen zuverlässig, dass ein Zugriff auf*((someUnion.array)+(index))
ein Zugriff auf istsomeUnion
. Die einzige Erklärung , die ich sehen kann , ist , dasssomeUnion.array[index]
noch*((someUnion.array)+(index))
nicht von der Norm definiert sind, sondern lediglich eine beliebte Erweiterungen und gcc / Klirren haben ausgewaehlt nicht die zweite zu unterstützen , aber scheinen die ersten, zumindest vorerst zu unterstützen.In C ++ ist dies meist undefiniertes Verhalten (es hängt von welchem Index ab).
Aus [expr.unary.op]:
Es
&thing.a
wird daher angenommen, dass sich der Ausdruck auf ein Array von eins beziehtint
.Aus [Ausdruck]:
Und von [expr.add]:
(&thing.a)[0]
ist perfekt geformt, da&thing.a
es sich um ein Array der Größe 1 handelt und wir diesen ersten Index verwenden. Das ist ein zulässiger Index.(&thing.a)[2]
dass gegen die Voraussetzung0 <= i + j <= n
, da wiri == 0
,j == 2
,n == 1
. Das einfache Konstruieren des Zeigers&thing.a + 2
ist ein undefiniertes Verhalten.(&thing.a)[1]
ist der interessante Fall. Es verletzt eigentlich nichts in [expr.add]. Wir dürfen einen Zeiger nach dem Ende des Arrays nehmen - was das wäre. Hier wenden wir uns einer Anmerkung in [basic.compound] zu:Daher ist das Nehmen des Zeigers
&thing.a + 1
ein definiertes Verhalten, aber das Dereferenzieren ist undefiniert, da es auf nichts zeigt.quelle
(&thing.a + 1)
ist ein interessanter Fall, den ich nicht behandelt habe. +1! ... Nur neugierig, sind Sie im ISO C ++ - Komitee?Dies ist undefiniertes Verhalten.
In C ++ gibt es viele Regeln, die versuchen, dem Compiler Hoffnung zu geben, zu verstehen, was Sie tun, damit er darüber nachdenken und es optimieren kann.
Es gibt Regeln für Aliasing (Zugriff auf Daten über zwei verschiedene Zeigertypen), Array-Grenzen usw.
Wenn Sie eine Variable haben
x
, bedeutet die Tatsache, dass sie kein Mitglied eines Arrays ist, dass der Compiler davon ausgehen kann, dass kein[]
basierter Array-Zugriff sie ändern kann. Es muss also nicht jedes Mal, wenn Sie es verwenden, die Daten ständig aus dem Speicher neu laden. nur wenn jemand es von seinem Namen hätte ändern können .Somit
(&thing.a)[1]
kann vom Compiler angenommen werden, dass er sich nicht darauf beziehtthing.b
. Diese Tatsache kann verwendet werden, um Lese- und Schreibvorgänge neu zu ordnen und dasthing.b
, was Sie möchten, ungültig zu machen, ohne das zu ungültig zu machen, was Sie ihm tatsächlich gesagt haben.Ein klassisches Beispiel dafür ist das Wegwerfen von const.
Hier erhalten Sie normalerweise einen Compiler, der 7, dann 2! = 7 und dann zwei identische Zeiger sagt. trotz der Tatsache, dass
ptr
darauf hinweistx
. Der Compiler nimmt die Tatsache, dassx
es sich um einen konstanten Wert handelt, um sich nicht die Mühe zu machen, ihn zu lesen, wenn Sie nach dem Wert von fragenx
.Aber wenn Sie die Adresse von nehmen
x
, erzwingen Sie, dass sie existiert. Sie werfen dann const weg und ändern es. Da der tatsächliche Speicherortx
geändert wurde, kann der Compiler ihn beim Lesen nicht lesenx
!Der Compiler wird möglicherweise schlau genug, um herauszufinden, wie man es vermeiden kann
ptr
, dem Lesen zu folgen*ptr
, aber oft sind sie es nicht. Fühlen Sie sich frei zu gehen undptr = ptr+argc-1
etwas Verwirrung zu stiften, wenn der Optimierer schlauer wird als Sie.Sie können eine benutzerdefinierte
operator[]
Datei bereitstellen , die den richtigen Artikel erhält.beides zu haben ist nützlich.
quelle
(&thing.a)[0]
kann es ändernx
da er weiß, dass Sie sie nicht auf definierte Weise ändern können. Eine ähnliche Optimierung kann auftreten, wenn Sieb
über ändern,(&blah.a)[1]
wenn der Compiler nachweisen kann, dass kein definierter Zugriff darauf vorhanden istb
, der dies ändern könnte. Eine solche Änderung kann aufgrund scheinbar harmloser Änderungen des Compilers, des umgebenden Codes oder was auch immer auftreten. Also selbst zu testen , dass es funktioniert , ist nicht ausreichend.Hier ist eine Möglichkeit, eine Proxy-Klasse zu verwenden, um auf Elemente in einem Member-Array nach Namen zuzugreifen. Es ist sehr C ++ und hat keinen Vorteil gegenüber Ref-Return-Accessor-Funktionen, außer für syntaktische Präferenzen. Dies überlastet den
->
Operator, um auf Elemente als Mitglieder zuzugreifen. Um akzeptabel zu sein, muss man sowohl die Syntax von accessors (d.a() = 5;
) ablehnen als auch die Verwendung->
mit einem Nicht-Zeiger-Objekt tolerieren . Ich gehe davon aus, dass dies auch Leser verwirren könnte, die mit dem Code nicht vertraut sind. Dies ist also eher ein ordentlicher Trick als etwas, das Sie in die Produktion einbauen möchten.Die
Data
Struktur in diesem Code enthält auch Überladungen für den Indexoperator, um auf indizierte Elemente innerhalb seinesar
Array- Elements sowie aufbegin
undend
Funktionen für die Iteration zuzugreifen . Außerdem sind alle diese Versionen mit Nicht-Konstanten- und Konstantenversionen überladen, die meiner Meinung nach der Vollständigkeit halber aufgenommen werden mussten.Wenn mit
Data
's->
auf ein Element nach Namen zugegriffen wird (wie folgt :)my_data->b = 5;
, wird einProxy
Objekt zurückgegeben. Da dieserProxy
r-Wert kein Zeiger ist, wird sein eigener->
Operator automatisch in der Kette aufgerufen, wodurch ein Zeiger auf sich selbst zurückgegeben wird. Auf diese Weise wird dasProxy
Objekt instanziiert und bleibt während der Auswertung des Anfangsausdrucks gültig.Bau eines
Proxy
Objekts auffüllt seinen 3 Referenzelementea
,b
undc
gemäß einem Zeiger in den Konstruktor übergeben, der Punkt zu einem Puffer angenommen wird, das mindestens 3 Werte , deren Typ wird als Template - Parameter angegebenT
. Anstatt benannte Referenzen zu verwenden, die Mitglieder derData
Klasse sind, wird Speicherplatz gespart, indem die Referenzen am Zugriffspunkt ausgefüllt werden (leider mit->
und nicht mit dem.
Operator).Um zu testen, wie gut das Optimierungsprogramm des Compilers alle durch die Verwendung von eingeführten Indirektionen eliminiert
Proxy
, enthält der folgende Code zwei Versionen vonmain()
. Die#if 1
Version verwendet die Operatoren->
und[]
, und die#if 0
Version führt die entsprechenden Prozeduren aus, jedoch nur durch direkten ZugriffData::ar
.Die
Nci()
Funktion generiert Laufzeit-Ganzzahlwerte zum Initialisieren von Array-Elementen, wodurch verhindert wird, dass der Optimierer nur konstante Werte direkt in jedenstd::cout
<<
Aufruf einfügt.Für gcc 6.2 generieren beide Versionen von -O3
main()
dieselbe Assembly (wechseln Sie zwischen#if 1
und#if 0
vor der erstenmain()
zu vergleichenden Assembly ): https://godbolt.org/g/QqRWZbquelle
main()
mit Timing-Funktionen! zBint getb(Data *d) { return (*d)->b; }
kompiliert nurmov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Ja,Data &d
würde die Syntax einfacher machen, aber ich habe einen Zeiger anstelle von ref verwendet, um die Seltsamkeit der Überladung auf->
diese Weise hervorzuheben .)int tmp[] = { a, b, c}; return tmp[idx];
Optimieren nicht weg, also ist es ordentlich, dass diese tut.operator.
in C ++ 17 vermisse .Wenn das Lesen von Werten ausreicht und die Effizienz keine Rolle spielt oder wenn Sie darauf vertrauen, dass Ihr Compiler die Dinge gut optimiert, oder wenn die Struktur nur aus diesen 3 Bytes besteht, können Sie dies sicher tun:
Für eine Nur-C ++ - Version möchten Sie wahrscheinlich
static_assert
überprüfen, ob dasstruct data
Standardlayout vorhanden ist, und stattdessen möglicherweise eine Ausnahme für einen ungültigen Index auslösen.quelle
Es ist illegal, aber es gibt eine Problemumgehung:
Jetzt können Sie v indizieren:
quelle