如何在不相关的类型上实现动态多态(运行时调用调度)?

时间:2013-01-11 23:01:22

标签: c++ c++11 runtime polymorphism boost-variant

目标:

我想实现类型安全的动态多态(即函数调用的运行时调度)对不相关的类型 - 即做的类型没有共同的基类。在我看来,这是可以实现的,或者至少在理论上是合理的。我会尝试更正式地定义我的问题。

问题定义:

鉴于以下内容:

  • 两个或更多无关的类型A1, ..., An,每个类型都有一个名为f的方法,可能有不同的签名,但具有相同的返回类型 R;和
  • 一个boost::variant<A1*, ..., An*>对象v(或其他任何类型的变体)可以和必须在任何时候假设其中任何一种类型的值;

如果实际类型,我的目标是编写概念上等同于v.f(arg_1, ..., arg_m);的指令,这些指令可以在运行时发送到函数Ai::f v中包含的值Ai。如果调用参数与每个函数Ai的形式参数不兼容,编译器应该引发错误。

当然,我不需要坚持使用v.f(arg_1, ..., arg_m)语法:例如call(v, f, ...)之类的内容也可以接受。

我试图在C ++中实现这一点,但到目前为止,我没有想出一个好的解决方案(我确实有很多坏的解决方案)。下面我通过“好的解决方案”澄清我的意思。

约束:

好的解决方案是让我模仿v.f(...)成语的任何内容,例如: call_on_variant(v, f, ...);满足以下约束条件

  1. 对于必须以这种方式调用的每个函数f(例如ENABLE_CALL_ON_VARIANT(f))或任何不相关类型列表{{1},不需要任何类型的单独声明可以在代码中的其他地方以多态方式(例如A1, ..., An)处理,尤其是在全局范围内;
  2. 执行调用时,
  3. 不需要显式命名输入参数的类型(例如ENABLE_VARIANT_CALL(A1, ..., An))。命名 return 类型是正常的,因此例如call_on_variant<int, double, string>(v, f, ...)是可以接受的。
  4. 按照一个示范性的例子,希望澄清我的愿望和要求。

    示例:

    call_on_variant<void>(v, f, ...)

    此程序的输出应为: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"); }

    BEST(FAILED)ATTEMPT:

    我最接近所需解决方案的是这个宏:

    ABC

    哪个可以完美运行,如果只允许模板成员使用本地类(请参阅this question)。有没有人知道如何解决这个问题,或建议另一种方法?

4 个答案:

答案 0 :(得分:7)

一段时间过去了,C ++ 14正在最终确定,编译器正在添加对新功能的支持,例如通用lambdas。

通用lambda与下面显示的机制一起,允许用不相关的类实现所需的(动态)多态性:

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

让我们付诸实践。假设有以下两个不相关的类:

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

可以通过这种方式多态调用foo()

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

输出正如预期的那样:

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

该计划已于2013年11月的CTP上在VC12上进行了测试。不幸的是,我不知道任何支持通用lambdas的在线编译器,所以我不能发布一个实例。

答案 1 :(得分:4)

好的,这是一个疯狂的镜头:

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

用法:

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

我通过解包元组隐藏了一些调用函数所需的工作,但我相信这已经在其他地方完成了。

此外,如果您需要存储引用而不是参数的副本,则可以这样做,但需要更加小心。 (你可以有一个引用元组。但你必须考虑是否也想要允许临时对象。)

答案 2 :(得分:4)

不幸的是,这不能用C ++完成(但是 - 见结论)。遵循证明。

考虑1:[关于模板的需要]

为了确定在满足表达式Ai::f(或其任何等效形式)时在运行时调用的正确成员函数call_on_variant(v, f, ...),给定变量对象是必要的v,以检索Ai所持有的值的类型v。这样做必然需要定义至少一个(类或函数)模板

这样做的原因是无论如何这样做,所需要的是迭代变体可以容纳的所有类型(类型列表暴露如boost::variant<...>::types,检查变量是否持有该类型的值(通过boost::get<>),并且(如果是)将该值检索为指针,成员函数通过该指针必须执行调用(在内部,这也是boost::apply_visitor<>所做的。)

对于列表中的每个单一类型,可以这样做:

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

I编译时常量。不幸的是,C ++ 不允许允许static for idiom允许编译器根据编译时循环生成一系列此类代码段。相反,必须使用模板元编程技术,例如:

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

其中F是带有模板调用运算符的仿函数。直接或间接地,至少需要定义一个类或函数模板,因为缺少static for迫使程序员编写必须为每种类型重复的例程

考虑2:[关于地方的需要]

所需解决方案的约束之一(问题文本中“ CONSTRAINTS ”部分的要求1)是没有必要在任何其他地方添加全局声明或任何其他声明范围而不是函数调用的范围。因此,无论是涉及宏扩展还是模板元编程,都需要做什么必须在函数调用发生的地方进行

这是有问题的,因为上面的“ CONSIDERATION 1 ”证明需要定义至少一个模板来执行任务。问题是C ++ 不允许在本地范围定义模板。类模板和函数模板也是如此,并且无法克服此限制。根据§14/ 2:

“模板声明只能作为命名空间范围或类范围声明出现”

因此,我们必须定义以执行作业的通用例程必须在调用站点之外的其他地方定义,并且必须实例化 em>在具有适当参数的呼叫站点。

考虑3:[关于功能名称]

由于call_on_variant()宏(或任何等效构造)必须能够处理任何可能的函数f,因此f名称必须传入作为我们基于模板的类型解析机器的参数。重要的是要强调只传递函数的名称,因为需要调用的特定函数 Ai::f必须由模板确定机械。

但是, names 不能是模板参数,因为它们不属于类型系统。

<强>结论:

上述三个考虑因素的组合证明,到目前为止,这个问题在C ++中无法解决。它需要使用名称作为模板参数的可能性或者定义本地模板的可能性。虽然第一件事至少是不可取的,但第二件事可能有意义,但标准化委员会并没有考虑到这一点。但是,一个例外可能会被录取

未来的机会:

Generic lambdas ,强烈推动进入下一个C ++标准,实际上是本地类,带有模板调用运算符

因此,即使我在问题文本末尾发布的宏仍然不起作用,另一种方法似乎也可行(处理返回类型需要进行一些调整):

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

最终评论:

这是类型安全调用的一个有趣案例,(遗憾的是)C ++不支持。 Mat Marcus,Jaakko Jarvi和Sean Parent的This paper似乎表明,不相关类型的动态多态性对于在编程中实现一个重要的(在我看来,基本的和不可避免的)范式转换至关重要

答案 3 :(得分:0)

我曾经通过模拟.NET代理解决了这个问题:

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

您可以像.NET代理一样使用它:

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

这适用于没有参数的方法。如果您可以使用C ++ 11,则可以对其进行修改以使用可变参数模板来处理任意数量的参数。如果没有C ++ 11,则需要添加更多Delegate特化以处理特定数量的参数:

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

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

使用此Delegate类,您还可以模拟基于委托的.NET事件。