C++ 在运行时处理类型擦除的数据-如何不重新发明轮子?

C++ 在运行时处理类型擦除的数据-如何不重新发明轮子?,c++,c++11,reflection,runtime,compile-time,c++14,C++,C++11,Reflection,Runtime,Compile Time,C++14,我正在编写一些代码,这些代码获取的数据如下所示: enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP }; struct buffer { data_type element_type; size_t size; // in elements of element_type, not bytes void* data; } (这被简化了;实际上,这个结构中还有很多类型、更多字段等。) 现在

我正在编写一些代码,这些代码获取的数据如下所示:

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP };
struct buffer {
    data_type element_type;
    size_t    size; // in elements of element_type, not bytes
    void*     data;
}
(这被简化了;实际上,这个结构中还有很多类型、更多字段等。)

现在,我发现自己编写了一系列实用程序代码,以便在编译时将枚举值“转换”为实际类型,反之亦然。然后我意识到我需要做一些,我也需要在运行时做同样的事情,并且使用可变数量的缓冲区。。。现在,除了基于类型特征的值查找和基于枚举模板参数的类型查找之外,我正在编写查找
std::type_info
s的代码。有点乱

但真的-我不应该这样做。这是重复的,我绝对相信我正在重新发明轮子——实现一些已经编写过很多次的东西:编译器、DBMS、数据文件解析器、序列化库等等

我能做些什么来减少我在这项工作上浪费的精力

注:

  • 我在运行时得到这些缓冲区,不能在编译时取消擦除类型(例如,使用type_)
  • 我无法更改API。或者更确切地说,我可以在我的代码中更改我想要的任何内容,但我仍然可以在内存中获取这个布局中的数据
  • 我不只是将这些缓冲区作为输入,我还需要将它们作为输出生成
  • 我有时需要同时处理许多不同的缓冲区,甚至是数量可变的缓冲区(例如,
    foo(buffer*buffers,int num_buffers);
  • C++11解决方案优于较新的标准版本
  • 实际上我经常使用gsl,所以如果你愿意,你可以在你的答案中使用它。至于Boost——这在政治上可能很难依赖,但就StackOverflow问题而言,我想这没关系

这似乎就是类型特征的用途()

基本上,您可以定义一个模板结构,默认情况下它是空的,并为您拥有的每个枚举专门化它。然后在代码中使用
MyTypeTraits::type
获取与所需枚举关联的类型


如果您需要运行时信息,您可以根据模板的值进行一些分派(例如,如果您也存储枚举)这里的目标应该是尽快返回到C++类型的系统中。为了做到这一点,应该有一个中央函数,它根据(运行时)<代码> DATAYTYPE < /COD>进行切换,然后将每个情况都交给(编译时)模板版本。 您尚未指出关联函数的外观,但以下是一个示例:

template<typename T>
struct TypedBuffer
{
  TypedBuffer(void* data, size_t elementCount) { /* ... */ }
  // ...
};

template<typename T>
void handleBufferTyped(void* data, size_t elementCount)
{
  TypedBuffer<T> buf(data, elementCount);
  // Do whatever you want - you're back in the type system.
}

void handleBuffer(buffer buf)
{
  switch (buf.element_type)
  {
  case INT16:     handleBufferTyped<int16_t>(buf.data, buf.size); break;
  case INT32:     handleBufferTyped<int32_t>(buf.data, buf.size); break;
  case UINT64:    handleBufferTyped<uint64_t>(buf.data, buf.size); break;
  case FLOAT:     handleBufferTyped<float>(buf.data, buf.size); break;
  case TIMESTAMP: handleBufferTyped<std::time_t>(buf.data, buf.size); break;
  }
}
模板
结构类型缓冲区
{
TypedBuffer(void*数据,大小\u t元素计数){/*…*/}
// ...
};
样板
void handleBufferTyped(void*数据、大小\u t元素计数)
{
类型Buffer buf(数据,元素计数);
//你想做什么就做什么——你又回到了打字系统。
}
空把手缓冲器(缓冲器buf)
{
开关(基本元件类型)
{
案例INT16:手柄类型化(基本数据,基本尺寸);中断;
案例INT32:手柄类型化(基本数据,基本尺寸);中断;
案例UINT64:手柄类型化(基本数据,基本尺寸);断裂;
箱体浮子:手柄式(基本数据,基本尺寸);断开;
案例时间戳:handleBufferTyped(基本数据,基本尺寸);break;
}
}
如果需要,您还可以让
TypedBuffer
从非模板基类继承,这样您就可以从
handleBuffer
多态返回,但这混合了很多范例,可能是不必要的

如何不重新发明轮子

简单地说,使用
std::variant
以及来回转换。它在标准库中是有原因的

在重新发明轮子时,访问是处理类型擦除数据的最简单的通用机制

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP, size };

