Ist es legal, in eine Struktur zu indizieren?

104

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, bund cwie (&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.

jcoder
quelle
51
Nein, es ist nicht "legal". Es ist undefiniertes Verhalten.
Sam Varshavchik
10
In diesem sehr einfachen Fall funktioniert dies für Sie, da der Compiler keine Auffüllung zwischen den Mitgliedern hinzufügt. Versuchen Sie es mit Strukturen, die unterschiedlich große Typen verwenden, und sie werden abstürzen.
Einige Programmierer Typ
7
Die Vergangenheit ausgraben - UB war früher ein Spitzname für Nasendämonen .
Adrian Colomitchi
21
Gut, hier stolpere ich herein, weil ich dem C-Tag folge, die Frage lese und dann eine Antwort schreibe, die nur für C gilt, weil ich das C ++ - Tag nicht gesehen habe. C und C ++ sind hier sehr unterschiedlich! C erlaubt das Punting mit Gewerkschaften, C ++ nicht.
Lundin
7
Wenn Sie als Array auf die Elemente zugreifen müssen, definieren Sie sie als Array. Wenn sie unterschiedliche Namen haben müssen, verwenden Sie die Namen. Der Versuch, Ihren Kuchen zu haben und ihn zu essen, führt schließlich zu Verdauungsstörungen - wahrscheinlich zum ungünstigsten denkbaren Zeitpunkt. (Ich denke, der Index 0 ist in C legal; der Index 1 oder 2 ist nicht legal. Es gibt Kontexte, in denen ein einzelnes Element als Array der Größe 1 behandelt wird.)
Jonathan Leffler

Antworten:

73

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):

[dcl.array / 1] : ... Ein Objekt vom Array-Typ enthält eine zusammenhängend zugewiesene nicht leere Menge von N Unterobjekten vom Typ T ...

Für Mitglieder gibt es jedoch keine solche zusammenhängende Anforderung:

[class.mem / 17] : ...; Anforderungen an die Ausrichtung der Implementierung können dazu führen, dass zwei benachbarte Mitglieder nicht unmittelbar nacheinander zugewiesen werden ...

Während die beiden obigen Anführungszeichen ausreichen sollten, um darauf hinzuweisen, warum die Indizierung in a structwie 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:

[expr.post//expr_sub/1] : Ein Postfix-Ausdruck, gefolgt von einem Ausdruck in eckigen Klammern, ist ein Postfix-Ausdruck. Einer der Ausdrücke muss ein Wert vom Typ "Array of T" oder ein Wert vom Typ "Zeiger auf T" sein, und der andere Wert muss ein Wert vom Typ "Scope ohne Aufzählung" oder vom Typ "Integral" sein. Das Ergebnis ist vom Typ "T". Der Typ „T“ muss ein vollständig definierter Objekttyp sein.66 Der Ausdruck E1[E2]ist (per Definition) identisch mit((E1)+(E2))

In den fetten Text des obigen Zitats eintauchen: Bezüglich des Hinzufügens eines integralen Typs zu einem Zeigertyp (beachten Sie die Betonung hier).

[expr.add / 4] : Wenn ein Ausdruck mit einem integralen Typ zu einem Zeiger hinzugefügt oder von diesem subtrahiert wird, hat das Ergebnis den Typ des Zeigeroperanden. Wenn die PExpressionspunkteElementx[i]von einem Array - Objektx mit n Elementen, die AusdrückeP + JundJ + P(wobeiJden Wertj) Punkt zu dem (ggf. hypothetischen) Elementex[i + j] wenn0 ≤ i + j ≤ n; Andernfalls ist das Verhalten undefiniert. ...

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.

WhiZTiM
quelle
1
@jcoder Es ist in class.mem definiert . Den letzten Text finden Sie im letzten Absatz.
NathanOliver
4
Striktes Alising ist hier nicht relevant. Der Typ int ist im Aggregattyp enthalten, und dieser Typ kann den Alias ​​int haben. - 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),
2501
1
@ Die Downvoters, möchten Sie kommentieren? - und zu verbessern oder darauf hinzuweisen, wo diese Antwort falsch ist?
WhiZTiM
4
Striktes Aliasing spielt dabei keine Rolle. Das Auffüllen ist nicht Teil des gespeicherten Werts eines Objekts. Auch diese Antwort geht nicht auf den häufigsten Fall ein: Was passiert, wenn keine Polsterung vorhanden ist? Würde empfehlen, diese Antwort tatsächlich zu löschen.
MM
1
Getan! Ich habe den Absatz über striktes Aliasing entfernt.
WhiZTiM
48

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:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

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.

