Che tipo di interfaccia dovrebbe offrire un doppio contenitore?

4

Voglio scrivere una classe che offra due sequenze di elementi ai suoi utenti. Il primo (chiamiamolo "primario") è il principale della classe e verrà utilizzato l'80% delle volte. Il secondo (chiamiamolo "secondario") è meno importante, ma deve comunque essere nella stessa classe. La domanda è: quale interfaccia dovrebbe offrire la classe ai suoi utenti?

Guardando lo stile STL, una classe con una singola sequenza di elementi dovrebbe offrire le funzioni begin () e end () per attraversare e funzionare come insert () e cancella () per le modifiche.

Ma come dovrebbe la mia classe offrire la seconda sequenza?

Per ora, ho due idee:

  • Esporre i due contenitori all'utente (per quanto riguarda la legge di Demeter ?)
  • Fornire il contenitore principale con l'interfaccia STL ed esporre solo il secondo.

Ecco un esempio.

#include <vector>

class A { 
    public:
        std::vector<int>&  primary();
        std::vector<char>& secondary();

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
};

class B { 
    public:
        std::vector<int>::iterator begin();
        std::vector<int>::iterator end();
        std::vector<char>& secondary();

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
};

// Classes implementation
// ...

int main() {

    // --------------------------------------------------
    // Case 1
    // --------------------------------------------------

    A a;

    for(auto it = a.primary().begin(); it != a.primary().end(); ++it) {
        // ...
    }   
    for(auto it = a.secondary().begin(); it != a.secondary().end(); ++it) {
        // ...
    }   

    // --------------------------------------------------
    // Case 2
    // --------------------------------------------------

    B b;

    for(auto it = b.begin(); it != b.end(); ++it) {
        // ...
    }   
    for(auto it = b.secondary().begin(); it != b.secondary().end(); ++it) {
        // ...
    }   
}

Qual è il modo più c ++ di farlo? È uno dei migliori dell'altro o esiste un'altra soluzione?

Contesto

Questo problema è venuto nel contesto di un esercizio in cui sto scrivendo un semplice framework di accesso al database. Ecco le classi coinvolte nella domanda:

  • tabella
  • Colonna
  • riga
  • campo

La classe della tabella consiste in una sequenza di colonne e un'altra sequenza di righe. L'uso principale della tabella è la manipolazione (accesso, aggiunta e rimozione) delle righe.

Più profondo nella gerarchia, una riga è fatta di colonna e campo in modo che un utente possa chiedere il valore (campo) corrispondente a una determinata colonna o nome di colonna. Ogni volta che una colonna viene aggiunta / modificata / rimossa dalla tabella, ogni riga dovrà essere modificata per riflettere la modifica.

Voglio che l'interfaccia sia semplice, estensibile e che combini bene con il codice esistente (come STL o Boost).

    
posta authchir 11.12.2011 - 04:20
fonte

4 risposte

3

Non esporre il tuo coraggio, guida i visitatori :

class A { 
    public:

        // we assume you want read-only versions, if not you can add non-const versions
        template< class Func >
        void for_each_primary( Func f ) const { for_each_value( f, m_primary ); }

        template< class Func >
        void for_each_secodary( Func f ) const { for_each_value( f, m_secondary ); }

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;

        template< class Func, class Container >
        void for_each_value( Func f, const Container& c )
        {
             for( auto i : c )
                 f( i );
        }
};



int main()
{
    A a;
    a.for_each_primary( [&]( int value ) 
         { std::cout << "Primary Value : " << value ; } );
    a.for_each_secondary( [&]( int value ) 
         { std::cout << "Secondary Value : " << value ; } )
}

Si noti che è possibile utilizzare std :: function invece del parametro template se si desidera inserire l'implementazione in un file cpp, rendendo le modifiche all'implementazione meno costose nei tempi di compilazione nei grandi progetti.

Inoltre, non ho provato a compilarlo ora, ma ho usato molto questo pattern nei miei progetti open-source.

Questa soluzione è un miglioramento di C ++ 11 di B immagino.

TUTTAVIA

Questa soluzione presenta diversi problemi:

  1. Richiede che C ++ 11 sia efficace, perché è efficace per l'utente della tua classe SOLO se può usare lambda.
  2. Si basa sul fatto che l'implementatore di classe sa veramente quali algoritmi devono essere disponibili per gli utenti. Se l'utente ha bisogno di fare manipolazioni complesse ai numeri, saltando da un indice all'altro in un modo imprevedibile per esempio, esponendo gli iteratori, una copia dei valori OPPURE i valori sarebbero migliori.

