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

Handing type-erased data at runtime - how not to reinvent the wheel?

本文关键字:何不重 新发明 数据 处理 运行时 类型 擦除      更新时间:2023-10-16

我正在编写一些代码,它可以获得如下数据:

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_infos的代码。这有点混乱。

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

我能做些什么来尽量减少我在这方面的浪费?

注:

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

这里的目标应该是尽快回到C++类型的系统中。要做到这一点,应该有一个中心函数,它基于(运行时(data_type进行切换,然后将每个案例移交给(编译时(模板版本。

您还没有指出相关函数的外观,但这里有一个示例:

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从非模板基类继承,这样您就可以从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>{});
}

CCD_ 8和友元可以在C++11中相对容易地实现。用作

struct printer
{
template<typename T>
void operator()(T* t, int n)
{
for(int i = 0; i < n; i++)
std::cout << t[i] << ' ';
std::cout << 'n';
}
};
void foo()
{
visit(printer{}, nullptr, 0);
}

这似乎就是type_traits的用途(https://en.cppreference.com/w/cpp/types)。

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

所有内容都是在编译时定义的。如果您需要运行时信息,您总是可以根据模板的值进行一些调度(例如,如果您也存储枚举(。

使用boost::variantgsl::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
}
}

现在你可以了

auto span = to_span( buff );

然后访问跨度以键入安全访问数据缓冲区。

由于[](auto&&)lambdas,在c++14中编写访问者不那么痛苦,但在c++11中是可行的。

编写template<class...Fs> struct overloaded也可以使编写访问者更容易。有无数的实现。

如果您不能使用boost,您可以将to_span转换为visit_span,并让它接收访问者。

如果您不能使用gsl,那么编写自己的span是很琐碎的。

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