È legale indicizzare in una struttura?


104

Indipendentemente da quanto "cattivo" sia il codice e supponendo che l'allineamento ecc. Non sia un problema sul compilatore / piattaforma, questo comportamento non è definito o non funziona?

Se ho una struttura come questa: -

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

struct data thing;

E ' legale per l'accesso a, be ccome (&thing.a)[0], (&thing.a)[1]e (&thing.a)[2]?

In ogni caso, su ogni compilatore e piattaforma su cui l'ho provato, con ogni impostazione che ho provato ha "funzionato". Sono solo preoccupato che il compilatore potrebbe non rendersi conto che b e cosa [1] sono la stessa cosa e memorizza in "b" potrebbe essere messo in un registro e cosa [1] legge il valore sbagliato dalla memoria (per esempio). In ogni caso ho provato ha fatto la cosa giusta però. (Mi rendo conto ovviamente che non prova molto)

Questo non è il mio codice; è il codice con cui devo lavorare, mi interessa sapere se si tratta di un codice errato o non funzionante poiché il diverso influisce sulle mie priorità per cambiarlo molto :)

Contrassegnato C e C ++. Mi interessa principalmente C ++ ma anche C se è diverso, solo per interesse.


51
No, non è "legale". È un comportamento indefinito.
Sam Varshavchik

10
Funziona per te in questo caso molto semplice perché il compilatore non aggiunge alcun riempimento tra i membri. Prova con strutture che utilizzano tipi di dimensioni diverse e si bloccheranno.
Un tizio programmatore

7
Scavando nel passato - UB era soprannominato demoni nasali .
Adrian Colomitchi

21
Bene, fantastico, qui mi imbatto perché seguo il tag C, leggo la domanda, quindi scrivo una risposta che si applica solo a C, perché non ho visto il tag C ++. C e C ++ sono molto diversi qui! Il C consente il gioco di parole con le unioni, il C ++ no.
Lundin

7
Se è necessario accedere agli elementi come array, definirli come array. Se devono avere nomi diversi, usa i nomi. Cercare di avere la tua torta e mangiarla alla fine porterà a un'indigestione, probabilmente nel momento più scomodo immaginabile. (Penso che l'indice 0 sia legale in C; l'indice 1 o 2 no. Ci sono contesti in cui un singolo elemento viene trattato come un array di dimensione 1.)
Jonathan Leffler

Risposte:


73

È illegale 1 . Questo è un comportamento indefinito in C ++.

Stai prendendo i membri in modo array, ma ecco cosa dice lo standard C ++ (enfasi mia):

[dcl.array / 1] : ... Un oggetto di tipo array contiene uninsieme non vuoto allocato in modo contiguo di N suboggetti di tipo T ...

Ma, per i membri, non esiste un requisito così contiguo :

[class.mem / 17] : ...; I requisiti di allineamento dell'implementazione potrebbero impedire l' assegnazione immediata di due membri adiacenti l'uno dopo l'altro ...

Mentre le due citazioni precedenti dovrebbero essere sufficienti per suggerire perché l'indicizzazione in a structcome hai fatto non è un comportamento definito dallo standard C ++, facciamo un esempio: guarda l'espressione (&thing.a)[2]- Riguardo all'operatore pedice:

[expr.post//expr.sub/1] : un'espressione postfissa seguita da un'espressione tra parentesi quadre è un'espressione postfissa. Una delle espressioni deve essere un valore collante di tipo "matrice di T" o un prvalore di tipo "puntatore a T" e l'altra deve essere un valore di enumerazione senza ambito o tipo integrale. Il risultato è di tipo "T". Il tipo "T" deve essere un tipo di oggetto completamente definito.66 L'espressione E1[E2]è identica (per definizione) a((E1)+(E2))

Scavando nel testo in grassetto della citazione sopra: riguardo all'aggiunta di un tipo integrale a un tipo di puntatore (notare l'enfasi qui) ..

