【发布时间】:2016-10-07 11:20:57
【问题描述】:
我正在研究现代 C++ (C++11/C++14) 中不相关类型的动态调度的可能实现。
“动态分派类型”是指在运行时我们需要通过其整数索引从列表中选择一个类型并对其进行处理(调用静态方法、使用类型特征等)的情况。
以序列化数据流为例:有多种数据值,它们的序列化/反序列化方式不同;有几个编解码器,可以进行序列化/反序列化;我们的代码从流中读取类型标记,然后决定它应该调用哪个编解码器来读取完整值。
我感兴趣的情况是有许多操作可以在类型上调用(几个静态方法、类型特征......),并且从逻辑类型到 C++ 类的映射可能不同,而不仅仅是 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; ... } }
优点:简单易懂;编译器可以内联调度的方法。
缺点:需要大量手动重复(或通过外部工具生成代码)。
-
像这样创建调度表
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(); }
优点:它更灵活一些 - 需要为每种调度类型编写一个调度程序类,然后可以将它们组合到不同的调度表中。
缺点:需要编写大量调度代码。由于虚拟调度和不可能将编解码器的方法内联到调用者的站点,会产生一些开销。
-
使用模板调度函数
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; } ); }
优点:它只需要一个 switch-case 调度函数和每个操作调用中的少量代码(至少手动编写)。并且编译器可能会内联它认为合适的内容。
缺点:它更复杂,需要 C++14(如此干净和紧凑)并依赖编译器能力来优化未使用的编解码器实例(仅用于为编解码器)。
-
当一组逻辑类型可能有多个映射到实现类(本例中为编解码器)时,最好推广解决方案#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)...); ... } }
我倾向于解决方案#3(或#4)。但我确实想知道 - 是否可以避免手动编写 dispatch 函数?我的意思是它的开关盒。这个 switch-case 完全是从类型值和类型之间的编译时映射派生的——有什么方法可以处理它到编译器的生成吗?
【问题讨论】:
标签: c++ c++11 polymorphism runtime c++14