如何在C++中实现具有不同列数据类型的数据表

How to implement a data table with different column data types in C++

本文关键字:数据类型 数据表 C++ 实现      更新时间:2023-10-16

我想实现一个数据表,其中的字段可能有不同的类型。一个字段可以是字符串的矢量。另一个字段可以是浮点向量。字段的类型在编译时是未知的,因为我希望能够从csv文件构造数据表。

我如何在C++中做到这一点?

使用boost::variant,它可以表示一组类型之一:

std::vector<boost::variant<std::string, float>> values;

然后,您可以将访问者应用于变体:

struct visitor_t : boost::static_visitor<> {
    void operator()(std::string const& x) const {
        std::cout << "got string: " << x << 'n';
    }
    void operator()(float x) const {
        std::cout << "got float: " << x << 'n';
    }
};
visitor_t visitor;
for (auto&& value : values) {
    boost::apply_visitor(visitor, value);
}

实例

因为数据类型在编译时是未知的,所以必须在运行时构造和存储这些信息。对于每行的每个字段,可能有三条信息需要编码:

  1. 字段的类型
  2. 字段的值(必须与#1中指定的类型匹配)
  3. (可选)字段的名称

您可以使用多态类型boost::anyboost::variant(或std::anystd::variant,如C++17中所定义),但一个更优雅、更健壮、更节省内存的解决方案将利用每一行都具有相同结构这一事实。

您所做的基本上是创建一个数据库程序。在数据库中,模式对数据的结构进行编码,但与数据本身是分开的。您想要的是一种在运行时对模式进行编码的方法,类似于以下内容:

enum class FieldType {
  // Scalar types:
  Boolean, Integer, FloatingPoint, String,
  // Array types:
  ArrayBit = 0x1000, // This bit set for array types
  Boolean_Array = Boolean | ArrayBit,
  Integer_Array, FloatingPoint_Array, String_Array
};
class FieldSchema {
  FieldType   m_type;
  std::string m_name;  // Optional, if fields are named
  ...
};
class RowSchema {
  std::vector<FieldSchema> m_fields;
  ...
};

数据字段本身只是可能的数据类型的并集。(请注意,将字符串或向量放入并集需要C++11或更高版本。)

union FieldValue {
  bool                     m_boolean;
  int                      m_integer;
  double                   m_floatingpoint;
  std::string              m_string;
  std::vector<bool>        m_boolean_array;
  std::vector<int>         m_integer_array;
  std::vector<double>      m_floatingpoint_array;
  std::vector<std::string> m_string_array;
  // Constructors for each type go here
};

数据行只是数据字段的矢量,带有指向模式的指针:

class RowValue {
  RowSchema*               m_schama;
  std::vector<FieldValue>  m_fields;
  ...
};

现在,对于每个CSV文件,整个表将有一个RowSchema对象,但每行有一个RowValue对象。给定文件的所有RowValue对象将共享(指向)相同的RowSchema对象。读取CSV文件的过程是:

  1. 确定所有行的结构(模式)(可能通过读取第一行)
  2. 构建一个反映该结构的RowSchema对象
  3. 对于每一行:创建一个指向步骤2中的RowSchemaRowValue对象;将每个字段读取为对应的CCD_ 13中指定的正确数据类型;并使用CCD_ 15将该值附加到CCD_

由于这是一个堆栈溢出的答案,而不是关于C++11的教科书,所以我不会详细介绍如何构造包含字符串或向量的并集,也不会介绍如何使用vector::emplace_back。所有这些信息都可以在其他地方获得(例如,cppreference.com)。这也可以在C++03中完成,还可以进行额外的工作来模拟非平凡类型的并集(例如,使用boost::variant)。

很明显,我遗漏了很多细节。我要提到的一个注意事项是,FieldValue的析构函数不足以销毁并集中包含的字符串或向量。相反,您必须在模式中查找数据类型,并显式调用字段的正确析构函数。因此,RowValue的析构函数必须对字段进行迭代,并分别销毁每个字段。C++17 std::variant(或boost::variant)在这里会有所帮助,但需要额外的内存。

我也尝试过类似的东西:

class Component;
class Field : public Component
{
  // Common interface methods
  public:  
    virtual std::string get_field_name() const = 0;
    virtual std::string get_value_as_string() const = 0;
};
class Record : public Component
{
  // Common interface methods
  std::vector< std::unique_ptr<Component> > fields;
};
class Integer_Field : public Field;

其思想是Record可以包含各种字段。各种字段由指向Component基类的指针实现。这允许记录包含子记录。

你应该看到Sean Parent关于"继承是邪恶的基类"的演讲。你可以在这里的"价值语义和基于概念的多态性"下看到它的打印格式

他提出了一个基于概念的对象类,定义了容器元素的接口。任何满足所需接口(即具有所需的独立功能)的对象都可以放入容器中。

您可能可以从下面的代码示例(取自我上面链接的文档)中获得要点。

class object_t {
  public:
    template <typename T>
    object_t(T x) : self_(make_shared<model<T>>(move(x)))
    { }
    friend void draw(const object_t& x, ostream& out, size_t position)
    { x.self_->draw_(out, position); }
  private:
    struct concept_t {
        virtual ~concept_t() = default;
        virtual void draw_(ostream&, size_t) const = 0;
    };
    template <typename T>
    struct model : concept_t {
        model(T x) : data_(move(x)) { }
        void draw_(ostream& out, size_t position) const 
        { draw(data_, out, position); }
        T data_;
    };
   shared_ptr<const concept_t> self_;
};

您的每个字段都是object_t类型中的一个,可以采用任何类型(std::vector<int>std::deque<float>std::string等)。您只需要确保为object_t支持的任何方法(在本例中,它只是draw())都是为您的不同输入定义的。这很好,因为它为您提供了值语义,还使添加新类型变得非常简单。