Come rendere il mio tipo personalizzato per lavorare con “range-based for loops”?

Come molte persone in questi giorni ho provato le diverse funzionalità che porta C + 11. Uno dei miei preferiti è il “range-based for loops”.

Lo capisco:

for(Type& v : a) { ... } 

È equivalente a:

 for(auto iv = begin(a); iv != end(a); ++iv) { Type& v = *iv; ... } 

E che begin() restituisce semplicemente a.begin() per i contenitori standard.

Ma cosa succede se voglio rendere il mio tipo personalizzato “range-based for loop” -aware ?

Devo specializzarmi su begin() e end() ?

Se il mio tipo personalizzato appartiene allo spazio dei nomi xml , dovrei definire xml::begin() o std::begin() ?

In breve, quali sono le linee guida per farlo?

Lo standard è stato modificato poiché la domanda (e la maggior parte delle risposte) sono state pubblicate nella risoluzione di questo rapporto sui difetti .

Il modo in cui creare un ciclo for(:) sul tuo tipo X è ora in due modi:

  • Crea membri X::begin() e X::end() che restituiscono qualcosa che si comporta come un iteratore

  • Crea una funzione gratuita begin(X&) e end(X&) che restituiscono qualcosa che si comporta come un iteratore, nello stesso spazio dei nomi del tuo tipo X

E simile per le variazioni const . Ciò funzionerà sia sui compilatori che implementano le modifiche ai report dei difetti, sia sui compilatori che non lo fanno.

Gli oggetti restituiti non devono essere effettivamente iteratori. Il ciclo for(:) , a differenza della maggior parte delle parti dello standard C ++, viene specificato per espandersi a qualcosa di equivalente a :

 for( range_declaration : range_expression ) 

diventa:

 { auto && __range = range_expression ; for (auto __begin = begin_expr, __end = end_expr; __begin != __end; ++__begin) { range_declaration = *__begin; loop_statement } } 

dove le variabili che iniziano con __ sono solo per esposizione, e begin_expr e end_expr è la magia che chiama begin / end

I requisiti sul valore di ritorno inizio / fine sono semplici: è necessario sovraccaricare pre- ++ , assicurarsi che le espressioni di inizializzazione siano valide, binario != Che può essere utilizzato in un contesto booleano, unario * che restituisce qualcosa che è ansible assegnare-inizializza range_declaration con, ed esporre un distruttore pubblico.

Farlo in un modo che non è compatibile con un iteratore è probabilmente una ctriggers idea, poiché le future iterazioni di C ++ potrebbero essere relativamente spregiudicate circa la rottura del codice, se lo si fa.

Per inciso, è ragionevolmente probabile che una revisione futura dello standard consentirà a end_expr di restituire un tipo diverso da begin_expr . Ciò è utile in quanto consente una valutazione “lazy-end” (come la rilevazione della terminazione nulla) che è facile da ottimizzare per essere efficiente come un ciclo C scritto a mano e altri vantaggi simili.


¹ Si noti che for(:) cicli for(:) memorizza qualsiasi elemento temporaneo in una variabile auto&& e lo passa a lvalue. Non è ansible rilevare se si sta iterando su un valore temporaneo (o altro valore); tale sovraccarico non verrà chiamato da un ciclo for(:) . Vedi [stmt.ranged] 1.2-1.3 da n4527.

² Chiamate il metodo begin / end , o solo la ricerca ADL di begin / end funzione gratuita o magia per il supporto dell’array in stile C. Si noti che std::begin non viene chiamato a meno che range_expression restituisca un object di tipo nello namespace std o dipendente dallo stesso.


In c ++ 17 l’espressione range-for è stata aggiornata

 { auto && __range = range_expression ; auto __begin = begin_expr; auto __end = end_expr for (;__begin != __end; ++__begin) { range_declaration = *__begin; loop_statement } } 

con i tipi di __begin e __end sono stati disaccoppiati.

Ciò consente che l’iteratore finale non sia dello stesso tipo di quello iniziale. Il tipo di iteratore finale può essere un “sentinella” che supporta solo != Con il tipo di iteratore iniziale.

