Progettazione di una tabella in memoria in C ++

5

Sto valutando le mie opzioni per strutturare un database in memoria e ho alcune idee su come implementarlo. Mi piacerebbe sapere la tua opinione su quale sia la migliore scelta di design.

Ho una classe di colonne che è parametrizzata per rappresentare diversi tipi di colonne.

template<typename T>
class Column<T> {
public:
   std::string name();
   T sum();
   T avg();
   ...
private:
   std::string name;
   std::vector<T> vec;
   ...
};

Non sono sicuro di quale sia il percorso migliore per memorizzare un vettore di Column con parametri di tipo diversi. Ad esempio una tabella a 3 colonne potrebbe avere una colonna integer, una colonna float e una colonna di stringhe.

So che c'è boost :: variant ma non ho il permesso di usare boost.

Stavo pensando di utilizzare uno dei seguenti:

  1. Tagged Union
  2. Pure OO: estendi la colonna come IntColumn: Column, ecc.

Quali sono i tuoi pensieri? Hai un'idea migliore?

    
posta jimjampez 29.02.2016 - 17:33
fonte

2 risposte

5

Poiché il tipo di una colonna è un parametro di modello, stai modellando il tipo di colonna all'interno del sistema di tipi C ++. Questo è buono. A Column<int> e Column<std::string> sono tipi diversi. Se esistono alcune proprietà comuni a tutti i tipi di colonna (ad esempio che una colonna ha un nome), è possibile estrarle in una classe base in modo che sia possibile accedere a queste operazioni comuni tramite un tipo comune. Tuttavia, in questa base non possono esistere operazioni specifiche del tipo come get() o sum() , e devono essere parte del Column<T> con template.

Se hai un tipo di tabella che ha colonne di tipi diversi , è chiaramente impensabile forzare questi ad avere lo stesso tipo dato che perderesti necessariamente l'accesso al parametro template ("cancellazione tipo “). Invece, abbraccia i diversi tipi e rendi anche il tuo Table strongmente digitato. Un contenitore come std::tuple<T...> può aiutarti qui.

Se hai bisogno di accedere alle parti indipendenti del tipo di colonna, puoi sempre ottenere un puntatore alla colonna che può essere utilizzata come tipo di base.

Uno schizzo con C ++ 14 (C ++ 11 richiederebbe di implementare un paio di funzioni pratiche da soli, ma ha std::tuple e pacchetti di parametri template):

class ColumnBase {
  ...
public:
  std::string name() { … }
};

template<class T>
class Column : public ColumnBase {
  std::vector<T> m_items;
  ...
};

template<class... T>
class Table {
  std::tuple<Column<T>...> m_columns;

  template<std::size_t... index>
  std::vector<ColumnBase*> columns_vec_helper(std::index_sequence<index...>) {
    return { (&std::get<index>(m_columns))... };
  }

public:
  std::vector<ColumnBase*> columns_vec() {
    return columns_vec_helper(std::make_index_sequence<sizeof...(T)>{});
  }
};

Potremmo quindi stampare il nome di tutte le colonne:

for (const auto& colBase : table.columns_vec())
  std::cout << "column " << colBase->name() << "\n";

senza dover gestire separatamente ogni tipo di colonna.

( demo eseguibile su ideone )

Solo i modelli ti daranno la sicurezza del tipo che ottieni un int da una colonna intera. Al contrario, i tipi di unioni / varianti richiedono l'uso del codice per ricordare tutti i tipi possibili (con il modello, il controllo del tipo impone che gestiamo tutto). Con la sottotipizzazione, non possiamo avere operazioni specifiche di tipo di colonna che condividono un'implementazione. Cioè un metodo int IntColumn::get(std::size_t i) e un metodo correlato const std::string& StringColumn::get(std::size_t i) potrebbero apparire come se avessero un'interfaccia comune, ma ciò sarebbe solo accidentale e non può essere applicato. In particolare, qualsiasi combinazione di metodi e modelli virtuali in C ++ diventa molto brutta, molto veloce.

Lo svantaggio dei modelli è che ti verrà richiesto di scrivere scrupolosamente codice generico e dovrai eseguire metaprogrammazione del modello. Se eseguiti correttamente, i risultati possono avere un'incredibile usabilità, ma l'implementazione sarebbe avanzata in C ++. Se il tuo progetto è progettato per essere gestito da programmatori meno avanzati (che saranno confusi come lo sarò quando ripenso a questo codice tra un paio di mesi), allora potrebbe essere più sensato evitare una soluzione così "intelligente" nonostante i suoi vantaggi e usi modelli OOP più tradizionali che ti danno una struttura simile, ma potrebbe richiedere un paio di% distatic_cast s per funzionare.

    
risposta data 29.02.2016 - 21:31
fonte
1

Mentre preferisco strongmente l'approccio @amon presentato, ci sono situazioni in cui non è possibile seguire tale percorso, ad esempio configurazioni di tabelle che non sono note fino al runtime.

In tal caso, e poiché l'hai già menzionato, funzionalità come boost:: variant o boost::any potrebbero fornire una buona soluzione.

Dal momento che sembri essere limitato dal fatto che non ti è permesso usare boost, perché non farlo da solo? I due approcci di base stanno usando un'unione taggata o sfruttando il sistema di tipo dinamico di C ++ usando una classe di base polimorfa (e un'interfaccia ben definita o dynamic_cast s, probabilmente nascosta dietro un visitatore aciclico)

Mi riferisco a una mia risposta su SO mostrando uno schizzo di base di entrambi gli approcci, con un link ad un più completo boost::any come implementazione.

    
risposta data 08.03.2016 - 10:07
fonte

Leggi altre domande sui tag