2011-02-08 26 views
10

Mi chiedo se sia una buona idea racchiudere i contenitori C++ STL per mantenere la coerenza e poter scambiare l'implementazione senza modificare il codice client.Contenimento dei contenitori per mantenere la coerenza

Per esempio, in un progetto che usiamo CamelCase per la denominazione classi e funzioni membro (Foo :: DoSomething()), mi sarebbe avvolgere std :: elenco in una classe come questa:

template<typename T> 
class List 
{ 
    public: 
     typedef std::list<T>::iterator Iterator; 
     typedef std::list<T>::const_iterator ConstIterator; 
     // more typedefs for other types. 

     List() {} 
     List(const List& rhs) : _list(rhs._list) {} 
     List& operator=(const List& rhs) 
     { 
      _list = rhs._list; 
     } 

     T& Front() 
     { 
      return _list.front(); 
     } 

     const T& Front() const 
     { 
      return _list.front(); 
     } 

     void PushFront(const T& x) 
     { 
      _list.push_front(x); 
     } 

     void PopFront() 
     { 
      _list.pop_front(); 
     } 

     // replace all other member function of std::list. 

    private: 
     std::list<T> _list; 
}; 

Poi ho sarebbe in grado di scrivere qualcosa del genere:

typedef uint32_t U32; 
List<U32> l; 
l.PushBack(5); 
l.PushBack(4); 
l.PopBack(); 
l.PushBack(7); 

for (List<U32>::Iterator it = l.Begin(); it != l.End(); ++it) { 
    std::cout << *it << std::endl; 
} 
// ... 

credo che la maggior parte dei moderni compliers C++ possono ottimizzare via l'indirezione più facilmente, e credo che questo metodo ha alcuni vantaggi come:

Posso estendere facilmente la funzionalità della classe List. Per esempio, io voglio una funzione di scorciatoia che ordina l'elenco e quindi chiamare unico(), posso estenderlo con l'aggiunta di una funzione membro:

template<typename T> 
void List<T>::SortUnique() 
{ 
    _list.sort(); 
    _list.unique(); 
} 

Inoltre, posso scambiare l'implementazione sottostante (se necessario), senza alcuna cambia il codice che usano List fintanto che il comportamento è lo stesso. Ci sono anche altri vantaggi, perché mantiene la coerenza delle convenzioni di denominazione in un progetto, in modo che non hanno push_back() per STL e pushback() per le altre classi di tutto il progetto come:

std::list<MyObject> objects; 
// insert some MyObject's. 
while (!objects.empty()) { 
    objects.front().DoSomething(); 
    objects.pop_front(); 
    // Notice the inconsistency of naming conventions above. 
} 
// ... 

Sono chiedendosi se questo approccio abbia qualche svantaggio maggiore (o minore) o se questo sia in realtà un metodo pratico.

Grazie.

MODIFICATO: Ok, grazie per le risposte finora. Penso di aver messo troppo a dire la coerenza nella domanda. In realtà le convenzioni di denominazione non sono la mia preoccupazione qui, dal momento che si può fornire una esattamente stessa interfaccia così:

template<typename T> 
void List<T>::pop_back() 
{ 
    _list.pop_back(); 
} 

Oppure si può anche rendere l'interfaccia di un'altra implementazione sembrano più STL quella che la maggior parte dei programmatori C++ sono già familiari con. Ma comunque, secondo me è più una cosa di stile e non così importante.

Quello che mi preoccupava è la coerenza per poter modificare facilmente i dettagli di implementazione. Uno stack può essere implementato in vari modi: un array e un indice superiore, un elenco collegato o anche un ibrido di entrambi, e tutti hanno la caratteristica LIFO di una struttura dati. Un albero binario di ricerca autoequilibrante può essere implementato anche con un albero AVL o un albero rosso-nero, ed entrambi hanno una complessità temporale media O (logn) per la ricerca, l'inserimento e l'eliminazione.

Quindi, se ho una libreria ad albero AVL e un'altra libreria ad albero rosso-nero con interfacce diverse, e io uso un albero AVL per memorizzare alcuni oggetti. Più tardi, ho capito (usando i profiler o qualsiasi altra cosa) che usare un albero rosso-nero avrebbe dato una spinta in termini di prestazioni, avrei dovuto andare in ogni parte dei file che usano gli alberi AVL e cambiare la classe, i nomi dei metodi e probabilmente l'argomento ordini alle sue controparti albero rosso-nero. Ci sono probabilmente anche alcuni scenari in cui la nuova classe non ha ancora una funzionalità equivalente scritta. Penso che possa anche introdurre dei bug sottili anche a causa delle differenze nell'implementazione, o che io commetta un errore.

