【问题标题】:How to achieve dynamic polymorphism (run-time call dispatch) on unrelated types?如何在不相关的类型上实现动态多态性(运行时调用调度)?
【发布时间】:2012-12-26 14:51:03
【问题描述】:

目标:

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

问题定义:

鉴于以下情况:

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

我的目标是编写概念上等同于 v.f(arg_1, ..., arg_m); 的指令,如果 实际类型包含在v 中是Ai。如果调用参数与 each 函数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))或任何可以处理的不相关类型列表A1, ..., An,不需要任何类型的单独声明多态(例如ENABLE_VARIANT_CALL(A1, ..., An))在代码的其他地方,尤其是在全局范围内;
  2. 在调用时不需要明确命名输入参数的类型(例如call_on_variant<int, double, string>(v, f, ...))。命名 return 类型是可以的,例如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");
}

这个程序的输出应该是:ABC

最佳(失败)尝试:

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

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

如果在本地类中只允许模板成员,那将非常有效(请参阅this question)。有没有人知道如何解决这个问题,或建议一种替代方法?

【问题讨论】:

  • 听起来像是类型擦除的工作。这可以接受吗?
  • @KerrekSB:确实,我几乎尝试了我所知道的一切,但到目前为止没有成功
  • 不知怎的,在我看来这是不可能的,但我很乐意看到其他情况。
  • 嗯,C++ 是静态类型的,所以你不能真正决定参数的数量和类型动态...
  • @SethCarnegie:不不,继续。我现在不太确定。我可以为固定的函数签名做到这一点,但我认为这不是我们想要的。

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


【解决方案1】:

一段时间过去了,C++14 即将完成,编译器正在添加对新功能的支持,例如通用 lambda。

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

#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

该程序已在 VC12 上使用 2013 年 11 月的 CTP 进行了测试。不幸的是,我不知道任何支持通用 lambda 的在线编译器,所以我无法发布一个实时示例。

【讨论】:

    【解决方案2】:

    不幸的是,这不能在 C++ 中完成(但 - 请参阅结论)。遵循一个证明。

    注意事项 1:[关于模板的需要]

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

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

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

    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 允许编译器基于编译时for 循环 生成这样的sn-ps 序列。相反,必须使用模板元编程技术,例如:

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

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

    注意事项 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++ 不支持。 This paper 由 Mat Marcus、Jaakko Jarvi 和 Sean Parent 撰写,似乎表明无关类型的动态多态性对于实现编程中重要的(在我看来是基本且不可避免的)范式转变至关重要。

    【讨论】:

    • +1 说得好,这很好地总结了问题,恐怕你是对的,问题不能用你做的约束来解决。
    • @SethCarnegie:谢谢,确实,看来我要求太多了。希望我们很快就会有多态 lambdas
    【解决方案3】:

    我曾经通过模拟 .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 事件。

    【讨论】:

    • 感谢您分享您的代码,这非常有趣:) 然而,这是std::functionstd::bind 已经支持的常规类型擦除。我想要实现的是不同的东西(可悲的是,截至今天是不可能的)。查看我的答案以获得解释,也许尝试重新阅读该问题以更好地理解我在寻找什么。但再次感谢您分享您的想法和代码,我很感激
    【解决方案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);
    

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

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

    【讨论】:

    • 这与我一整天都在努力的工作非常相似 :) 我有元组解包的代码,或者至少我有 - 恐怕我删除了所有内容。但我想看一个自包含的例子
    • 我曾经写过一个带有元组的printf,据我记得我并不热衷于重温它。我很确定这里有人以前做过。
    • @AndyProwl:它必须被编码到元组解包代码中。 Here is one versionhere is another。啊,我明白了,函数的名称不能模板化,因为那是语言之外的。
    • 其实我觉得有一个问题隐藏在某个地方:call_on_variant 也应该接受一个函数名,并且将那个传递给访问者是困难的部分。这是因为访问者只有在调用调用运算符时才知道要调用 哪个 成员函数(属于哪个类),这意味着您应该只传递一个 name,而您只能用宏来做到这一点
    • @AndyProwl:是的,你是对的。您不能将函数 name 设为 C++ 实体:由于类型不相关,因此函数具有相同名称是词汇上的巧合,但您不能在语言中利用它。
    猜你喜欢
    • 1970-01-01
    • 2018-04-12
    • 2015-04-21
    • 2019-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多