Criando uma tabela na memória em C ++

5

Estou avaliando minhas opções para estruturar um banco de dados em memória e tenho algumas idéias de como implementá-lo. Eu gostaria de saber sua opinião sobre qual é a melhor escolha de design.

Eu tenho uma classe de coluna que é parametrizada para representar diferentes tipos de colunas.

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

Não sei muito bem qual é a melhor rota para armazenar um vetor de coluna com parâmetros de tipo diferentes. Por exemplo, uma tabela de 3 colunas pode ter uma coluna inteira, uma coluna flutuante e uma coluna de string.

Eu sei que há boost :: variant mas não tenho permissão para usar boost.

Eu estava pensando em usar um dos itens a seguir:

  1. União marcada
  2. Pure OO: estenda a coluna como IntColumn: Column, etc.

Quais são seus pensamentos? Tem uma ideia melhor?

    
por jimjampez 29.02.2016 / 17:33
fonte

2 respostas

5

Como o tipo de coluna é um parâmetro de modelo, você está modelando o tipo de coluna no sistema de tipos C ++. Isso é bom. Um Column<int> e Column<std::string> são tipos diferentes. Se houver algumas propriedades comuns a todos os tipos de colunas (por exemplo, se uma coluna tiver um nome), você poderá extraí-las em uma classe base para que essas operações comuns possam ser acessadas por meio de um tipo comum. No entanto, nenhuma operação específica de tipo, como get() ou sum() , pode existir nessa base e deve fazer parte do modelo Column<T> .

Se você tem um tipo de tabela que tem colunas de tipos diferentes , claramente não é sensato forçá-los a ter o mesmo tipo, pois você perderia necessariamente o acesso ao parâmetro do modelo (“tipo apagar ”). Em vez disso, adote os diferentes tipos e também faça com que o seu Table seja strongmente tipado. Um contêiner como std::tuple<T...> pode ajudar aqui.

Se você precisar acessar as partes independentes do tipo coluna, poderá sempre obter um ponteiro para a coluna que pode ser usada como o tipo base.

Um esboço usando C ++ 14 (C ++ 11 exigiria que você mesmo implementasse algumas funções de conveniência, mas possui std::tuple e pacotes de parâmetros de modelo):

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)>{});
  }
};

Poderíamos então imprimir o nome de todas as colunas:

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

sem ter que lidar com cada tipo de coluna separadamente.

( demonstração executável sobre o ideone )

Somente os modelos darão a você o tipo de segurança de que você obterá um int de uma coluna inteira. Em contraste, os tipos de uniões / variantes requerem o código de uso para lembrar de todos os tipos possíveis (com o modelo, o verificador de tipos impõe que lidamos com tudo). Com a subtipagem, não podemos ter operações específicas do tipo coluna que compartilhem uma implementação. Ou seja um método int IntColumn::get(std::size_t i) e um método relacionado const std::string& StringColumn::get(std::size_t i) podem parecer que eles têm uma interface comum, mas isso seria apenas acidental e não pode ser aplicado. Em particular, qualquer combinação de métodos e modelos virtuais em C ++ fica muito feia, muito rápida.

A desvantagem dos modelos é que você precisará escrever cuidadosamente o código genérico e terá que fazer a metaprogramação do modelo. Quando feito corretamente, os resultados podem ter uma usabilidade incrível, mas a implementação seria avançada em C ++. Se o seu design é destinado a ser mantido por programadores menos avançados (o que será tão confuso quanto eu estarei quando voltar a esse código em alguns meses), então seria mais sensato evitar uma solução tão “inteligente”. apesar de seus benefícios e usar padrões OOP mais tradicionais que fornecem uma estrutura semelhante, mas podem exigir alguns static_cast s para funcionar.

    
por 29.02.2016 / 21:31
fonte
1

Enquanto eu strongmente favorece a abordagem @amon apresentada, há situações em que você não pode seguir essa rota, por exemplo, configurações de tabela que não são conhecidas até o tempo de execução.

Nesse caso, e como você já mencionou, funcionalidade como boost:: variant ou boost::any pode fornecer uma boa solução.

Já que você parece estar limitado por não ter permissão para usar o boost, por que não criar o seu próprio? As duas abordagens básicas estão usando uma união marcada ou explorando o sistema de tipo dinâmico do C ++ usando uma classe base polimórfica (e uma interface bem definida ou dynamic_cast s, provavelmente escondida atrás de um visitante acíclico)

Estou referenciando uma resposta minha ao SO mostrando um esboço básico de ambas as abordagens, com um link para um mais completo boost::any como implementação.

    
por 08.03.2016 / 10:07
fonte

Tags