template<data_type d>
struct data
{
    using type = void;
};
template<>
struct data<INT16>
{
    using type = int16_t;
};
// and so on

template<data_type d>
using data_t = typename data<d>::type;


template<typename F, typename T>
void indirect(void* f, void* t, int n)
{
    (*(F*)f)((T*)t, n);
}

template<typename F, size_t... Is>
void visit_(F&& f, buffer* bufs, int n, std::index_sequence<Is...>)
{
    using rF = typename std::remove_reference<F>::type;
    using f_t = void(*)(void*, void*, int);
    static constexpr f_t fs[] = {indirect<rF, data_t<data_type(Is)>>...};
    for(int i = 0; i < n; i++)
        fs[bufs[i].element_type](&f, bufs[i].data, bufs[i].size);
}

template<typename F>
void visit(F&& f, buffer* bufs, int n)
{
    visit_(std::forward<F>(f), bufs, n, std::make_index_sequence<data_type::size>{});
}

使用
boost::variant
gsl::span

enum data_type { INT16 = 0, INT32, UINT64, FLOAT, TIMESTAMP };
struct buffer {
  data_type element_type;
  size_t    size; // in elements of element_type, not bytes
  void*     data;
};

template<class...Ts>
using var_span = boost::variant< gsl::span< Ts > ... >;

using buffer_span = var_span< std::int16_t, std::int32_t, std::uint64_t, float, ??? >;

buffer_span to_span( buffer buff ) {
  switch (buff.element_type) {
    case INT16: return gsl::span<std::int16_t>( (std::int16_t*)buff.data, buff.size );
    // etc
  }
}
然后访问span类型以安全访问数据缓冲区

由于[](自动&&)lambdas,在中编写访问者不那么痛苦,但在中是可行的

编写
template struct重载
也可以更容易地编写访问者

如果您不能使用
boost
,您可以将
转换为
访问,并让访问者访问

如果您不能使用
gsl
,那么编写自己的
span
就很简单了

visit_span( buff, overload(
  [](span<int16_t> span) { /* code */ },
  [](span<int32_t> span) { /* code */ },
  // ...
 ));
visit_span(buff,重载(
[](span){/*代码*/},
[](span){/*代码*/},
// ...
));

struct do\u foo{
样板
void运算符()(span){/*代码*/}
};
访问_span(buff,do_foo{captures});

如果你能使用c++17,你可能想看看std::variantYeah,我想指出的是,这可以简化为向量的变体。除非我遗漏了什么。@Chemistree:这没有帮助,因为我无法更改API(请参见编辑)@StoryTeller:请参见编辑。我的一个可能相关的老问题,不是关于变体:为了使用类型特征,您需要在编译时知道枚举值。我只在运行ti.e1时知道。我在问如何避免编写此类内容…2.我还需要翻译回。3.实际缓冲区比这更复杂,并且单开关不起作用。4.这适用于单个缓冲区。但我有多个函数,可以使用多个缓冲区,如果这还不够的话,可以使用可变数量的缓冲区的函数:
void foo(buffer*buffers,int num\u buffers)
@einpoklum 1.回到类型系统正需要这种东西。2.根据你的要求,让每个
TypedBuffer
都有一个到
buffer
的转换函数是很简单的。但是,如果我们不知道它们之间有什么不同/相同,我们就无法为你提供好的代码/抽象你编写了一个不明确的“它更复杂”的代码。4.循环
auto span = to_span( buff );
visit_span( buff, overload(
  [](span<int16_t> span) { /* code */ },
  [](span<int32_t> span) { /* code */ },
  // ...
 ));
 struct do_foo {
   template<class T>
   void operator()(span<T> span) { /* code */ }
 };
 visit_span( buff, do_foo{captures} );