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

后端 未结 4 439
说谎
说谎 2021-02-01 23:50

GOAL:

I would like to achieve type-safe dynamic polymorphism (i.e. run-time dispatch of a function call) on unrelated types

相关标签:
4条回答
  • 2021-02-02 00:10

    I once solved this by simulating .NET delegates:

    template<typename T>
    class Delegate
    {
        //static_assert(false, "T must be a function type");
    };
    
    template<typename ReturnType>
    class Delegate<ReturnType()>
    {
    private:
        class HelperBase
        {
        public:
            HelperBase()
            {
            }
    
            virtual ~HelperBase()
            {
            }
    
            virtual ReturnType operator()() const = 0;
            virtual bool operator==(const HelperBase& hb) const = 0;
            virtual HelperBase* Clone() const = 0;
        };
    
        template<typename Class>
        class Helper : public HelperBase
        {
        private:
            Class* m_pObject;
            ReturnType(Class::*m_pMethod)();
    
        public:
            Helper(Class* pObject, ReturnType(Class::*pMethod)()) : m_pObject(pObject), m_pMethod(pMethod)
            {
            }
    
            virtual ~Helper()
            {
            }
    
            virtual ReturnType operator()() const
            {
                return (m_pObject->*m_pMethod)();
            }
    
            virtual bool operator==(const HelperBase& hb) const
            {
                const Helper& h = static_cast<const Helper&>(hb);
                return m_pObject == h.m_pObject && m_pMethod == h.m_pMethod;
            }
    
            virtual HelperBase* Clone() const
            {
                return new Helper(*this);
            }
        };
    
        HelperBase* m_pHelperBase;
    
    public:
        template<typename Class>
        Delegate(Class* pObject, ReturnType(Class::*pMethod)())
        {
            m_pHelperBase = new Helper<Class>(pObject, pMethod);
        }
    
        Delegate(const Delegate& d)
        {
            m_pHelperBase = d.m_pHelperBase->Clone();
        }
    
        Delegate(Delegate&& d)
        {
            m_pHelperBase = d.m_pHelperBase;
            d.m_pHelperBase = nullptr;
        }
    
        ~Delegate()
        {
            delete m_pHelperBase;
        }
    
        Delegate& operator=(const Delegate& d)
        {
            if (this != &d)
            {
                delete m_pHelperBase;
                m_pHelperBase = d.m_pHelperBase->Clone();
            }
    
            return *this;
        }
    
        Delegate& operator=(Delegate&& d)
        {
            if (this != &d)
            {
                delete m_pHelperBase;
                m_pHelperBase = d.m_pHelperBase;
                d.m_pHelperBase = nullptr;
            }
    
            return *this;
        }
    
        ReturnType operator()() const
        {
            (*m_pHelperBase)();
        }
    
        bool operator==(const Delegate& d) const
        {
            return *m_pHelperBase == *d.m_pHelperBase;
        }
    
        bool operator!=(const Delegate& d) const
        {
            return !(*this == d);
        }
    };
    

    You can use it much like .NET delegates:

    class A
    {
    public:
        void M() { ... }
    };
    
    class B
    {
    public:
        void M() { ... }
    };
    
    A a;
    B b;
    
    Delegate<void()> d = Delegate<void()>(&a, &A::M);
    d(); // calls A::M
    
    d = Delegate<void()>(&b, &B::M);
    d(); // calls B::M
    

    This works with methods that have no arguments. If you can use C++11, you can modify it to use variadic templates to handle any number of parameters. Without C++11, you need to add more Delegate specializations to handle specific numbers of parameters:

    template<typename ReturnType, typename Arg1>
    class Delegate<ReturnType(Arg1)>
    {
        ...
    };
    
    template<typename ReturnType, typename Arg1, typename Arg2>
    class Delegate<ReturnType(Arg1, Arg2)>
    {
        ...
    };
    

    With this Delegate class you can also emulate .NET events, which are based on delegates.

    0 讨论(0)
  • 2021-02-02 00:14

    OK, here's a wild shot:

    template <typename R, typename ...Args>
    struct visitor : boost::static_visitor<R>
    {
        template <typename T>
        R operator()(T & x)
        { 
            return tuple_unpack(x, t);   // this needs a bit of code
        }
    
        visitor(Args const &... args) : t(args...) { }
    
    private:
        std::tuple<Args...> t;
    };
    
    template <typename R, typename Var, typename ...Args>
    R call_on_variant(Var & var, Args const &... args)
    {
        return boost::apply_visitor(visitor<R, Args...>(args...), var);
    }
    

    Usage:

    R result = call_on_variant<R>(my_var, 12, "Hello", true);
    

    I've hidden a certain amount of work you need for calling a function by unpacking a tuple, but I believe this has been done elsewhere on SO.

    Also, if you need to store references rather than copies of the arguments, this can possibly be done, but needs more care. (You can have a tuple of references. But you have to think about whether you also want to allow temporary objects.)

    0 讨论(0)
  • 2021-02-02 00:16

    Unfortunately, this cannot be done in C++ (yet - see the conclusions). Follows a proof.

    CONSIDERATION 1: [on the need of templates]

    In order to determine the correct member function Ai::f to be invoked at run-time when the expression call_on_variant(v, f, ...) is met (or any equivalent form of it), it is necessary, given the variant object v, to retrieve the type Ai of the value being held by v. Doing so necessarily requires the definition of at least one (class or function) template.

    The reason for this is that no matter how this is done, what is needed is to iterate over all the types the variant can hold (the type list is exposed as boost::variant<...>::types, check whether the variant is holding a value of that type (through boost::get<>), and (if so) retrieve that value as the pointer through which the member function invocation must be performed (internally, this is also what boost::apply_visitor<> does).

    For each single type in the list, this can be done this way:

    using types = boost::variant<A1*, ..., An*>::types;
    mpl::at_c<types, I>::type* ppObj = (get<mpl::at_c<types, I>::type>(&var));
    if (ppObj != NULL)
    {
        (*ppObj)->f(...);
    }
    

    Where I is a compile-time constant. Unfortunately, C++ does not allow for a static for idiom that would allow a sequence of such snippets to be generated by the compiler based on a compile-time for loop. Instead, template meta-programming techniques must be used, such as:

    mpl::for_each<types>(F());
    

    where F is a functor with a template call operator. Directly or indirectly, at least one class or function template needs to be defined, since the lack of static for forces the programmer to code the routine that must be repeated for each type generically.

    CONSIDERATION 2: [on the need of locality]

    One of the constraints for the desired solution (requirement 1 of the section "CONSTRAINTS" in the question's text) is that it shall not be necessary to add global declarations or any other declaration at any other scope than the one where the function call is being done. Therefore, no matter whether macro expansion or template meta-programming is involved, what needs to be done must be done in the place where the function call occurs.

    This is problematic, because "CONSIDERATION 1" above has proved that it is needed to define at least one template to carry out the task. The problem is that C++ does not allow templates to be defined at local scope. This is true of class templates and function templates, and there is no way to overcome this restriction. Per §14/2:

    "A template-declaration can appear only as a namespace scope or class scope declaration"

    Thus, the generic routines we have to define in order to do the job must be defined elsewhere than at call site, and must be instantiated at call-site with proper arguments.

    CONSIDERATION 3: [on function names]

    Since the call_on_variant() macro (or any equivalent construct) must be able to handle any possible function f, the name of f must be passed in as an argument to our template-based, type resolving machinery. It is important to stress that only the name of the function shall be passed, because the particular function Ai::f that needs to be invoked must be determined by the template machinery.

    However, names cannot be template arguments, because they do not belong to the type system.

    CONCLUSION:

    The combination of the three considerations above proves that this problem cannot be solved in C++ as of today. It requires either the possibility of using names as template arguments or the possibility of defining local templates. While the first thing is undesirable at least, the second one might make sense, but it is not being taken into consideration by the standardization committee. However, one exception is likely to be admitted.

    FUTURE OPPORTUNITIES:

    Generic lambdas, which are being strongly pushed to get into the next C++ standard, are in fact local classes with a template call operator.

    Thus, even though the macro I posted at the end of the question's text will still not work, an alternative approach seems viable (with some tweaking required for handling return types):

    // Helper template for type resolution
    template<typename F, typename V>
    struct extractor
    {
        extractor(F f, V& v) : _f(f), _v(v) { }
    
        template<typename T>
        void operator () (T pObj)
        {
            T* ppObj = get<T>(&_v));
            if (ppObj != NULL)
            {
                _f(*ppObj);
                return;
            }
        }
    
        F _f;
        V& _v;
    };
    
    // v is an object of type boost::variant<A1*, ..., An*>;
    // f is the name of the function to be invoked;
    // The remaining arguments are the call arguments.
    #define call_on_variant(v, f, ...) \
        using types = decltype(v)::types; \
        auto lam = [&] (auto pObj) \
        { \
            (*pObj)->f(__VA_ARGS__); \
        }; \
        extractor<decltype(lam), decltype(v)>(); \
        mpl::for_each<types>(ex);
    

    FINAL REMARKS:

    This is an interesting case of type-safe call that is (sadly) not supported by C++. This paper by Mat Marcus, Jaakko Jarvi, and Sean Parent seems to show that dynamic polymorphism on unrelated types is crucial to achieve an important (in my opinion, fundamental and unavoidable) paradigm shift in programming.

    0 讨论(0)
  • 2021-02-02 00:22

    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.

    0 讨论(0)
提交回复
热议问题