Come evitare la duplicazione del codice che implementa gli iteratori const e non-const?

Sto implementando un contenitore personalizzato con un’interfaccia simile a STL. Devo fornire un iteratore normale e un iteratore const. La maggior parte del codice per le due versioni degli iteratori è identica. Come posso evitare questa duplicazione?

Ad esempio, la mia class contenitore è Foo e sto implementando FooIterator e FooConstIterator . Entrambi gli iteratori devono fornire metodi come operator++() identici.

La mia domanda è simile a Come faccio a rimuovere la duplicazione del codice tra funzioni costanti simili e non membro const? , ma la risposta a questo è specifica per i metodi const e non-const, in particolare per gli accessor. Non vedo come ciò potrebbe generalizzare al problema degli iteratori.

Dovrei ottenere FooIterator da FooConstIterator ed estenderlo con metodi aggiuntivi non const? Ciò porta a metodi virtuali o metodi di occultamento, che sembrano inappropriati qui.

Forse FooIterator dovrebbe contenere un FooConstIterator . Sebbene questo approccio riduca la duplicazione dell’implementazione, sembra reintrodurre un sacco di definizioni dei metodi standard.

C’è una tecnica di template intelligente per generare i due iteratori da una singola definizione? O forse c’è un modo per – rabbrividire – usare il preprocessore per eliminare queste classi quasi identiche.

Ho provato a guardare la mia implementazione STL locale per vedere come gestirlo. Ci sono così tante classi di supporto che ho difficoltà a groove il design, ma sembra che la funzionalità sia semplicemente duplicata.

Nei progetti precedenti, il mio contenitore personalizzato è stato costruito sopra un contenitore STL standard, quindi non ho dovuto fornire i miei iteratori. Non è un’opzione in questo caso.

[La risposta migliore è stata, purtroppo, cancellata da un moderatore perché si trattava di una risposta solo per collegamento. Capisco perché le risposte di solo collegamento sono scoraggiate; eliminarlo, tuttavia, ha privato i futuri ricercatori di informazioni molto utili. Il collegamento è rimasto stabile per oltre sette anni e continua a funzionare al momento della stesura di questo documento.]

Consiglio vivamente l’articolo originale del Dr. Dobb’s Journal di Matt Austern dal titolo “The Standard Librarian: Defining Iterators and Const Iterators” , gennaio 2001. Se quel link andasse a male, ora che il Dr. Dobb’s ha cessato di funzionare, è disponibile anche qui .

Per evitare che questa risposta di sostituzione venga cancellata, riassumerò la soluzione.

L’idea è di implementare l’iteratore una volta come modello che prende un parametro di modello extra, un booleano che dice se questa è la versione const. Ovunque nell’implementazione in cui le versioni const e non-const sono diverse, si utilizza un meccanismo di modello per selezionare il codice corretto. Il meccanismo di Matt Austern è stato chiamato choose . Sembrava questo:

 template  struct choose; template  struct choose { typedef IsTrue type; }; template  struct choose { typedef IsFalse type; }; 

Se aveste implementazioni separate per gli iteratori const e non-const, l’implementazione const includerebbe typedefs come questo:

 typedef const T &reference; typedef const T *pointer; 

e l’implementazione non const avrebbe:

 typedef T &reference; typedef T *pointer; 

Ma con la choose , puoi avere una singola implementazione che seleziona in base al parametro extra del template:

 typedef typename choose::type reference; typedef typename choose::type pointer; 

Usando typedef per i tipi sottostanti, tutti i metodi iteratori possono avere un’implementazione identica. Vedi l’ esempio completo di Matt Austern.

Dal momento che C ++ 11/14 è ansible evitare tali piccoli aiuti, dedurre la costanza direttamente da un modello booleano.

constness.h:

 #ifndef ITERATOR_H #define ITERATOR_H #include  #include  #include  #include  struct dummy_struct { int hello = 1; int world = 2; dummy_struct() : hello{ 0 }, world{ 1 }{ } }; template< class T > class iterable { public: template< bool Const = false > class my_iterator { public: using iterator_category = std::forward_iterator_tag; using value_type = T; using difference_type = std::ptrdiff_t; /* deduce const qualifier from bool Const parameter */ using reference = typename std::conditional_t< Const, T const &, T & >; using pointer = typename std::conditional_t< Const, T const *, T * >; protected: pointer i; public: my_iterator( T* _i ) : i{ reinterpret_cast< pointer >( _i ) } { } /* SFINAE enables the const dereference operator or the non const variant depending on bool Const parameter */ template< bool _Const = Const > std::enable_if_t< _Const, reference > operator*() const { std::cout << "Const operator*: "; return *i; } template< bool _Const = Const > std::enable_if_t< !_Const, reference > operator*() { std::cout << "Non-Const operator*: "; return *i; } my_iterator & operator++() { ++i; return *this; } bool operator!=( my_iterator const & _other ) const { return i != _other.i; } bool operator==( my_iterator const & _other ) const { return !( *this != _other ); } }; private: T* __begin; T* __end; public: explicit iterable( T* _begin, std::size_t _count ): __begin{ _begin }, __end{ _begin + _count } { std::cout << "End: " << __end << "\n"; } auto begin() const { return my_iterator< false >{ __begin }; } auto end() const { return my_iterator< false >{ __end }; } auto cbegin() const { return my_iterator< true >{ __begin }; } auto cend() const { return my_iterator< true >{ __end }; } }; #endif 

Questo può essere usato con qualcosa del genere:

 #include  #include  #include "constness.h" int main() { dummy_struct * data = new dummy_struct[ 5 ]; for( int i = 0; i < 5; ++i ) { data[i].hello = i; data[i].world = i+1; } iterable< dummy_struct > i( data, 5 ); using iter = typename iterable< dummy_struct >::my_iterator< false >; using citer = typename iterable< dummy_struct >::my_iterator< true >; for( iter it = i.begin(); it != i.end(); ++it ) { std::cout << "Hello: " << (*it).hello << "\n" << "World: " << (*it).world << "\n"; } for( citer it = i.cbegin(); it != i.cend(); ++it ) { std::cout << "Hello: " << (*it).hello << "\n" << "World: " << (*it).world << "\n"; } delete[] data; } 

STL utilizza l’ereditarietà

 template class _Vector_iterator : public _Vector_const_iterator<_myvec> 

Oltre al suggerimento di templatizzare la costanza e la non-costanza, potresti anche ridurre la quantità di lavoro dando un’occhiata al tutorial di Boost.Iterator , che menziona anche la stessa soluzione.

Puoi usare CRTP e una base comune per “iniettare” i metodi (ma devi ancora duplicare i vettori nel C ++ corrente), o semplicemente usare il preprocessore (non è necessario rabbrividire, maneggia facilmente i vettori):

 struct Container { #define G(This) \ This operator++(int) { This copy (*this); ++*this; return copy; } // example of postfix++ delegating to ++prefix struct iterator : std::iterator<...> { iterator& operator++(); G(iterator) }; struct const_iterator : std::iterator<...> { const_iterator& operator++(); G(const_iterator) }; #undef G // G is "nicely" scoped and treated as an implementation detail }; 

Usa std :: iterator, i typedefs che ti dà e ogni altro typedef che potresti fornire per rendere la macro straight-forward.