[expr.add / 4] : quando un'espressione con un tipo integrale viene aggiunta o sottratta da un puntatore, il risultato ha il tipo dell'operando del puntatore. Se l'espressionePpunta a un elementox[i]di un oggetto xarray con n elementi, le espressioniP + JeJ + P(doveJha il valorej) puntano all'elemento (possibilmente ipotetico)x[i + j] if0 ≤ i + j ≤ n; in caso contrario , il comportamento è indefinito. ...

Notare il requisito dell'array per la clausola if ; altrimenti il contrario nella citazione sopra. L'espressione (&thing.a)[2]ovviamente non si qualifica per la clausola if ; Quindi, comportamento indefinito.


Nota a margine: sebbene abbia ampiamente sperimentato il codice e le sue variazioni su vari compilatori e non introducano alcun riempimento qui, ( funziona ); dal punto di vista della manutenzione, il codice è estremamente fragile. dovresti comunque affermare che l'implementazione ha assegnato i membri in modo contiguo prima di fare ciò. E rimani nei limiti :-). Ma è ancora un comportamento indefinito ...

Alcune soluzioni valide (con un comportamento definito) sono state fornite da altre risposte.



Come giustamente sottolineato nei commenti, [basic.lval / 8] , che era nella mia precedente modifica, non si applica. Grazie @ 2501 e @MM

1 : Vedi la risposta di @ Barry a questa domanda per l'unico caso legale in cui puoi accedere al thing.amembro della struttura tramite questa parte.


1
@jcoder È definito in class.mem . Vedere l'ultimo paragrafo per il testo effettivo.
NathanOliver

4
Alising rigoroso non è rilevante qui. Il tipo int è contenuto nel tipo aggregato e questo tipo può alias int. - 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
@ I downvoters, vi interessa commentare? - e per migliorare o sottolineare dove questa risposta è sbagliata?
WhiZTiM

4
Il rigoroso aliasing è irrilevante per questo. Il riempimento non fa parte del valore memorizzato di un oggetto. Anche questa risposta non riesce ad affrontare il caso più comune: cosa succede quando non c'è padding. Consiglierei di eliminare questa risposta in realtà.
MM

1
Fatto! Ho rimosso il paragrafo sull'aliasing rigoroso.
WhiZTiM

48

No. In C, questo è un comportamento indefinito anche se non c'è riempimento.

La cosa che causa un comportamento indefinito è l'accesso fuori dai limiti 1 . Quando hai uno scalare (membri a, b, c nella struttura) e provi a usarlo come array 2 per accedere al prossimo elemento ipotetico, provi un comportamento indefinito, anche se capita che ci sia un altro oggetto dello stesso tipo in quell'indirizzo.

Tuttavia è possibile utilizzare l'indirizzo dell'oggetto struct e calcolare l'offset in un membro specifico:

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

Questo deve essere fatto individualmente per ogni membro, ma può essere inserito in una funzione che assomiglia a un accesso ad array.


1 (Citato da: ISO / IEC 9899: 201x 6.5.6 Operatori additivi 8)
Se il risultato punta uno oltre l'ultimo elemento dell'oggetto array, non deve essere utilizzato come operando di un operatore unario * che viene valutato.

2 (Citato da: ISO / IEC 9899: 201x 6.5.6 Operatori additivi 7)
Ai fini di questi operatori, un puntatore a un oggetto che non è un elemento di un array si comporta come un puntatore al primo elemento di un array di lunghezza uno con il tipo dell'oggetto come tipo di elemento.


3
Nota che funziona solo se la classe è un tipo di layout standard. Altrimenti è ancora UB.
NathanOliver

@NathanOliver Devo dire che la mia risposta si applica solo a C. Edited. Questo è uno dei problemi di queste domande in linguaggio dual tag.
2501

Grazie, ed è per questo che ho chiesto separatamente C ++ e C poiché è interessante conoscere le differenze
jcoder

@NathanOliver Si garantisce che l'indirizzo del primo membro coincida con l'indirizzo della classe C ++ se si tratta di layout standard. Tuttavia, ciò non garantisce che l'accesso sia ben definito né implica che tali accessi su altre classi siano indefiniti.
Potatoswatter

diresti che questo char* p = ( char* )&thing.a + offsetof( thing , b );porta a un comportamento indefinito?
MM