2501
quelle
3
Beachten Sie, dass dies nur funktioniert, wenn die Klasse ein Standardlayouttyp ist. Wenn nicht, ist es immer noch UB.
NathanOliver
@ NathanOliver Ich sollte erwähnen, dass meine Antwort nur für C. Edited gilt. Dies ist eines der Probleme solcher Fragen mit zwei Tag-Sprachen.
2501
Danke, und deshalb habe ich getrennt nach C ++ und C gefragt, da es interessant ist, die Unterschiede zu erkennen
jcoder
@NathanOliver Die Adresse des ersten Mitglieds stimmt garantiert mit der Adresse der C ++ - Klasse überein, wenn es sich um ein Standardlayout handelt. Dies garantiert jedoch weder, dass der Zugriff genau definiert ist, noch impliziert dies, dass solche Zugriffe auf andere Klassen nicht definiert sind.
Potatoswatter
Würden Sie sagen, dass dies char* p = ( char* )&thing.a + offsetof( thing , b );zu undefiniertem Verhalten führt?
MM
43

Wenn Sie es in C ++ wirklich brauchen, erstellen Sie den Operator []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

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:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Diese Lösung würde die Strukturgröße ändern, sodass Sie auch Methoden verwenden können:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};
Slava
quelle
1
Ich würde gerne sehen, wie dies zerlegt wird, im Gegensatz zur Demontage eines C-Programms mit Typ Punning. Aber, aber ... C ++ ist so schnell wie C ... richtig? Richtig?
Lundin
6
@Lundin Wenn Sie sich für die Geschwindigkeit dieser Konstruktion interessieren, sollten die Daten zunächst als Array und nicht als separate Felder organisiert werden.
Slava
2
@Lundin in beiden meinst du unlesbares und undefiniertes Verhalten? Nein Danke.
Slava
1
@Lundin Operator Overloading ist eine syntaktische Funktion zur Kompilierungszeit, die im Vergleich zu normalen Funktionen keinen Overhead verursacht. Schauen Sie sich godbolt.org/g/vqhREz an, um zu sehen, was der Compiler tatsächlich tut, wenn er den C ++ - und C-Code kompiliert. Es ist erstaunlich, was sie tun und was man von ihnen erwartet. Ich persönlich bevorzuge eine bessere Typensicherheit und Ausdruckskraft von C ++ gegenüber einer Million Mal. Und es funktioniert die ganze Zeit, ohne sich auf Annahmen über das Auffüllen zu verlassen.
Jens
2
Diese Referenzen werden mindestens die Größe der Sache verdoppeln. Tu es einfach thing.a().
TC
14

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.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;
Geschichtenerzähler - Unslander Monica
quelle
1
Dies nutzt die Sprachfunktionen und ist daher gut definiert und, wie ich annehme, effizient. Beste Antwort.
Peter - Monica
2
Effizient annehmen? Ich nehme das Gegenteil an. Schauen Sie sich den generierten Code an.
JDługosz
1
@ JDługosz, du hast ganz recht. Bei einem Blick auf die generierte Assembly scheint gcc 6.2 Code zu erstellen, der der Verwendung offsetoffin C entspricht.
StoryTeller - Unslander Monica
3
Sie können die Dinge auch verbessern, indem Sie arr constexpr machen. Dadurch wird eine einzelne feste Nachschlagetabelle im Datenabschnitt erstellt, anstatt sie im laufenden Betrieb zu erstellen.
Tim
10

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 auszusetzen switch(). 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.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

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.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

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

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Dies ist offensichtlich schrecklich im Vergleich zur gewerkschaftsbasierten C-Version (oder GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]
Peter Cordes
quelle
@ MM: guter Punkt. Es ist eher eine Antwort auf verschiedene Kommentare und eine Alternative zu Slavas Antwort. Ich habe das Eröffnungsbit umformuliert, damit es zumindest als Antwort auf die ursprüngliche Frage beginnt. Vielen Dank für den Hinweis.
Peter Cordes
Während gewerkschaftsbasiertes Typ-Punning in gcc und clang zu funktionieren scheint, während der []Operator direkt auf einem Gewerkschaftsmitglied verwendet wird, definiert der Standard dies array[index]als äquivalent zu *((array)+(index)), und weder gcc noch clang erkennen zuverlässig, dass ein Zugriff auf *((someUnion.array)+(index))ein Zugriff auf ist someUnion. Die einzige Erklärung , die ich sehen kann , ist , dass someUnion.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.
Supercat
9

In C ++ ist dies meist undefiniertes Verhalten (es hängt von welchem ​​Index ab).

Aus [expr.unary.op]:

Für die Zwecke der Zeigerarithmetik (5.7) und des Vergleichs (5.9, 5.10) wird ein Objekt, das kein Array-Element ist, dessen Adresse auf diese Weise verwendet wird, als zu einem Array mit einem Element vom Typ gehörend betrachtet T.

Es &thing.awird daher angenommen, dass sich der Ausdruck auf ein Array von eins bezieht int.

Aus [Ausdruck]:

Der Ausdruck E1[E2]ist (per Definition) identisch mit*((E1)+(E2))

Und von [expr.add]:

Wenn ein Ausdruck mit einem integralen Typ zu einem Zeiger hinzugefügt oder von diesem subtrahiert wird, hat das Ergebnis den Typ des Zeigeroperanden. Wenn der Ausdruck Pauf ein Element x[i]eines Array-Objekts xmit nElementen zeigt, zeigen die Ausdrücke P + Jund J + P(wo Jhat der Wert j) auf das (möglicherweise hypothetische) Element x[i + j]if 0 <= i + j <= n; Andernfalls ist das Verhalten undefiniert.

(&thing.a)[0]ist perfekt geformt, da &thing.aes 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 Voraussetzung 0 <= i + j <= n, da wir i == 0, j == 2, n == 1. Das einfache Konstruieren des Zeigers &thing.a + 2ist 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:

Ein Wert eines Zeigertyps, der ein Zeiger auf oder nach dem Ende eines Objekts ist, repräsentiert die Adresse des ersten Bytes im Speicher (1.7), das vom Objekt 53 belegt ist, oder des ersten Bytes im Speicher nach dem Ende des vom Objekt belegten Speichers , beziehungsweise. [Hinweis: Ein Zeiger hinter dem Ende eines Objekts (5.7) verweist nicht auf ein nicht verwandtes Objekt des Objekttyps, das sich möglicherweise an dieser Adresse befindet.

Daher ist das Nehmen des Zeigers &thing.a + 1ein definiertes Verhalten, aber das Dereferenzieren ist undefiniert, da es auf nichts zeigt.

Barry
quelle
Das Auswerten von (& thing.a) + 1 ist nur legal, da ein Zeiger hinter dem Ende eines Arrays legal ist. Das Lesen oder Schreiben der dort gespeicherten Daten ist ein undefiniertes Verhalten. Der Vergleich mit & thing.b mit <,>, <=,> = ist ein undefiniertes Verhalten. (& thing.a) + 2 ist absolut illegal.
Gnasher729
@ gnasher729 Ja, es lohnt sich, die Antwort noch etwas zu klären.
Barry
Das (&thing.a + 1)ist ein interessanter Fall, den ich nicht behandelt habe. +1! ... Nur neugierig, sind Sie im ISO C ++ - Komitee?
WhiZTiM
Dies ist auch ein sehr wichtiger Fall, da sonst jede Schleife, die Zeiger als halboffenes Intervall verwendet, UB wäre.
Jens
In Bezug auf das letzte Standardzitat. C ++ muss hier besser angegeben werden als C.
2501
8

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 bezieht thing.b. Diese Tatsache kann verwendet werden, um Lese- und Schreibvorgänge neu zu ordnen und das thing.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.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

Hier erhalten Sie normalerweise einen Compiler, der 7, dann 2! = 7 und dann zwei identische Zeiger sagt. trotz der Tatsache, dass ptrdarauf hinweist x. Der Compiler nimmt die Tatsache, dass xes sich um einen konstanten Wert handelt, um sich nicht die Mühe zu machen, ihn zu lesen, wenn Sie nach dem Wert von fragen x.

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 Speicherort xgeändert wurde, kann der Compiler ihn beim Lesen nicht lesen x!

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 und ptr = ptr+argc-1etwas Verwirrung zu stiften, wenn der Optimierer schlauer wird als Sie.

Sie können eine benutzerdefinierte operator[]Datei bereitstellen , die den richtigen Artikel erhält.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

beides zu haben ist nützlich.

Yakk - Adam Nevraumont
quelle
"Die Tatsache, dass es kein Mitglied eines Arrays ist, bedeutet, dass der Compiler davon ausgehen kann, dass kein [] basierter Array-Zugriff es ändern kann." - nicht wahr, zB (&thing.a)[0]kann es ändern
MM
Ich sehe nicht, wie das const-Beispiel etwas mit der Frage zu tun hat. Dies schlägt nur fehl, weil es eine bestimmte Regel gibt, dass ein const-Objekt nicht geändert werden darf, aus keinem anderen Grund.
MM
1
@MM, es ist kein Beispiel für die Indizierung in eine Struktur, aber es ist ein sehr gutes Beispiel dafür , wie durch seine auf Referenz etwas undefiniertes Verhalten mit scheinbarer Position im Speicher, kann in unterschiedlicher Ausgabe führt als erwartet, da der Compiler kann etwas anderes tut mit die UB als Sie es wollten.
Wildcard
@MM Leider gibt es keinen anderen Array-Zugriff als einen trivialen über einen Zeiger auf das Objekt selbst. Und die zweite ist nur ein Beispiel für leicht erkennbare Nebenwirkungen von undefiniertem Verhalten. Der Compiler optimiert die Lesevorgänge auf, xda er weiß, dass Sie sie nicht auf definierte Weise ändern können. Eine ähnliche Optimierung kann auftreten, wenn Sie büber ändern, (&blah.a)[1]wenn der Compiler nachweisen kann, dass kein definierter Zugriff darauf vorhanden ist b, 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.
Yakk - Adam Nevraumont
6

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 DataStruktur in diesem Code enthält auch Überladungen für den Indexoperator, um auf indizierte Elemente innerhalb seines arArray- Elements sowie auf beginund endFunktionen 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 ein ProxyObjekt zurückgegeben. Da dieser Proxyr-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 das ProxyObjekt instanziiert und bleibt während der Auswertung des Anfangsausdrucks gültig.

Bau eines Proxy Objekts auffüllt seinen 3 Referenzelemente a, bund cgemäß einem Zeiger in den Konstruktor übergeben, der Punkt zu einem Puffer angenommen wird, das mindestens 3 Werte , deren Typ wird als Template - Parameter angegeben T. Anstatt benannte Referenzen zu verwenden, die Mitglieder der DataKlasse 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 von main(). Die #if 1Version verwendet die Operatoren ->und [], und die #if 0Version führt die entsprechenden Prozeduren aus, jedoch nur durch direkten Zugriff Data::ar.

Die Nci()Funktion generiert Laufzeit-Ganzzahlwerte zum Initialisieren von Array-Elementen, wodurch verhindert wird, dass der Optimierer nur konstante Werte direkt in jeden std::cout <<Aufruf einfügt.

Für gcc 6.2 generieren beide Versionen von -O3 main()dieselbe Assembly (wechseln Sie zwischen #if 1und #if 0vor der ersten main()zu vergleichenden Assembly ): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif
Christopher Oicles
quelle
Raffiniert. Upvoted vor allem, weil Sie bewiesen haben, dass dies weg optimiert. Übrigens können Sie das viel einfacher tun, indem Sie eine sehr einfache Funktion schreiben, nicht eine ganze main()mit Timing-Funktionen! zB int getb(Data *d) { return (*d)->b; }kompiliert nur mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Ja, Data &dwürde die Syntax einfacher machen, aber ich habe einen Zeiger anstelle von ref verwendet, um die Seltsamkeit der Überladung auf ->diese Weise hervorzuheben .)
Peter Cordes
Jedenfalls ist das cool. Andere Ideen wie int tmp[] = { a, b, c}; return tmp[idx];Optimieren nicht weg, also ist es ordentlich, dass diese tut.
Peter Cordes
Ein weiterer Grund, den ich operator.in C ++ 17 vermisse .
Jens
2

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:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Für eine Nur-C ++ - Version möchten Sie wahrscheinlich static_assertüberprüfen, ob das struct dataStandardlayout vorhanden ist, und stattdessen möglicherweise eine Ausnahme für einen ungültigen Index auslösen.

Hyde
quelle
1

Es ist illegal, aber es gibt eine Problemumgehung:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Jetzt können Sie v indizieren:

Sven Nilsson
quelle
6
Viele C ++ - Projekte halten Downcasting überall für in Ordnung. Wir sollten immer noch keine schlechten Praktiken predigen.
Geschichtenerzähler - Unslander Monica
2
Die Gewerkschaft löst das strikte Aliasing-Problem in beiden Sprachen. Typ-Punning durch Gewerkschaften ist jedoch nur in C in Ordnung, nicht in C ++.
Lundin
1
Trotzdem wäre ich nicht überrascht, wenn dies auf 100% aller C ++ - Compiler funktioniert. je.
Sven Nilsson
1
Sie können es in gcc mit den aggressivsten Optimierungseinstellungen versuchen.
Lundin
1
@Lundin: Union Type Punning ist in GNU C ++ als Erweiterung über ISO C ++ legal . Es scheint im Handbuch nicht sehr klar angegeben zu sein , aber da bin ich mir ziemlich sicher. Diese Antwort muss jedoch erklären, wo sie gültig ist und wo nicht.
Peter Cordes