C++中针对纯结构体数组的通用TableView/DataFrame功能

4

我试图在任意结构的连续数组(称之为 DataItem)的顶部制作轻量级层,该层将处理常见的操作,如文件IO、在屏幕/GUI上呈现(如Excel表格)、按不同属性搜索和排序等。

但是我希望我的 class Table 和用户定义的结构/类 DataItem 完全独立于彼此(即两者都可以编译而不知道彼此的头文件.h)。我认为这不能像这样实现:template<class T> class Table{ std::vectro<T> data;};,因为那么用户将被强制实现功能,如 DataItem::toString(int icolumn),而我不想对 DataItem 结构施加这种限制。

我的当前实现依赖于指针算术、switch,并且只能处理少量数据成员类型(bool、int、float、double)。我想知道是否可以通过使用模板等方式来改进它(使其更通用、安全等),而不会显著增加复杂性和性能成本。

我想这样使用它:

#include "Table.h"
#include "GUI.h"
#include "Vec3d.h"

// example of user defined DataItem struct
struct TestStruct{ 
    int    inum = 115;
    double dnum = 11.1154546;
    double fvoid= 0.0;
    float  fnum = 11.115;
    Vec3d  dvec = (Vec3d){ 1.1545, 2.166, 3.1545};
};

int main(){

    // ==== Initialize test data
    Table* tab1 = new Table();
    tab1->n      =  120;
    TestStruct* tab_data = new TestStruct[tab1->n];
    for(int i=0; i<tab1->n; i++){ 
       tab_data[i].inum = i; 
       tab_data[i].fnum = i*0.1; 
       tab_data[i].dnum = i*0.01; 
    }

    // ==== Bind selected properties/members of TestStruct as columns int the table
    tab1->bind(tab_data, sizeof(*tab_data) );
    // This is actually quite complicated =>
    // I would be happy if it could be automatized by some template magic ;-)
    tab1->addColum( &(tab_data->inum), 1, DataType::Int    );
    tab1->addColum( &(tab_data->fnum), 1, DataType::Float  );
    tab1->addColum( &(tab_data->dnum), 1, DataType::Double );
    tab1->addColum( &(tab_data->dvec), 3, DataType::Double );

    // ==== Visualize the table Table in GUI
    gui.addPanel( new TableView( tab1, "tab1", 150.0, 250.0,  0, 0, 5, 3 ) );
    gui.run();

}

我的当前实现看起来像这样:

enum class DataType{ Bool, Int, Float, Double, String };

struct Atribute{
    int      offset;  // offset of data member from address of struct instance [bytes]
    int      nsub;    // number of sub units. e.g. 3 for Vec3
    DataType type;    // type for conversion
    Atribute() = default;
    Atribute(int offset_,int nsub_,DataType type_):offset(offset_),nsub(nsub_),type(type_){};
};

class Table{ public:
    int n;              // number of items/lines in table
    int   itemsize = 0; // number of bytes per item
    char* data     = 0; // pointer to data buffer with structs; type is erased to make it generic  

    std::unordered_map<std::string,int> name2column;
    std::vector       <Atribute>        columns;

    void bind(void* data_, int itemsize_){
        data=(char*)data_;
        itemsize=itemsize_;
    }

    int addColum(void* ptr, int nsub, DataType type){
        // determine offset of address of given data-member with respect to address of enclosing struct
        int offset = ((char*)ptr)-((char*)data);
        columns.push_back( Atribute( offset, nsub, type ) );
        return columns.size()-1;
    }

    char* toStr(int i, int j, char* s){
        const Atribute& kind = columns[j];
        void* off = data+itemsize*i+kind.offset; // address of j-th member of i-th instance in data array
        // I don't like this switch, 
        // but still it seems simpler and more efficient that alternative solutions using 
        // templates/lambda function or function pointers
        switch(kind.type){
            case DataType::Bool   :{ bool*   arr=(bool  *)off; for(int i=0; i<kind.nsub; i++){ s+=sprintf(s,"%c ",  arr[i]?'T':'F' ); }} break;
            case DataType::Int    :{ int*    arr=(int   *)off; for(int i=0; i<kind.nsub; i++){ s+=sprintf(s,"%i ",  arr[i] ); }} break;
            case DataType::Float  :{ float*  arr=(float *)off; for(int i=0; i<kind.nsub; i++){ s+=sprintf(s,"%g ",  arr[i] ); }} break;
            case DataType::Double :{ double* arr=(double*)off; for(int i=0; i<kind.nsub; i++){ s+=sprintf(s,"%g ",  arr[i] ); }} break;
            case DataType::String :{ char*   arr=(char  *)off; for(int i=0; i<kind.nsub; i++){ s+=sprintf(s,"%s ",  arr[i] ); }} break;
        }
        return s;
    }
};

    // .... Ommited most of TableView GUI ....

    void TableView::render(){
        Draw  ::setRGB( textColor );
        char stmp[1024];
        for(int i=i0; i<imax;i++){
            int ch0 = 0;
            for(int j=j0; j<jmax;j++){
                int nch = table->toStr(i,j,stmp)-stmp; // HERE!!! I call Table::toStr()
                Draw2D::drawText( stmp, nch, {xmin+ch0*fontSizeDef, ymax-(i-i0+1)*fontSizeDef*2}, 0.0,  GUI_fontTex, fontSizeDef );
                ch0+=nchs[j];
            }
        }
    }