43

In C ++, se ne hai davvero bisogno, crea l'operatore []:

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

non solo è garantito il funzionamento, ma l'utilizzo è più semplice, non è necessario scrivere espressioni illeggibili (&thing.a)[0]

Nota: questa risposta è data supponendo che tu abbia già una struttura con campi e devi aggiungere l'accesso tramite index. Se la velocità è un problema e puoi cambiare la struttura, questo potrebbe essere più efficace:

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

Questa soluzione cambierebbe la dimensione della struttura in modo da poter utilizzare anche metodi:

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

1
Mi piacerebbe vedere il disassemblaggio di questo, contro lo smontaggio di un programma C usando il tipo di giochi di parole. Ma, ma ... C ++ è veloce quanto C ... giusto? Destra?
Lundin

6
@Lundin, se ti interessa la velocità di questa costruzione, i dati dovrebbero essere organizzati come un array in primo luogo, non come campi separati.
Slava

2
@Lundin in entrambi intendi comportamento illeggibile e indefinito? No grazie.
Slava

1
@Lundin Operator overloading è una caratteristica sintattica in fase di compilazione che non induce alcun sovraccarico rispetto alle normali funzioni. Dai un'occhiata a godbolt.org/g/vqhREz per vedere cosa fa effettivamente il compilatore quando compila il codice C ++ e C. È incredibile quello che fanno e quello che ci si aspetta che facciano. Personalmente preferisco una maggiore sicurezza dei tipi ed espressività di C ++ rispetto a C un milione di volte. E funziona sempre senza fare affidamento su supposizioni sull'imbottitura.
Jens

2
Quei riferimenti raddoppieranno almeno la dimensione della cosa. Fallo e basta thing.a().
TC

14

Per c ++: se è necessario accedere a un membro senza conoscerne il nome, è possibile utilizzare un puntatore alla variabile del membro.

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;

1
Questo utilizza le strutture linguistiche e, di conseguenza, è ben definito e, come presumo, efficiente. Migliore risposta.
Peter - Ripristina Monica il

2
Presumi efficiente? Suppongo il contrario. Guarda il codice generato.
JDługosz

1
@ JDługosz, hai perfettamente ragione. Facendo una sbirciatina al generato assemblea, sembra gcc 6.2 crea il codice equivalente all'utilizzo offsetoffin C.
Storyteller - Unslander Monica

3
puoi anche migliorare le cose facendo arr constexpr. Questo creerà una singola tabella di ricerca fissa nella sezione dati invece di crearla al volo.
Tim

10

In ISO C99 / C11, il gioco di parole basato sull'unione è legale, quindi puoi usarlo invece di indicizzare i puntatori a non array (vedi varie altre risposte).

ISO C ++ non consente il gioco di parole basato sull'unione. GNU C ++ lo fa, come estensione , e penso che alcuni altri compilatori che non supportano le estensioni GNU in generale supportano la tipizzazione di tipo union. Ma questo non ti aiuta a scrivere codice strettamente portabile.

Con le versioni correnti di gcc e clang, scrivere una funzione membro C ++ usando a switch(idx)per selezionare un membro ottimizzerà gli indici delle costanti in fase di compilazione, ma produrrà terribili asm ramificati per gli indici di runtime. Non c'è niente di intrinsecamente sbagliato switch()in questo; questo è semplicemente un bug di mancata ottimizzazione nei compilatori attuali. Potrebbero compilare la funzione switch () di Slava in modo efficiente.


La soluzione / rimedio a questo è farlo nell'altro modo: dare alla tua classe / struttura un membro dell'array e scrivere funzioni accessor per allegare nomi a elementi specifici.

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]; }
};

Possiamo dare uno sguardo all'output asm per diversi casi d'uso, sul compilatore Godbolt explorer . Queste sono funzioni complete x86-64 System V, con l'istruzione RET finale omessa per mostrare meglio cosa otterresti quando sono in linea. ARM / MIPS / qualunque cosa sarebbe simile.

# 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]

