Ecco una soluzione "completa" che utilizza uno iterator
incorporato. Alcuni tratti privati vengono utilizzati per contribuire a ridurre il disordine nella classe:
template <typename T> class List;
template <typename T>
class ListTraits {
protected:
typedef std::list<std::shared_ptr<T>> Impl;
typedef typename Impl::iterator Iterator;
typedef typename Impl::const_iterator ConstIterator;
typedef typename Impl::reverse_iterator Rotareti;
typedef typename Impl::const_reverse_iterator ConstRotareti;
typedef std::map<const List<T> *, typename Impl::iterator> Ref;
};
Come mostrato, l'attuazione lista sarà utilizzando std::list
, ma il tipo di valore sottostante sarà un std::shared_ptr
. Quello che sto cercando è che un'istanza di T
ricava in modo efficiente il proprio iterator
, per ottenere la cancellazione di O (1). Questo viene fatto usando un Ref
per memoizzare l'iteratore dell'elemento dopo che è stato inserito nell'elenco.
template <typename T>
class List : public ListTraits<T> {
template <typename ITER> class IteratorT;
typedef ListTraits<T> Traits;
typename Traits::Impl impl_;
public:
typedef typename Traits::Impl::size_type size_type;
typedef typename Traits::Impl::value_type pointer;
typedef pointer value_type;
typedef IteratorT<typename Traits::Iterator> iterator;
typedef IteratorT<typename Traits::ConstIterator> const_iterator;
typedef IteratorT<typename Traits::Rotareti> reverse_iterator;
typedef IteratorT<typename Traits::ConstRotareti> const_reverse_iterator;
class Item;
~List() { while (!empty()) pop_front(); }
size_type size() const { return impl_.size(); }
bool empty() const { return impl_.empty(); }
iterator begin() { return impl_.begin(); }
iterator end() { return impl_.end(); }
const_iterator begin() const { return impl_.begin(); }
const_iterator end() const { return impl_.end(); }
reverse_iterator rbegin() { return impl_.rbegin(); }
reverse_iterator rend() { return impl_.rend(); }
const_reverse_iterator rbegin() const { return impl_.rbegin(); }
const_reverse_iterator rend() const { return impl_.rend(); }
pointer front() const { return !empty() ? impl_.front() : pointer(); }
pointer back() const { return !empty() ? impl_.back() : pointer(); }
void push_front (const pointer &e);
void pop_front();
void push_back (const pointer &e);
void pop_back();
void erase (const pointer &e);
bool contains (const pointer &e) const;
};
Questo List
segue per lo più una coda come l'interfaccia. Ma un oggetto può essere rimosso da qualsiasi posizione nell'elenco. Le funzioni semplici per lo più solo delegano al sottostante std::list
. Ma i metodi push_*()
e pop_*()
memoize anche il iterator
.
template <typename T>
template <typename ITER>
class List<T>::IteratorT : public ITER {
friend class List<T>;
ITER iter_;
IteratorT (ITER i) : iter_(i) {}
public:
IteratorT() : iter_() {}
IteratorT & operator ++() { ++iter_; return *this; }
IteratorT & operator --() { --iter_; return *this; }
IteratorT operator ++ (int) { return iter_++; }
IteratorT operator -- (int) { return iter_--; }
bool operator == (const IteratorT &x) const { return iter_ == x.iter_; }
bool operator != (const IteratorT &x) const { return iter_ != x.iter_; }
T & operator *() const { return **iter_; }
pointer operator ->() const { return *iter_; }
};
Questa è l'implementazione del modello di supporto utilizzato per definire i tipi iteratori per List
. Quello che fa in modo diverso è che gli operatori *
e ->
sono definiti in modo tale che l'iteratore si comporti come se fosse un T *
piuttosto che uno std::shared_ptr<T> *
(che è ciò che l'iteratore sottostante farebbe normalmente).
template <typename T>
class List<T>::Item {
friend class List<T>;
mutable typename Traits::Ref ref_;
};
Un tipo T
derivato da List<T>::Item
possibile aggiungere a un List<T>
. Questa classe base contiene l'istanza Ref
utilizzata per memoizzare l'iteratore quando l'elemento viene aggiunto a un elenco.
template <typename T>
inline void List<T>::push_front (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i == item.ref_.end()) {
item.ref_[this] = impl_.insert(impl_.begin(), e);
} else if (front() != e) {
impl_.erase(i->second);
i->second = impl_.insert(impl_.begin(), e);
}
}
template <typename T>
inline void List<T>::pop_front() {
if (!empty()) {
const Item &item = *front();
item.ref_.erase(this);
impl_.pop_front();
}
}
Questo codice illustra come viene eseguita la memoizzazione. Quando si esegue push_front()
, l'elemento viene prima controllato per vedere se è già contenuto.In caso contrario, viene inserito e l'iteratore risultante viene aggiunto all'oggetto ref_
. Altrimenti, se non è già anteriore, l'elemento viene rimosso e reinserito nella parte anteriore e l'iteratore memoizzato viene aggiornato. pop_front()
rimuove l'iteratore memoized e quindi chiama pop_front()
sullo std::list
.
template <typename T>
inline void List<T>::push_back (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i == item.ref_.end()) {
item.ref_[this] = impl_.insert(impl_.end(), e);
} else if (back() != e) {
impl_.erase(i->second);
i->second = impl_.insert(impl_.end(), e);
}
}
template <typename T>
inline void List<T>::pop_back() {
if (!empty()) {
const Item &item = *back();
item.ref_.erase(this);
impl_.pop_back();
}
}
push_back()
e pop_back()
sono simili a push_front()
e pop_front()
.
template <typename T>
inline void List<T>::erase (const pointer &e) {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
if (i != item.ref_.end()) {
item.ref_.erase(i);
impl_.erase(i->second);
}
}
Il erase()
di routine recupera l'iteratore memoized, e lo utilizza per eseguire la cancellazione.
template <typename T>
inline bool List<T>::contains (const pointer &e) const {
const Item &item = *e;
typename Traits::Ref::iterator i = item.ref_.find(this);
return i != item.ref_.end();
}
Dal momento che un oggetto è il suo iteratore per molti aspetti, un metodo find()
non dovrebbe essere necessario in questa versione di List
. Ma, al posto di questo è questo metodo che viene utilizzato per vedere se l'elemento è un membro della lista.
Ora, la soluzione presentata utilizza un std::map
per associare le istanze dell'elenco agli iteratori. Ciò mantiene lo spirito di O (1) se il numero di elenchi di un elemento è un membro di simultaneamente è relativamente piccolo.
Proverò la mia versione successiva alla versione boost::intrusive
.
Basti pensare a questo proposito. Non puoi mai accedere all'elemento casuale di implementazione dell'elenco collegato in O (1) volta, sarà sempre O (n). È possibile accedere solo a testa e coda entro un tempo costante. Il fatto che sia necessario essere in grado di accedere a determinati elementi (accesso casuale) suggerisce semplicemente da solo che si sta usando il contenitore sbagliato e si dovrebbe optare per 'vector' o qualcosa di simile. Creare involucri di questo tipo "overkill" è inefficace (in termini di memoria e prestazioni) e poco pratico dal mio punto di vista. –
Quindi il nodo nell'elenco vuole rimuoversi dall'elenco? – cppguy
@ cppguy: in poche parole, sì. – jxh