I'm investigating possible implementations of dynamic dispatch of unrelated types in modern C++ (C++11/C++14).
By "dynamic dispatch of types" I mean a case when in runtime we need to choose a type from list by its integral index and do something with it (call a static method, use a type trait and so on).
For example, consider stream of serialized data: there are several kinds of data values, which are serialized/deserialized differently; there are several codecs, which do serialization/deserialization; and our code read type marker from stream and then decide which codec it should invoke to read full value.
I'm interested in a case where are many operations, which could be invoked on types (several static methods, type traits...), and where could be different mapping from logical types to C++ classes and not only 1:1 (in example with serialization it means that there could be several data kinds all serialized by the same codec).
I also wish to avoid manual code repetition and to make the code more easily maintainable and less error-prone. Performance also is very important.
Currently I'm seeing those possible implementations, am I missing something? Can this be done better?
Manually write as many functions with switch-case as there are possible operations invocations on types.
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; ... } }
Pros: it's simple and understandable; compiler could inline dispatched methods.
Cons: it requires a lot of manual repetition (or code generation by external tool).
Create dispatching table like this
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(); }
Pros: it's a little more flexible - one needs to write a dispatcher class for each dispatched type, but then one could combine them in different dispatch tables.
Cons: it requires writing a lot of dispatching code. And there is some overhead due to virtual dispatching and impossibility to inline codec's methods into caller's site.
Use templated dispatching function
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; } ); }
Pros: it requires just one switch-case dispatching function and a little of code in each operation invocation (at least manually written). And compiler may inline what it finds apropriate.
Cons: it's more complicated, requires C++14 (to be such clean and compact) and relies on compiler ability to optimize away unused codec instance (which is used only to choose right overload for codec).
When for one set of logical types there may be several mapping to implementation classes (codecs in this example), it may be better to generalize solution #3 and write completely generic dispatch function, which receive compile-time mapping between type values and invoked types. Something like this:
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)...); ... } }
I'm leaning on solution #3 (or #4). But I do wonder - is it possible to avoid manually writing of dispatch
function? Its switch-case I mean. This switch-case is completely derived from compile-time mapping between type values and types - is there any method to handle its generation to compiler?