In confronto, la risposta di @ Slava usando a switch()per C ++ rende asm come questo per un indice di variabile di runtime. (Codice nel precedente link Godbolt).

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

Questo è ovviamente terribile, rispetto alla versione di punning di tipo basata sull'unione C (o 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]

@ MM: buon punto. È più una risposta a vari commenti e un'alternativa alla risposta di Slava. Ho riformulato la parte iniziale, quindi almeno inizia come una risposta alla domanda originale. Grazie per la segnalazione.
Peter Cordes

Mentre il punning di tipo basato sull'unione sembra funzionare in gcc e clang mentre si usa l' []operatore direttamente su un membro del sindacato, lo Standard definisce array[index]come equivalente a *((array)+(index)), e né gcc né clang riconosceranno in modo affidabile che un accesso a *((someUnion.array)+(index))è un accesso a someUnion. L'unica spiegazione che posso vedere è che someUnion.array[index]*((someUnion.array)+(index))non sono definiti dallo Standard, ma sono semplicemente estensioni popolari, e gcc / clang ha scelto di non supportare la seconda ma sembra supportare la prima, almeno per ora.
supercat

9

In C ++, questo è per lo più un comportamento indefinito (dipende da quale indice).

Da [expr.unary.op]:

Ai fini dell'aritmetica dei puntatori (5.7) e del confronto (5.9, 5.10), un oggetto che non è un elemento di array il cui indirizzo è preso in questo modo è considerato appartenere a un array con un elemento di tipo T.

L'espressione &thing.aè quindi considerata riferirsi a un array di uno int.

Da [expr.sub]:

L'espressione E1[E2]è identica (per definizione) a*((E1)+(E2))

E da [expr.add]:

Quando un'espressione con un tipo integrale viene aggiunta o sottratta da un puntatore, il risultato ha il tipo dell'operando del puntatore. Se l'espressione Ppunta a un elemento x[i]di un oggetto array xcon nelementi, le espressioni P + Je J + P(dove Jha il valore j) puntano all'elemento (possibilmente ipotetico) x[i + j]if 0 <= i + j <= n; in caso contrario, il comportamento è indefinito.

(&thing.a)[0]è perfettamente formato perché &thing.aè considerato un array di dimensione 1 e stiamo prendendo quel primo indice. Questo è un indice consentito da prendere.

(&thing.a)[2]viola il presupposto che 0 <= i + j <= n, dal momento che abbiamo i == 0, j == 2, n == 1. La semplice costruzione del puntatore &thing.a + 2è un comportamento indefinito.

(&thing.a)[1]è il caso interessante. In realtà non viola nulla in [expr.add]. Ci è permesso prendere un puntatore uno dopo la fine dell'array, che sarebbe. Qui, passiamo a una nota in [basic.compound]:

Un valore di un tipo di puntatore che è un puntatore alla o oltre la fine di un oggetto rappresenta l'indirizzo del primo byte in memoria (1.7) occupato dall'oggetto53 o il primo byte in memoria dopo la fine della memoria occupata dall'oggetto , rispettivamente. [Nota: un puntatore oltre la fine di un oggetto (5.7) non è considerato puntare a un oggetto non correlato del tipo di oggetto che potrebbe trovarsi a quell'indirizzo.

Quindi, prendere il puntatore &thing.a + 1è un comportamento definito, ma dereferenziarlo non è definito perché non punta a nulla.


Valutare (e thing.a) + 1 è quasi legale perché un puntatore oltre la fine di un array è legale; leggere o scrivere i dati memorizzati è un comportamento indefinito, il confronto con & thing.b con <,>, <=,> = è un comportamento indefinito. (& cosa.a) + 2 è assolutamente illegale.
gnasher729

@ gnasher729 Sì, vale la pena chiarire ulteriormente la risposta.
Barry

Il (&thing.a + 1)è un caso interessante non sono riuscito a coprire. +1! ... Sono solo curioso, fai parte del comitato ISO C ++?
WhiZTiM

È anche un caso molto importante perché altrimenti ogni ciclo che utilizza i puntatori come intervallo semiaperto sarebbe UB.
Jens