In effetti, questo tipo di scelta dipende totalmente da ciò che intendi fare all'utente con questa classe.

Di default preferisco la soluzione che ti ho dato perché è la più "isolata", assicurandoti che la classe sappia come i valori possono essere manipolati da un codice esterno. È un po 'come "punti di estensione". Se si tratta di una mappa, fornire una funzione di ricerca alla tua classe è facile. Quindi penso che sia il modo più sensato per esporre i dati ed è anche reso disponibile da lambdas.

Come detto, se hai bisogno di assicurarti che l'utente possa manipolare i dati come preferisce, allora fornire una copia del contenitore è la prossima opzione "isolata" (magari con un modo per reimpostare il contenitore con la copia dopo quella ). Se una copia fosse costosa, allora gli iteratori sarebbero migliori. Se non bastasse, un riferimento è accettabile ma è certamente una cattiva idea.

Ora, supponendo che tu stia utilizzando C ++ 11 e non vuoi fornire algoritmi, il modo più idiomatico è quello di utilizzare gli iteratori in questo modo (solo il codice utente cambia):

class B { 
    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
    public:

        // your code is read-write enabled... make sure it's not const_iterator you want
        // also I'm using decltypt to allow changing container type without having to manually change functions signatures
        decltype(m_primary)::iterator primary_begin() const;
        decltype(m_primary)::iterator primary_end() const;

        decltype(m_secondary)::iterator secondary_begin() const;
        decltype(m_secondary)::iterator secondary_end() const; 


};

int main()
{
    B b;


    std::for_each( b.primary_begin(), b.primary_end(), []( int& value ) {
        // ...
    });   
    std::for_each( b.secondary_begin(), b.secondary_end(), []( double& value ) {
        // ...
    });   

}
    
risposta data 11.12.2011 - 11:25
fonte
4

Poiché stai efficacemente reimplementando Boost.MultiIndex e la maggior parte degli sviluppatori di C ++ dovrebbe avere almeno una vaga familiarità con le librerie Boost, considerare la duplicazione della loro interfaccia. Usano un oggetto indice per indice e contenitore che pubblica begin() e end() :

class A {
    struct primary {
        struct iterator;
        iterator begin();
        iterator end();
    };
    struct secondary {
        struct iterator;
        iterator begin();
        iterator end();
    };
    primary primary_view();
    secondary secondary_view();
};

Uno dei vantaggi è che puoi dare agli oggetti vista qualsiasi funzione che si aspetta contenitori o intervalli di incremento.

    
risposta data 12.12.2011 - 09:19
fonte
3

Si noti che molti container STL hanno una seconda interfaccia per scorrere nella direzione opposta: rbegin e rend !

Quindi esiste la tecnica anteriore per usare begin() e end() per l'interfaccia primaria, e ad es. sec_begin() e sec_end() per l'interfaccia secondaria.

Qualcosa come (typedefs aggiunto per una maggiore leggibilità):

class B { 
    public:
        typedef std::vector<int>::iterator iterator;
        typedef std::vector<char>::iterator secondary_iterator;

        iterator begin() { return m_primary.begin(); }
        iterator end()   { return m_primary.end(); }

        secondary_iterator sec_begin() { return m_secondary.begin(); }
        secondary_iterator sec_end()   { return m_secondary.end(); }

    private:
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
};

Aggiunta aggiuntiva : nota che vengono restituiti gli iteratori non const. Se vuoi che entrambi i contenitori agiscano sincronizzati (ovvero cambiando uno dovrebbe cambiare l'altro), potrebbe essere meglio restituire le versioni const_iterator . begin() / end() / ... può diventare funzioni membro const.

I typedef che ho aggiunto aiuteranno a cambiare se decidi di farlo.

    
risposta data 12.12.2011 - 11:17
fonte
-1

Un modo corretto sarebbe implementare in questo modo:

    #include <vector>
    struct A { 
        std::vector<int>  m_primary;
        std::vector<char> m_secondary;
    };

int main()
{
  A a;
  for ( auto & it : a.m_primary )
  {
  }
  for ( auto & it : a.m_secondary )
  {
  }
}

Come puoi vedere, non hai bisogno di un'interfaccia per tali classi.

    
risposta data 11.12.2011 - 10:47
fonte

Leggi altre domande sui tag