Un esempio pratico del perché questo è utile è che il tuo iteratore finale può leggere “controlla il tuo char* per vedere se punta a '0' ” quando == con un char* . Ciò consente a un intervallo C ++, che un’espressione generi codice ottimale quando si esegue l’iterazione su un buffer char* con terminazione null.

 struct null_sentinal_t { template{},int> =0 > friend bool operator==(Rhs const& ptr, null_sentinal_t) { return !*ptr; } template{},int> =0 > friend bool operator!=(Rhs const& ptr, null_sentinal_t) { return !(ptr==null_sentinal_t{}); } template{},int> =0 > friend bool operator==(null_sentinal_t, Lhs const& ptr) { return !*ptr; } template{},int> =0 > friend bool operator!=(null_sentinal_t, Lhs const& ptr) { return !(null_sentinal_t{}==ptr); } friend bool operator==(null_sentinal_t, null_sentinal_t) { return true; } friend bool operator!=(null_sentinal_t, null_sentinal_t) { return false; } }; 

esempio live in un compilatore senza supporto completo per C ++ 17; for loop espanso manualmente.

La parte rilevante della norma è 6.5.4 / 1:

se _RangeT è un tipo di class, gli ID univimi fi niti iniziano e finiscono vengono cercati nell’ambito della class _RangeT come se la ricerca di accesso dei membri della class (3.4.5), e se uno (o entrambi) trova almeno una dichiarazione, iniziare – expr e end-expr sono __range.begin() e __range.end() , rispettivamente;

– altrimenti, begin-expr e end-expr sono begin(__range) e end(__range) , rispettivamente, dove start e end sono ricercati con la ricerca dipendente dall’argomento (3.4.2). Ai fini della ricerca di questo nome, lo spazio dei nomi std è uno spazio dei nomi associato.

Pertanto, puoi eseguire una delle seguenti azioni:

  • definire le funzioni membro iniziale e end
  • definire le funzioni gratuite di begin e end che verranno trovate da ADL (versione semplificata: inserirle nello stesso spazio dei nomi della class)
  • specializza std::begin e std::end

std::begin chiama comunque la funzione membro begin() , quindi se si implementa solo uno dei precedenti, i risultati dovrebbero essere gli stessi indipendentemente da quale si sceglie. Questo è lo stesso risultato per i loop basati su ranghi, e anche lo stesso risultato per il semplice codice mortale che non ha le sue regole di risoluzione dei nomi magiche, quindi using std::begin; seguito da una chiamata non qualificata per begin(a) .

Se si implementano le funzioni membro e le funzioni ADL, tuttavia, i loop basati su intervalli dovrebbero chiamare le funzioni membro, mentre i comuni mortali chiameranno le funzioni ADL. Meglio assicurati che facciano la stessa cosa in quel caso!

Se la cosa che stai scrivendo implementa l’interfaccia del contenitore, allora avrà già le funzioni membro begin() e end() , che dovrebbero essere sufficienti. Se è un intervallo che non è un contenitore (che sarebbe una buona idea se è immutabile o se non si conosce la dimensione in primo piano), sei libero di scegliere.

Tra le opzioni che disponi, tieni presente che non devi sovraccaricare std::begin() . È consentito specializzare modelli standard per un tipo definito dall’utente, ma a parte questo, l’aggiunta di definizioni allo spazio dei nomi std è un comportamento non definito. Ma in ogni caso, specializzare le funzioni standard è una scelta sbagliata, se non altro perché la mancanza di specializzazione delle funzioni parziali significa che puoi farlo solo per una singola class, non per un modello di class.

Devo specializzarmi su begin () e end ()?

Per quanto ne so, è abbastanza. Devi anche assicurarti che l’incremento del puntatore si avvii dall’inizio alla fine.

Il prossimo esempio (manca la versione const di inizio e fine) compila e funziona bene.

 #include  #include  int i=0; struct A { A() { std::generate(&v[0], &v[10], [&i](){ return ++i;} ); } int * begin() { return &v[0]; } int * end() { return &v[10]; } int v[10]; }; int main() { A a; for( auto it : a ) { std::cout << it << std::endl; } } 