Per quanto riguarda l'ultima citazione standard. C ++ deve essere specificato meglio di C qui.
2501

8

Questo è un comportamento indefinito.

Ci sono molte regole in C ++ che tentano di dare al compilatore qualche speranza di capire cosa stai facendo, in modo che possa ragionarci su e ottimizzarlo.

Esistono regole sull'aliasing (accesso ai dati tramite due diversi tipi di puntatore), limiti di array, ecc.

Quando si dispone di una variabile x, il fatto che non sia un membro di un array significa che il compilatore può presumere che nessun []accesso basato sull'array possa modificarla. Quindi non deve ricaricare costantemente i dati dalla memoria ogni volta che lo usi; solo se qualcuno avesse potuto modificarlo dal suo nome .

Quindi si (&thing.a)[1]può presumere che il compilatore non si riferisca a thing.b. Può usare questo fatto per riordinare le letture e le scritture thing.b, invalidando ciò che vuoi che faccia senza invalidare ciò che gli hai effettivamente detto di fare.

Un classico esempio di questo è il lancio di 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';

qui si ottiene tipicamente un compilatore che dice 7 poi 2! = 7 e poi due puntatori identici; nonostante il fatto che ptrsta indicando x. Il compilatore prende il fatto che xè un valore costante per non preoccuparsi di leggerlo quando chiedi il valore di x.

Ma quando prendi l'indirizzo di x, lo costringi ad esistere. Quindi getti via const e lo modifichi. Quindi la posizione effettiva nella memoria in cui xè stata modificata, il compilatore è libero di non leggerlo effettivamente durante la lettura x!

Il compilatore può diventare abbastanza intelligente da capire come evitare anche di seguire ptrper leggere *ptr, ma spesso non lo sono. Sentiti libero di andare a usare ptr = ptr+argc-1o un po 'di confusione se l'ottimizzatore sta diventando più intelligente di te.

Puoi fornire una personalizzazione operator[]che ottenga l'articolo giusto.

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

avere entrambi è utile.


"il fatto che non sia un membro di un array significa che il compilatore può presumere che nessun accesso all'array basato su [] possa modificarlo." - Non è vero, ad esempio (&thing.a)[0]può modificarlo
MM

Non vedo come l'esempio const abbia qualcosa a che fare con la domanda. Ciò fallisce solo perché esiste una regola specifica per cui un oggetto const non può essere modificato, non per altri motivi.
MM

1
@MM, non è un esempio di indicizzazione in una struttura, ma è una molto buona illustrazione di come usare comportamento indefinito di riferimento qualcosa dalla sua apparente posizione nella memoria, possono dare un'uscita diverso da quello previsto, perché il compilatore può fare qualcos'altro con l'UB di quanto volevi.
Wildcard

@MM Siamo spiacenti, nessun accesso all'array diverso da quello banale tramite un puntatore all'oggetto stesso. E il secondo è solo un esempio di effetti collaterali facili da vedere di un comportamento indefinito; il compilatore ottimizza le letture in xperché sa che non puoi cambiarlo in un modo definito. Un'ottimizzazione simile potrebbe verificarsi quando si modifica btramite (&blah.a)[1]se il compilatore può provare che non esiste un accesso definito bche potrebbe alterarlo; un tale cambiamento potrebbe verificarsi a causa di cambiamenti apparentemente innocui nel compilatore, nel codice circostante o altro. Quindi anche testare che funzioni non è sufficiente.
Yakk - Adam Nevraumont

6

Ecco un modo per utilizzare una classe proxy per accedere agli elementi in un array di membri in base al nome. È molto C ++ e non ha alcun vantaggio rispetto alle funzioni di accesso che restituiscono ref, ad eccezione della preferenza sintattica. Questo sovraccarica l' ->operatore per accedere agli elementi come membri, quindi per essere accettabile, è necessario non apprezzare la sintassi di accessors ( d.a() = 5;), oltre a tollerare l'utilizzo ->con un oggetto non puntatore. Mi aspetto che questo possa anche confondere i lettori che non hanno familiarità con il codice, quindi questo potrebbe essere più un trucco accurato di qualcosa che vuoi mettere in produzione.

