Come fare in modo che il mio tipo personalizzato funzioni con "range-based per loop"?


252

Come molte persone in questi giorni ho provato le diverse funzionalità offerte da C ++ 11. Uno dei miei preferiti è il "range-based per loop".

Lo capisco:

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

È equivalente a:

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

E questo begin()ritorna semplicemente a.begin()per contenitori standard.

E se volessi rendere il mio tipo personalizzato "range-based for loop" -aware ?

Dovrei solo specializzarmi begin()e end()?

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

In breve, quali sono le linee guida per farlo?


È possibile definendo un membro begin/endo un amico, statico o gratuito begin/end. Basta essere attenti in cui spazio dei nomi si inserisce la funzione libera: stackoverflow.com/questions/28242073/...
ALFC

Potrebbe per favore qualcuno inviare una risposta con l'esempio di un campo di valori float che non è un contenitore: for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. Sono curioso di sapere come aggirare il fatto che `` operatore! = () `` È difficile da definire. E la dereferenziazione ( *__begin) in questo caso? Penso che sarebbe un grande contributo, se qualcuno ci ha mostrato come che è fatto!
BitTickler,

Risposte:


183

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

Il modo per far funzionare un for(:)ciclo sul tuo tipo Xè ora in due modi:

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

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

E simile per le constvariazioni. Questo funzionerà sia sui compilatori che implementano le modifiche al rapporto sui difetti, sia sui compilatori che non lo fanno.

Gli oggetti restituiti non devono essere effettivamente iteratori. Il for(:)ciclo, a differenza della maggior parte delle parti dello standard C ++, è specificato per espandersi in 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, begin_expred 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, binarie !=che possano essere utilizzate in un contesto booleano, unarie *che restituiscano qualcosa che è possibile assegnare-inizializzare range_declarationed esporre un pubblico distruttore.

Farlo in un modo che non è compatibile con un iteratore è probabilmente una cattiva idea, poiché le future iterazioni di C ++ potrebbero essere relativamente sprezzanti sulla violazione del codice, se lo fai.

A parte questo, è ragionevolmente probabile che una futura revisione dello standard consentirà end_exprdi restituire un tipo diverso da begin_expr. Ciò è utile in quanto consente una valutazione "lazy-end" (come il rilevamento della terminazione nulla) che è facile da ottimizzare per essere efficiente come un ciclo C scritto a mano e altri vantaggi simili.


¹ Nota che i for(:)loop memorizzano qualsiasi temporaneo in una auto&&variabile e te lo passano come un valore. Non è possibile rilevare se si sta ripetendo un temporaneo (o un altro valore); un tale sovraccarico non verrà chiamato da un for(:)loop. Vedi [stmt.ranged] 1.2-1.3 da n4527.

² O chiamare il begin/ endmetodo o ADL di sola ricerca del libero funzione di begin/ end, o la magia per il supporto serie in stile C. Si noti che std::beginnon viene chiamato a meno che non range_expressionrestituisca un oggetto di tipo namespace stdo dipendente dallo stesso.


Nel l'intervallo per l'espressione è stato aggiornato

{
  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 __begine __endsono stati disaccoppiati.

Ciò consente all'iteratore finale di non essere dello stesso tipo di inizio. Il tipo di iteratore finale può essere una "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 a char*. Ciò consente a un intervallo C ++ per l'espressione di generare codice ottimale durante l'iterazione su un char*buffer con terminazione null .

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},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 pieno supporto C ++ 17; forloop espanso manualmente.


Se basato su intervallo per utilizza un diverso meccanismo di ricerca, forse è possibile organizzare tale intervallo per ottenere una coppia begine endfunzioni diverse da quelle disponibili nel codice normale. Forse potrebbero quindi essere molto specializzati per comportarsi diversamente (cioè più velocemente ignorando l'argomento finale per ottenere le ottimizzazioni possibili al massimo). Ma non sono abbastanza bravo con gli spazi dei nomi per essere sicuro di come farlo.
Aaron McDaid,

@AaronMcDaid non molto pratico. Si finirebbe facilmente con risultati sorprendenti, perché alcuni mezzi per chiamare inizio / fine finirebbero con l'intervallo basato su inizio / fine, e altri no. Cambiamenti innocui (dal lato client) otterrebbero cambiamenti comportamentali.
Yakk - Adam Nevraumont,

1
Non hai bisogno begin(X&&). Il temporaneo è sospeso a mezz'aria auto&&in un intervallo basato su per ed beginè sempre chiamato con un lvalue ( __range).
TC,

2
Questa risposta trarrebbe davvero beneficio da un esempio di modello che è possibile copiare e implementare.
Tomáš Zato - Ripristina Monica il

Preferirei mettere in rilievo le proprietà del tipo di iteratore (*, ++,! =). Dovrei chiederti di riformulare questa risposta per rendere più audaci le specifiche del tipo di iteratore.
Red.Wave
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.