Ecco un altro esempio con start / end come funzioni. Devono essere nello stesso spazio dei nomi della class, a causa di ADL:

 #include  #include  namespace foo{ int i=0; struct A { A() { std::generate(&v[0], &v[10], [&i](){ return ++i;} ); } int v[10]; }; int *begin( A &v ) { return &v.v[0]; } int *end( A &v ) { return &v.v[10]; } } // namespace foo int main() { foo::A a; for( auto it : a ) { std::cout << it << std::endl; } } 

Scrivo la mia risposta perché alcune persone potrebbero essere più felici con un semplice esempio di vita reale senza includere STL.

Ho la mia semplice implementazione di array di dati solo per qualche motivo, e volevo usare il range based for loop. Ecco la mia soluzione:

  template  class PodArray { public: class iterator { public: iterator(DataType * ptr): ptr(ptr){} iterator operator++() { ++ptr; return *this; } bool operator!=(const iterator & other) const { return ptr != other.ptr; } const DataType& operator*() const { return *ptr; } private: DataType* ptr; }; private: unsigned len; DataType *val; public: iterator begin() const { return iterator(val); } iterator end() const { return iterator(val + len); } // rest of the container definition not related to the question ... }; 

Quindi l’esempio di utilizzo:

 PodArray array; // fill up array in some way for(auto& c : array) printf("char: %c\n", c); 

Nel caso in cui si desideri eseguire il backing di un’iterazione della class direttamente con il suo membro std::vector o std::map , ecco il codice per questo:

 #include  using std::cout; using std::endl; #include  using std::string; #include  using std::vector; #include  using std::map; ///////////////////////////////////////////////////// /// classs ///////////////////////////////////////////////////// class VectorValues { private: vector v = vector(10); public: vector::iterator begin(){ return v.begin(); } vector::iterator end(){ return v.end(); } vector::const_iterator begin() const { return v.begin(); } vector::const_iterator end() const { return v.end(); } }; class MapValues { private: map v; public: map::iterator begin(){ return v.begin(); } map::iterator end(){ return v.end(); } map::const_iterator begin() const { return v.begin(); } map::const_iterator end() const { return v.end(); } const int& operator[](string key) const { return v.at(key); } int& operator[](string key) { return v[key]; } }; ///////////////////////////////////////////////////// /// main ///////////////////////////////////////////////////// int main() { // VectorValues VectorValues items; int i = 0; for(int& item : items) { item = i; i++; } for(int& item : items) cout << item << " "; cout << endl << endl; // MapValues MapValues m; m["a"] = 1; m["b"] = 2; m["c"] = 3; for(auto pair: m) cout << pair.first << " " << pair.second << endl; } 

Qui, sto condividendo l’esempio più semplice di creazione di un tipo personalizzato, che funzionerà con ” range-based for loop “:

 #include using namespace std; template class MyCustomType { private: T *data; int indx; public: MyCustomType(){ data = new T[sizeOfArray]; indx = -1; } ~MyCustomType(){ delete []data; } void addData(T newVal){ data[++indx] = newVal; } //write definition for begin() and end() //these two method will be used for "ranged based loop idiom" T* begin(){ return &data[0]; } T* end(){ return &data[sizeOfArray]; } }; int main() { MyCustomType numberList; numberList.addData(20.25); numberList.addData(50.12); for(auto val: numberList){ cout< 

Spero, sarà utile per alcuni sviluppatori alle prime armi come me: p 🙂
Grazie.

La risposta di Chris Redford funziona anche con contenitori Qt (ovviamente). Ecco un adattamento (avviso restituisco un constBegin() , rispettivamente constEnd() dai metodi const_iterator):

 class MyCustomClass{ QList data_; public: // ctors,dtor, methods here... QList::iterator begin() { return data_.begin(); } QList::iterator end() { return data_.end(); } QList::const_iterator begin() const{ return data_.constBegin(); } QList::const_iterator end() const{ return data_.constEnd(); } };