Lo Datastruct in questo codice include anche sovraccarichi per l'operatore di indicizzazione, per accedere indicizzata elementi all'interno del suo armembro di matrice, nonché begine endfunzioni, per l'iterazione. Inoltre, tutti questi sono sovraccarichi di versioni non const e const, che ho ritenuto necessario includere per completezza.

Quando Dataè-> si usa per accedere a un elemento in base al nome (come questo :), viene restituito my_data->b = 5;un Proxyoggetto. Quindi, poiché questo Proxyvalore non è un puntatore, il suo ->operatore viene chiamato a catena automatica, che restituisce un puntatore a se stesso. In questo modo, l' Proxyoggetto viene istanziato e rimane valido durante la valutazione dell'espressione iniziale.

La costruzione di un Proxyoggetto popola i suoi 3 membri di riferimento a,b e csecondo un puntatore passato nel costruttore, che si presume scegliere un tampone contenente almeno 3 valori cui tipo è dato come parametro di template T. Quindi, invece di usare riferimenti con nome che sono membri della Dataclasse, questo risparmia memoria popolando i riferimenti nel punto di accesso (ma sfortunatamente, usando ->e non l' .operatore).

Per verificare quanto l'ottimizzatore del compilatore elimini tutte le indicazioni indirette introdotte dall'uso di Proxy, il codice seguente include 2 versioni dimain() . La #if 1versione utilizza la ->e []operatori, e la #if 0versione esegue l'equivalente serie di procedure, ma solo accedendo direttamente Data::ar.

La Nci()funzione genera valori interi di runtime per l'inizializzazione degli elementi dell'array, il che impedisce all'ottimizzatore di inserire semplicemente i valori costanti direttamente in ogni std::cout <<chiamata.

Per gcc 6.2, usando -O3, entrambe le versioni main()generano lo stesso assembly ( passa da #if 1e #if 0prima del primo main()per confrontare): 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

Nifty. Votato principalmente perché hai dimostrato che questo ottimizza. A proposito, puoi farlo molto più facilmente scrivendo una funzione molto semplice, non un intero main()con funzioni di temporizzazione! es. int getb(Data *d) { return (*d)->b; }compila solo in mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Sì, Data &drenderebbe la sintassi più semplice, ma ho usato un puntatore invece di ref per evidenziare la stranezza del sovraccarico in ->questo modo.)
Peter Cordes

Comunque, questo è fantastico. Altre idee come int tmp[] = { a, b, c}; return tmp[idx];non ottimizzare, quindi è carino che questa lo faccia.
Peter Cordes

Un motivo in più che mi manca operator.in C ++ 17.
Jens

2

Se leggere i valori è sufficiente e l'efficienza non è un problema, o se ti fidi del tuo compilatore per ottimizzare bene le cose, o se la struttura è solo quella di 3 byte, puoi tranquillamente farlo:

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];
}

Per la versione solo C ++, probabilmente vorrai usare static_assertper verificare che struct dataabbia un layout standard e forse invece lanciare un'eccezione su un indice non valido.


1

È illegale, ma c'è una soluzione alternativa:

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

Ora puoi indicizzare v:


6
Molti progetti C ++ pensano che il downcasting ovunque vada bene. Non dovremmo ancora predicare cattive pratiche.
StoryTeller - Unslander Monica

2
L'unione risolve il problema del rigoroso aliasing in entrambe le lingue. Ma il gioco di parole tramite le unioni va bene solo in C, non in C ++.
Lundin

1
tuttavia, non sarei sorpreso se funzionasse sul 100% di tutti i compilatori c ++. mai.
Sven Nilsson

1
Puoi provarlo in gcc con le impostazioni di ottimizzazione più aggressive attive.
Lundin

1
@Lundin: il punning di tipo union è legale in GNU C ++, come estensione su ISO C ++. Non sembra essere affermato molto chiaramente nel manuale , ma ne sono abbastanza sicuro. Tuttavia, questa risposta deve spiegare dove è valida e dove non lo è.
Peter Cordes
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.