12

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?

  1. 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).

  1. 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.

  1. 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).

  1. 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?

Ami Tavory
  • 74,578
  • 11
  • 141
  • 185

2 Answers2

7

Tag dispatching, where you pass a type to pick an overload, is efficient. std libraries typically use it for algorithms on iterators, so different iterator categories get different implementations.

When I have a list of type ids, I ensure they are contiguous and write a jump table.

This is an array of pointers to functions that do the task at hand.

You can automate writing this in C++11 or better; I call it the magic switch, as it acts like a runtime switch, and it calls a function with a compile time value based off the runtime one. I make the functions with lambdas, and expand a parameter pack inside them so their bodies differ. They then dispatch to the passed-in function object.

Write that, then you can move your serialization/deserialization code into "type safe" code. Use traits to map from compile-time indexes to type tags, and/or dispatch based on the index to an overloaded function.

Here is a C++14 magic switch:

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

use looks like:

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";

live example.

If you can register your type enum to type map at compile time (via type traits or whatever), you can round trip through a magic switch to turn your runtime enum value into a compile time type tag.

template<class T> struct tag_t {using type=T;};

then you can write your serialize/deserialize like this:

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

If we have an enum DataType, we write a traits:

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

all the heavy lifting is now done by enum_to_type traits class specializations, the DataType enum, and overloads of the form:

void serialize( serialize_target t, int const* pdata );

which are type safe.

Note that your any is not actually an any, but rather a variant. It contains a bounded list of types, not anything.

This magic_switch ends up being used to reimplement std::visit function, which also gives you type-safe access to the type stored within the variant.

If you want it to contain anything, you have to determine what operations you want to support, write type-erasure code for it that runs when you store it in the any, store the type-erased operations along side the data, and bob is your uncle.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Nice solution! Although, in your example it's not actually dispatching to types, rather to functions overloads. But it should be easy to write an additional helper which translates compile time integral constant to type from a type list. – Alexander Morozov Oct 10 '16 at 08:51
  • There is a somewhat bigger problem: with function pointers table, just like with my solution #2, compiler is unable to inline operations on types. For static methods it's not too bad, but for type traits it's a downside. Consider `is_trivially_serializable` from my example - each operation on type is just an access to a constant, known at compile time, so it's preferable that compiler was able to unroll solution to something equivalent to a switch. – Alexander Morozov Oct 10 '16 at 08:56
  • @Yakk - Adam Nevraumont Thanks for this elegant solution. I tried with my gcc v7.3 and it fails, see `https://godbolt.org/z/XebTI5`, any advice? – Nayfe Nov 04 '18 at 23:33
  • @nayte upgrade to gcc 8? Painfully convert the lambda into an function object and use it? `[](PF pf)->R { return (*pf)( index{} ); }...` can be rewritten as returns to template functions returning a function pointer. – Yakk - Adam Nevraumont Nov 04 '18 at 23:40
  • @Yakk-AdamNevraumont I'm using Yocto in an embedded system and gcc8 is not yet supported. Still gcc7.3 should be enough for C++14, am I wrong? I'll try to found an older magic_switch as I'm cleary not good enough to modify your work. Thanks again! – Nayfe Nov 05 '18 at 08:32
  • @Yakk-AdamNevraumont it seems it's related to this gcc [issue](https://stackoverflow.com/questions/40752568/expanding-parameter-pack-into-lambda-with-fold-expression-gcc-vs-clang) – Nayfe Nov 05 '18 at 09:50
  • `template fn(PF pf) { return pf( std::integral_constant{} ); }` then `static const table_entry table[]={ fn... };` should do it. – Yakk - Adam Nevraumont Nov 05 '18 at 12:08
  • @Yakk-AdamNevraumont thank you so much for hint, I managed to run this way `https://godbolt.org/z/Yf9F59` – Nayfe Nov 05 '18 at 14:54
0

Here is a solution somewhere in between your #3 and #4. Maybe it gives some inspiration, not sure if it's really useful.

Instead of using a interface base class and virtual dispatch, you can just put your "codec" code into some unrelated trait structures:

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()
    {
        // ...
    }
};

You can then put these trait types into a type list, here I just use a std::tuple for that:

typedef std::tuple<AnyFooCodec, AnyBarCodec> DispatchTable;

Now we can write a generic dispatch function that passes the n'th type trait to a given functor:

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

This uses a linear search to find the proper trait, but with some effort one could at least make it a binary search. Also the compiler should be able to inline all the code as there is no virtual dispatch involved. Maybe the compiler is even smart enough to basically turn it into a switch.

Live example: http://coliru.stacked-crooked.com/a/1c597883896006c4

Horstling
  • 2,131
  • 12
  • 14
  • Nice! If we change `tuple` to a type list, then this solution could dispatch to types without any intermediate classes. The real question is: can we hope that a compiler will optimize this from N function calls to just one with a series of `ifs` or a `switch`? – Alexander Morozov Oct 10 '16 at 09:02
  • I sure would expect any decent compiler to at least inline the code, but as always with such questions you would have to check the assembly output that your compiler generates for your code. – Horstling Oct 10 '16 at 19:31
  • I wrote a test: https://gist.github.com/geluspeculum/8c644d35087c66494fcb86b3e3d82668 and Clang 3.8 emit equivalent assembler code for template recursion and for manually written switch. But there is a problem - with template recursion compiler limits itself to 256 levels of recursion and emit error for larger cases. So, this technique is limited to 256 types in type list. – Alexander Morozov Oct 11 '16 at 10:52
  • I'm pretty sure one could write a non-recursive function that passes the n'th type of a type list to a generic visitor, but this goes beyond the scope of this question. – Horstling Oct 11 '16 at 18:10
  • Looks like Clang 4.0 doesn't have a recursion depth limitation and generate "logarithmic" search (a tree of small switchs). And if use a throw-away `std::initializer_list` for iterating through type list, Clang 4.0 generate exactly the same code as for manual switch. GCC, on the other hand, generate for both cases a sequence of `if`s, unfortunately. – Alexander Morozov Aug 25 '17 at 05:18