Search code examples
c++c++11runtimepolymorphismboost-variant

How to achieve dynamic polymorphism (run-time call dispatch) on unrelated types?


GOAL:

I would like to achieve type-safe dynamic polymorphism (i.e. run-time dispatch of a function call) on unrelated types - i.e. on types which do not have a common base class. It seems to me that this is achievable, or at least theoretically sound. I will try to define my problem more formally.

PROBLEM DEFINITION:

Given the following:

  • two or more unrelated types A1, ..., An, each of which has a method called f, possibly with different signatures, but with the same return type R; and
  • a boost::variant<A1*, ..., An*> object v (or whatever other type of variant) which can and must assume at any time one value of any of those types;

My goal is to write instructions conceptually equivalent to v.f(arg_1, ..., arg_m); that would get dispatched at run-time to function Ai::f if the actual type of the value contained in v is Ai. If the call arguments are not compatible with the formal parameters of each function Ai, the compiler should raise an error.

Of course I do not need to stick to the syntax v.f(arg_1, ..., arg_m): for instance, something like call(v, f, ...) is also acceptable.

I tried to achieve this in C++, but so far I have failed to come up with a good solution (I do have a bunch of bad ones). Below I clarify what I mean by "good solution".

CONSTRAINTS:

A good solution is anything that lets me mimic the v.f(...) idiom, e.g. call_on_variant(v, f, ...);, and satisfies the following constraints:

  1. does not require any sort of separate declaration for each function f that must be called this way (e.g. ENABLE_CALL_ON_VARIANT(f)) or for any list of unrelated types A1, ..., An that can be treated polymorphically (e.g. ENABLE_VARIANT_CALL(A1, ..., An)) somewhere else in the code, especially on global scope;
  2. does not require to explicitly name the types of the input arguments when doing the call (e.g. call_on_variant<int, double, string>(v, f, ...)). Naming the return type is OK, so for instance call_on_variant<void>(v, f, ...) is acceptable.

Follows a demonstrative example that hopefully clarifies my wish and requirements.

EXAMPLE:

struct A1 { void f(int, double, string) { cout << "A"; } };
struct A2 { void f(int, double, string) { cout << "B"; } };
struct A3 { void f(int, double, string) { cout << "C"; } };

using V = boost::variant<A1, A2, A3>;

// Do not want anything like the following here:
// ENABLE_VARIANT_CALL(foo, <whatever>)

int main()
{
    A a;
    B b;
    C c;

    V v = &a;
    call_on_variant(v, f, 42, 3.14, "hello");

    // Do not want anything like the following here:
    // call_on_variant<int, double, string>(v, f, 42, 3.14, "hello");

    V v = &b;
    call_on_variant(v, f, 42, 3.14, "hello");

    V v = &c;
    call_on_variant(v, f, 42, 3.14, "hello");
}

The output of this program should be: ABC.

BEST (FAILED) ATTEMPT:

The closest I got to the desired solution is this macro:

#define call_on_variant(R, v, f, ...) \
[&] () -> R { \
    struct caller : public boost::static_visitor<void> \
    { \
        template<typename T> \
        R operator () (T* pObj) \
        { \
            pObj->f(__VA_ARGS__); \
        } \
    }; \
    caller c; \
    return v.apply_visitor(c); \
}();

Which would work perfectly, if only template members were allowed in local classes (see this question). Does anybody have an idea how to fix this, or suggest an alternative approach?


Solution

  • Some time has passed, C++14 is being finalized, and compilers are adding support for new features, like generic lambdas.

    Generic lambdas, together with the machinery shown below, allow achieving the desired (dynamic) polymorphism with unrelated classes:

    #include <boost/variant.hpp>
    
    template<typename R, typename F>
    class delegating_visitor : public boost::static_visitor<R>
    {
    public:
        delegating_visitor(F&& f) : _f(std::forward<F>(f)) { }
        template<typename T>
        R operator () (T x) { return _f(x); }
    private:
        F _f;
    };
    
    template<typename R, typename F>
    auto make_visitor(F&& f)
    {
        using visitor_type = delegating_visitor<R, std::remove_reference_t<F>>;
        return visitor_type(std::forward<F>(f));
    }
    
    template<typename R, typename V, typename F>
    auto vcall(V&& vt, F&& f)
    {
        auto v = make_visitor<R>(std::forward<F>(f));
        return vt.apply_visitor(v);
    }
    
    #define call_on_variant(val, fxn_expr) \
        vcall<int>(val, [] (auto x) { return x-> fxn_expr; });
    

    Let's put this into practice. Supposing to have the following two unrelated classes:

    #include <iostream>
    #include <string>
    
    struct A
    {
        int foo(int i, double d, std::string s) const
        { 
            std::cout << "A::foo(" << i << ", " << d << ", " << s << ")"; 
            return 1; 
        }
    };
    
    struct B
    {
        int foo(int i, double d, std::string s) const
        { 
            std::cout << "B::foo(" << i << ", " << d << ", " << s << ")"; 
            return 2;
        }
    };
    

    It is possible to invoke foo() polymorphically this way:

    int main()
    {
        A a;
        B b;
    
        boost::variant<A*, B*> v = &a;
        auto res1 = call_on_variant(v, foo(42, 3.14, "Hello"));
        std::cout << std::endl<< res1 << std::endl;
    
        v = &b;
        auto res2 = call_on_variant(v, foo(1337, 6.28, "World"));
        std::cout << std::endl<< res2 << std::endl;
    }
    

    And the output is, as expected:

    A::foo(42, 3.14, Hello)
    1
    B::foo(1337, 6.28, World)
    2
    

    The program has been tested on VC12 with November 2013's CTP. Unfortunately, I do not know of any online compiler that supports generic lambdas, so I cannot post a live example.