在运行时处理类型擦除的数据-如何不重新发明轮子
Handing type-erased data at runtime - how not to reinvent the wheel?
我正在编写一些代码,它可以获得如下数据:
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_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::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
}
}
现在你可以了
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} );
- 如何将stdout重定向到stderr
- 使用新行和不使用新行读取文件
- 如何在选项卡视图Qt中设置一个新项目,并保存以前的项目
- 在C++中,是否可以基于给定的标识符创建基类的新实例,反之亦然
- 遇到新行时,有没有办法停止istream_iterator
- QTableView:endMoveRows在模型中重置水平页眉大小
- Visual studio代码重构似乎不起作用(例如,重命名符号-f2)
- 在运行时处理类型擦除的数据-如何不重新发明轮子
- Constexpr替代了新的放置方式,可以让内存中的对象保持未初始化状态
- 编写时C++中的输入重定向问题
- 当一个新对象被分配到它的地址时,对象是否必须被销毁
- 模板元编程:如何将参数包组合成新的参数包
- 使用不同的CRT将新的C++代码与旧的(二进制)组件隔离开来的最佳方法是什么
- 如何使用CLion在Mac上创建一个新的.txt文件
- 重置QMENU以接受新的Qactions
- 重置对象m_object=对象(新,参数);
- OpenGL的状态需要在新的共享上下文上重置
- 我可以使用放置新重置一个对象内的shared_ptr
- 重定向计数到用winapi创建的新缓冲区
- 使用readdirectorychangesc++重命名文件夹后获取旧名称和新名称