不相关类型的动态调度解决方案

Ale*_*zov 9 c++ polymorphism runtime c++11 c++14

我正在研究在现代C++(C++ 11/C++ 14)中动态调度不相关类型的可能实现.

通过"动态分派类型",我的意思是在运行时我们需要通过其整数索引从列表中选择一个类型并对其执行某些操作(调用静态方法,使用类型特征等).

例如,考虑序列化数据流:有几种数据值,它们以不同方式序列化/反序列化; 有几个编解码器,它们进行序列化/反序列化; 我们的代码从流中读取类型标记,然后决定它应该调用哪个编解码器来读取完整值.

我对许多操作感兴趣,这些操作可以在类型上调用(几个静态方法,类型特征......),并且可以是从逻辑类型到C++类的不同映射,而不仅仅是1:1(在使用序列化的例子意味着可能存在由同一编解码器序列化的多种数据类型.

我还希望避免手动代码重复,并使代码更易于维护,并且不易出错.表现也很重要.

目前我正在看到那些可能的实现,我错过了什么?这可以做得更好吗?

  1. 使用switch-case手动编写尽可能多的函数,因为类型上可能有操作调用.

    size_t serialize(const Any & any, char * data)
    {
        switch (any.type) {
            case Any::Type::INTEGER:
                return IntegerCodec::serialize(any.value, data);
            ...
        }
    }
    Any deserialize(const char * data, size_t size)
    {
        Any::Type type = deserialize_type(data, size);
        switch (type) {
            case Any::Type::INTEGER:
                return IntegerCodec::deserialize(data, size);
            ...
        }
    }
    bool is_trivially_serializable(const Any & any)
    {
        switch (any.type) {
            case Any::Type::INTEGER:
                return traits::is_trivially_serializable<IntegerCodec>::value;
            ...
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

优点:简单易懂; 编译器可以内联调度方法.

缺点:它需要大量的手动重复(或通过外部工具生成代码).

  1. 像这样创建调度表

    class AnyDispatcher
    {
    public:
        virtual size_t serialize(const Any & any, char * data) const = 0;
        virtual Any deserialize(const char * data, size_t size) const = 0;
        virtual bool is_trivially_serializable() const = 0;
        ...
    };
    class AnyIntegerDispatcher: public AnyDispatcher
    {
    public:
        size_t serialize(const Any & any, char * data) const override
        {
            return IntegerCodec::serialize(any, data);
        }
        Any deserialize(const char * data, size_t size) const override
        {
            return IntegerCodec::deserialize(data, size);
        }
        bool is_trivially_serializable() const
        {
            return traits::is_trivially_serializable<IntegerCodec>::value;
        }
        ...
    };
    ...
    
    // global constant
    std::array<AnyDispatcher *, N> dispatch_table = { new AnyIntegerDispatcher(), ... };
    
    size_t serialize(const Any & any, char * data)
    {
        return dispatch_table[any.type]->serialize(any, data);
    }
    Any deserialize(const char * data, size_t size)
    {
        return dispatch_table[any.type]->deserialize(data, size);
    }
    bool is_trivially_serializable(const Any & any)
    {
        return dispatch_table[any.type]->is_trivially_serializable();
    }
    
    Run Code Online (Sandbox Code Playgroud)

优点:它更灵活一点 - 需要为每个调度类型编写一个调度程序类,然后可以将它们组合在不同的调度表中.

缺点:它需要编写大量的调度代码.由于虚拟调度和将codec的方法内联到调用者站点的不可能性,因此存在一些开销.

  1. 使用模板化调度功能

    template <typename F, typename... Args>
    auto dispatch(Any::Type type, F f, Args && ...args)
    {
        switch (type) {
            case Any::Type::INTEGER:
                return f(IntegerCodec(), std::forward<Args>(args)...);
            ...
        }
    }
    
    size_t serialize(const Any & any, char * data)
    {
        return dispatch(
                    any.type,
                    [] (const auto codec, const Any & any, char * data) {
                        return std::decay_t<decltype(codec)>::serialize(any, data);
                    },
                    any,
                    data
                );
    }
    bool is_trivially_serializable(const Any & any)
    {
        return dispatch(
                    any.type,
                    [] (const auto codec) {
                        return traits::is_trivially_serializable<std::decay_t<decltype(codec)>>::value;
                    }
                );
    }
    
    Run Code Online (Sandbox Code Playgroud)

优点:它只需要一个switch-case调度函数和每个操作调用中的一些代码(至少手动编写).编译器可以内联它认为合适的内容.

缺点:它更复杂,需要C++ 14(如此干净和紧凑)并依赖于编译器能力来优化掉未使用的编解码器实例(仅用于为编解码器选择正确的重载).

  1. 对于一组逻辑类型,可能有几个映射到实现类(在此示例中为编解码器),最好概括解决方案#3并编写完全通用的调度函数,它接收类型值和调用类型之间的编译时映射.像这样的东西:

    template <typename Mapping, typename F, typename... Args>
    auto dispatch(Any::Type type, F f, Args && ...args)
    {
        switch (type) {
            case Any::Type::INTEGER:
                return f(mpl::map_find<Mapping, Any::Type::INTEGER>(), std::forward<Args>(args)...);
            ...
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

我倾向于解决方案#3(或#4).但我确实想知道 - 是否可以避免手动编写dispatch功能?我的意思是它的开关盒.这个switch-case完全来自类型值和类型之间的编译时映射 - 有没有任何方法可以处理它对编译器的生成?

Yak*_*ont 5

标记分派非常有效,您可以在其中传递类型以选择重载。 std库通常将其用于迭代器上的算法,因此不同的迭代器类别将获得不同的实现。

当我有类型ID的列表时,请确保它们是连续的并编写一个跳转表。

这是指向执行手头任务的函数的指针数组。

您可以使用C ++ 11或更高版本自动编写此代码;我称其为魔术开关,因为它的作用类似于运行时开关,并且它会调用基于运行时具有编译时间值的函数。我使用lambda制作函数,并在其中扩展参数包,以使它们的主体不同。然后,它们将分派到传入的函数对象。

写下来,然后就可以将序列化/反序列化代码移到“类型安全”代码中。使用特征可以从编译时索引映射到类型标记,和/或根据索引将其调度到重载函数。

这是C ++ 14魔术开关:

template<std::size_t I>using index=std::integral_constant<std::size_t, I>;

template<class F, std::size_t...Is>
auto magic_switch( std::size_t I, F&& f, std::index_sequence<Is...> ) {
  auto* pf = std::addressof(f);
  using PF = decltype(pf);
  using R = decltype( (*pf)( index<0>{} ) );
  using table_entry = R(*)( PF );

  static const table_entry table[] = {
    [](PF pf)->R {
      return (*pf)( index<Is>{} );
    }...
  };

  return table[I](pf);
}    

template<std::size_t N, class F>
auto magic_switch( std::size_t I, F&& f ) {
  return magic_switch( I, std::forward<F>(f), std::make_index_sequence<N>{} );
}
Run Code Online (Sandbox Code Playgroud)

使用看起来像:

std::size_t r = magic_switch<100>( argc, [](auto I){
  return sizeof( char[I+1] ); // I is a compile-time size_t equal to argc
});
std::cout << r << "\n";
Run Code Online (Sandbox Code Playgroud)

现场例子

如果可以在编译时(通过类型特征或其他方式)将类型枚举注册为类型映射,则可以通过魔术开关来回切换,以将运行时枚举值转换为编译时类型标记。

template<class T> struct tag_t {using type=T;};
Run Code Online (Sandbox Code Playgroud)

那么您可以像这样编写序列化/反序列化:

template<class T>
void serialize( serialize_target t, void const* pdata, tag_t<T> ) {
  serialize( t, static_cast<T const*>(pdata) );
}
template<class T>
void deserialize( deserialize_source s, void* pdata, tag_t<T> ) {
  deserialize( s, static_cast<T*>(pdata) );
}
Run Code Online (Sandbox Code Playgroud)

如果我们有一个enum DataType,我们就写一个特征:

enum DataType {
  Integer,
  Real,
  VectorOfData,
  DataTypeCount, // last
};

template<DataType> struct enum_to_type {};

template<DataType::Integer> struct enum_to_type:tag_t<int> {};
// etc

void serialize( serialize_target t, Any const& any ) {
  magic_switch<DataType::DataTypeCount>(
    any.type_index,
    [&](auto type_index) {
      serialize( t, any.pdata, enum_to_type<type_index>{} );
    }
  };
}
Run Code Online (Sandbox Code Playgroud)

现在,所有繁重的工作都由enum_to_type特质类专业化,DataType枚举和以下形式的重载完成:

void serialize( serialize_target t, int const* pdata );
Run Code Online (Sandbox Code Playgroud)

这是类型安全的。

请注意,您any实际上不是any,而是variant。它包含类型的有界列表,什么也没有。

magic_switch最终被用来重新实现std::visit的功能,这也为您提供了类型安全的存储内的访问类型variant

如果您希望它包含任何内容,则必须确定要支持的操作,为其编写类型擦除代码,然后将其存储在中any,然后将类型擦除的操作与数据一起存储,而bob是您的叔叔。


Hor*_*ing 0

这是介于#3 和#4 之间的解决方案。或许能给一些启发,不知道是否真的有用。

您可以将“编解码器”代码放入一些不相关的特征结构中,而不是使用接口基类和虚拟调度:

struct AnyFooCodec
{
    static size_t serialize(const Any&, char*)
    {
        // ...
    }

    static Any deserialize(const char*, size_t)
    {
        // ...
    }

    static bool is_trivially_serializable()
    {
        // ...
    }
};

struct AnyBarCodec
{
    static size_t serialize(const Any&, char*)
    {
        // ...
    }

    static Any deserialize(const char*, size_t)
    {
        // ...
    }

    static bool is_trivially_serializable()
    {
        // ...
    }
};
Run Code Online (Sandbox Code Playgroud)

然后,您可以将这些特征类型放入类型列表中,这里我只使用 astd::tuple来实现:

typedef std::tuple<AnyFooCodec, AnyBarCodec> DispatchTable;
Run Code Online (Sandbox Code Playgroud)

现在我们可以编写一个通用调度函数,将第 n 个类型特征传递给给定的函子:

template <size_t N>
struct DispatchHelper
{
    template <class F, class... Args>
    static auto dispatch(size_t type, F f, Args&&... args)
    {
        if (N == type)
            return f(typename std::tuple_element<N, DispatchTable>::type(), std::forward<Args>(args)...);
        return DispatchHelper<N + 1>::dispatch(type, f, std::forward<Args>(args)...);
    }
};

template <>
struct DispatchHelper<std::tuple_size<DispatchTable>::value>
{
    template <class F, class... Args>
    static auto dispatch(size_t type, F f, Args&&... args)
    {
        // TODO: error handling (type index out of bounds)
        return decltype(DispatchHelper<0>::dispatch(type, f, args...)){};
    }
};

template <class F, class... Args>
auto dispatch(size_t type, F f, Args&&... args)
{
    return DispatchHelper<0>::dispatch(type, f, std::forward<Args>(args)...);
}
Run Code Online (Sandbox Code Playgroud)

这使用线性搜索来找到正确的特征,但通过一些努力,至少可以使其成为二分搜索。此外,编译器应该能够内联所有代码,因为不涉及虚拟调度。也许编译器甚至足够聪明,基本上可以将其变成一个开关。

实例:http ://coliru.stacked-crooked.com/a/1c597883896006c4