在运行时处理类型擦除的数据-如何不浪费时间?

时间:2018-10-31 09:59:19

标签: c++ c++11 reflection runtime compile-time

我正在处理一些代码,该代码获取的数据如下所示:

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

但实际上-我不应该这样做。这是重复的,而且我绝对可以确定自己正在重新发明轮子-实现已经被多次编写的东西:编译器,DBMS,数据文件解析器,序列化库等。

该如何做才能最大程度地减少在此方面浪费的精力?

注意:

  • 我在运行时获得了这些缓冲区,不能仅仅在编译时就删除类型(例如,使用type_traits)。
  • 我无法更改API。或者说,我可以更改代码中想要的任何内容,但仍然可以在内存中以这种布局获取数据。
  • 我不仅将此类缓冲区作为输入,还需要将其作为输出。
  • 我有时需要一次处理许多不同的缓冲区,甚至要处理数量不定的缓冲区(例如foo(buffer* buffers, int num_buffers);
  • C ++ 11解决方案优于新标准版本的解决方案。
  • 我实际上经常使用gsl,因此您可以根据需要在答案中使用它。至于Boost-在政治上可能很难依靠,但是出于StackOverflow问题的目的,我想很好。

4 个答案:

答案 0 :(得分:3)

这里的目标应该是尽快回到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返回,但这混合了很多范例,可能没有必要。

答案 1 :(得分:2)

  

如何不重新发明轮子?

只需将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>{});
}

std::index_sequence和朋友可以在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);
}

答案 2 :(得分:1)

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

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

一切都在编译时定义。如果需要运行时信息,则始终可以根据模板的值进行一些调度(例如,如果您还存储枚举)。

答案 3 :(得分:1)

使用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&&)的lambda,写作访客的痛苦较小,但在中是可行的。

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