Così che cosa ho cominciato a chiedermi che se vale la pena l'overhead a mantenere tale classe wrapper per nascondere i dettagli di implementazione e fornire un'interfaccia uniforme per diverse implementazioni:

template<typename T> 
class AVLTree 
{ 
    // ... 
    Iterator Find(const T& val) 
    { 
     // Suppose the find function takes the value to be searched and an iterator 
     // where the search begins. It returns end() if val cannot be found. 
     return _avltree.find(val, _avltree.begin()); 
    } 
}; 

template<typename T> 
class RBTree 
{ 
    // ... 
    Iterator Find(const T& val) 
    { 
     // Suppose the red-black tree implementation does not support a find function, 
     // so you have to iterate through all elements. 
     // It would be a poor tree anyway in my opinion, it's just an example. 
     auto it = _rbtree.begin(); // The iterator will iterate over the tree 
            // in an ordered manner. 
     while (it != _rbtree.end() && *it < val) { 
      ++it; 
     } 
     if (*++it == val) { 
      return it; 
     } else { 
      return _rbtree.end(); 
     } 
    } 
}; 

Ora, devo solo assicurati che AVLTree :: Find() e RBTree :: Find() facciano esattamente la stessa cosa (es. prendi il valore da cercare, restituisci un iteratore all'elemento o End(), altrimenti). E poi, se voglio cambiare da un albero AVL ad un albero rosso-nero, tutto quello che devo fare è modificare la dichiarazione:

AVLTree<MyObject> objectTree; 
AVLTree<MyObject>::Iterator it; 

a:

RBTree<MyObject> objectTree; 
RBTree<MyObject>::Iterator it; 

e tutto il resto sarà lo stesso, mantenendo due classi.

Quanto sopra è solo un esempio di ciò che volevo archiviare con la classe wrapper. Forse questo tipo di scenario è troppo raro nel vero sviluppo e che mi preoccupo troppo, non ne ho idea. Ecco perché lo sto chiedendo.

Ho cambiato il titolo da "Wrapping STL Container" a "Wrapping Containers per mantenere la coerenza" per riflettere il problema in modo più accurato.

Grazie per il vostro tempo.

+2

Pensare a questa idea mi sta facendo fisicamente male. –

+0

Non vedo nulla di sbagliato nel farlo. ma vuoi davvero ??? – Arunmu

+0

Quindi, per ottenere tutto questo, vuoi fare tutto questo solo a causa dei nomi dei nomi e scambiando l'implementazione? Non è possibile nominare il nome, perché in questo caso si potrebbero includere molte altre cose STL. – RvdK

risposta

6

Mi chiedo se questo approccio ha grossi (o minori) svantaggi,

due parole: Manutenzione incubo.

E quindi, quando si ottiene un nuovo compilatore C++ 0x abilitato per lo spostamento, sarà necessario estendere tutte le classi wrapper.

Non fraintendetemi: non c'è niente di sbagliato nel confezionamento di un contenitore STL se sono necessarie funzionalità aggiuntive, ma solo per "nomi di funzioni membro coerenti"? Troppo sovraccarico. Troppo tempo investito per nessun ROI.

Aggiungo: Convenzioni di denominazione incoerenti sono solo le cose con cui vivi quando lavori con C++. Esistono troppi stili diversi in troppe librerie disponibili (e utili).

0

No, non avvolgerli così.
Si avvolgono i contenitori se si desidera modificare i contenitori.
Ad esempio, se si dispone di una mappa non ordinata che deve solo aggiungere/rimuovere elementi internamente ma deve esporre l'operatore [] che si avvolge e creare il proprio [] che espone il contenitore interno [] e si espone anche un const_iterator che è const_iterator di unordered_map.

2

... essere in grado di scambiare l'attuazione senza modificare il codice client

A livello di problem-solving di codice, si consiglia di selezionare un contenitore interno diverso pur presentando la stessa API. Tuttavia, non si può ragionevolmente disporre del codice che sceglie specificamente di utilizzare un elenco, quindi sostituire l'implementazione con qualsiasi altra cosa senza compromettere le prestazioni e le caratteristiche di utilizzo della memoria che hanno portato il codice client a selezionare un elenco per iniziare. I compromessi realizzati nei contenitori Standard sono ben compresi e ben documentati ...non vale la pena svalutare l'educazione del personale di programmazione, materiale di riferimento a disposizione, il tempo per il nuovo personale per arrivare fino a velocità ecc solo per avere un involucro homegrown crescente.

2

Qualsiasi compilatore decente sarà in grado di rendere le vostre classi avvolte veloci come quelle standard, tuttavia le classi potrebbero essere meno utilizzabili nei debugger e in altri strumenti che potrebbero essere stati specializzati per contenitori standard. Anche Probabilmente si sta andando ad avere messaggi di errore sugli errori in fase di compilazione che sono un po 'più criptico di quelli già criptici che un programmatore ottiene quando commettere errori nell'uso di un modello.

tuo convenzione di denominazione non sembra a me meglio di quella standard; in realtà è un IMO un po 'peggiore e un po' pericoloso; per esempio. usa lo stesso CamelCasing per entrambe le classi e metodi e mi chiedo se sai quali sono le limitazioni imposte dallo standard su un nome come _list ...

Inoltre la tua convenzione di denominazione è nota solo a te e probabilmente a pochi altri, invece, quello standard è conosciuto da un numero enorme di programmatori C++ (compresi voi e, si spera, quei pochi altri). E che dire delle librerie esterne che potresti aver bisogno di aggiungere al tuo progetto? Cambierete la loro interfaccia in modo da utilizzare i contenitori personalizzati incartati anziché quelli standard?

Quindi mi chiedo, dove sono i vantaggi di usare la vostra convenzione di denominazione personalizzato? Vedo solo aspetti negativi ...

+0

Per quanto ne so, lo standard C++ riserva solo nomi che iniziano con un trattino basso nello spazio dei nomi globale. [collegamento] (http://stackoverflow.com/q/228783/509880) Rendere i messaggi di errore del modello criptico ancora più criptici è comunque un ottimo punto. – PkmX

+0

Questo è esattamente il limite di cui stavo parlando. Come ho detto prima, l'uso è legale ma IMO pericoloso perché la validità dipende dal contesto. Mettere il carattere di sottolineatura alla fine è IMO un'idea migliore. – 6502

3

Suona come un lavoro per un typedef, non un involucro.

+0

Completamente d'accordo. Infatti, uso sempre typedef ogni volta che utilizzo un container. Non solo per rendere più semplice (più facile, non automaticamente) modificare i contenitori in un secondo momento, ma anche per evitare di ripetere la definizione completa di std :: ... più e più volte (soprattutto per gli iteratori, anche se questo è meno problematico ora con parola chiave automatica). – Patrick

1

Avvolgere i contenitori STL per renderli thread-safe, per nascondersi dai dettagli dell'implementazione dell'utente, per dare all'utente funzionalità limitate ... questi sono validi motivi.

Wrapping uno solo perché non segue la vostra convention involucro è uno spreco di tempo e denaro e potrebbe portare a bug. Accetta solo che STL usi tutto-minuscolo.

0

In risposta alla tua modifica: vi consiglio di dare un'occhiata al modello adattatore design:

Convert l'interfaccia di una classe in un'altra interfaccia clienti si aspettano. L'adattatore consente alle classi di funzionare insieme che non potrebbero altrimenti a causa delle interfacce incompatibili . (. Il design Patterns, E. Gamma et al)

Poi, si dovrebbe cambiare il tuo punto di vista e utilizzare un vocabolario diverso: prendere in considerazione la classe come un traduttrice invece di un involucro ed e preferiscono il termine "adattatore".

Si avrà una classe base astratta che definisce un'interfaccia comune previsto dall'applicazione, e una nuova sottoclasse per ogni specifica applicazione:

class ContainerInterface 
{ 
    virtual Iterator Find(...) = 0; 
}; 
class AVLAdapter : public ContainerInterface 
{ 
    virtual Iterator Find(...) { /* implement AVL */ } 
} 
class RBAdapter : public ContainerInterface 
{ 
    virtual Iterator Find(...) { /* implement RB tree */ } 
} 

Si può facilmente scambiare tra le possibili implementazioni:

ContainerInterface *c = new AVLAdapter ; 
c->Find(...); 
delete c; 
c = new RBAdapter ; 
c->Find(...); 

E questo modello è scalabile: per testare una nuova implementazione, fornire una nuova sottoclasse. Il codice dell'applicazione non cambia.

class NewAdapter : public ContainerInterface 
{ 
    virtual Iterator Find(...) { /* implement new method */ } 
} 
delete c; 
c = new NewAdapter ; 
c->Find(...); 
Problemi correlati