我建议您提供一个样例 Table.ccTable.hDataItem.ccDataItem.h,以及可能使用它们的 prog.cc,所有这些文件都应该是极简主义的,以满足您的需求。您所提供的内容只是这个的粗略代理。 - sancho.s ReinstateMonicaCellio
2个回答

3

一种解决这种问题的方法是提供一个“特性”类,告诉一个类如何处理另一个类而无需修改第二个类。这种模式在标准库中广泛使用。

您的代码可以写成:

#include <iostream>
#include <string>
#include <vector>
#include <array>

template <typename T>
struct TableTraits;

template <typename T>
class Table
{
public:
  void setData( const std::vector<T>& value )
  {
    data = value;
  }

  std::string toString( size_t row, size_t column )
  {
    return TableTraits<T>::toString( data[ row ], column );
  }

  void print()
  {
    for ( size_t row = 0; row < data.size(); row++ )
    {
      for ( size_t column = 0; column < TableTraits<T>::columns; column++ )
      {
        std::cout << toString( row, column ) << ", ";
      }
      std::cout << "\n";
    }
  }
private:
  std::vector<T> data;
};

struct TestStruct
{
  int    inum = 115;
  double dnum = 11.1154546;
  double fvoid = 0.0;
  float  fnum = 11.115f;
  std::array<double, 3> dvec = { 1.1545, 2.166, 3.1545 };
};

template <typename T>
std::string stringConvert( const T& value )
{
  return std::to_string( value );
}

template <typename T, size_t N>
std::string stringConvert( const std::array<T, N>& value )
{
  std::string result;
  for ( auto& v : value )
  {
    result += stringConvert( v ) + "; ";
  }
  return result;
}

template <>
struct TableTraits<TestStruct>
{
  static const size_t columns = 5;

  static std::string toString( const TestStruct& row, size_t column )
  {
    switch ( column )
    {
    case 0:
      return stringConvert( row.inum );
    case 1:
      return stringConvert( row.dnum );
    case 2:
      return stringConvert( row.fvoid );
    case 3:
      return stringConvert( row.fnum );
    case 4:
      return stringConvert( row.dvec );
    default:
      throw std::invalid_argument( "column out of range" );
    }
  }
};

int main()
{
  std::vector<TestStruct> data( 10 );
  Table<TestStruct> table;
  table.setData( data );
  table.print();
}

如果这个示例不完全符合您的需求,特征类的确切细节可能会有所不同。

您可能还发现,将特征方法和常量设置为非静态的,这样您就可以将特征对象传递到表中,以允许每个表实例进行自定义。

您可能还希望允许在您的表中使用自定义特征类,例如:

template <typename T, typename Traits = TableTraits<T>>
class Table
{
...
  std::string toString( size_t row, size_t column )
  {
    return Traits::toString( data[ row ], column );
  }

这很好,我在这里学到了一些东西。谢谢!但是我实际想要的是在运行时动态添加/删除列的功能,或者重新将表格绑定到不同类型结构体的数组。但是我没有清楚地说明,因为我现在才意识到这是主要问题。 - Prokop Hapala
如果您将特征类设置为非静态,则可以在运行时更改其返回的值。使用模板无法更改表格类型,但是可以使用类似的方式,并具有虚基础特征类,您可以为每个数据结构专门化。 - Alan Birtles

0

我看你正在尝试使用C语言结构和C++中的C语言多态性来实现动态多态性(在运行时)。模板对于静态多态性非常有用。正确的方向是使用面向对象编程(特别是类多态性),将概念定义为类:表格、单元格、列、行、单元格值等。你可以在Github上查看以下示例:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接