14

目标:

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

问题定义:

鉴于以下情况:

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

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

当然我不需要拘泥于语法v.f(arg_1, ..., arg_m):例如,类似的东西call(v, f, ...)也是可以接受的。

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

约束:

一个好的解决方案是任何让我模仿v.f(...)成语的东西,例如call_on_variant(v, f, ...);,并满足以下约束

  1. 不需要为必须以这种方式调用的每个函数(例如)或任何可以在代码中的其他地方以多态方式处理的不相关类型列表(例如)进行任何类型的单独声明,尤其是在全局范围内;fENABLE_CALL_ON_VARIANT(f)A1, ..., AnENABLE_VARIANT_CALL(A1, ..., An)
  2. 调用时不需要显式命名输入参数的类型(例如call_on_variant<int, double, string>(v, f, ...))。命名返回类型是可以的,所以例如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); \
}();

如果本地类中只允许模板成员(请参阅此问题),这将完美地工作。有没有人知道如何解决这个问题,或者提出一种替代方法?

4

4 回答 4

7

一段时间过去了,C++14 正在完成,编译器正在增加对新特性的支持,比如通用 lambda。

泛型 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

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

于 2013-12-10T20:55:39.020 回答
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 的其他地方完成了。

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

于 2013-01-11T23:59:44.897 回答
4

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

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

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

这样做的原因是,无论如何完成,都需要遍历变体可以包含的所有类型(类型列表公开为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++不允许使用允许编译器基于编译时 for loop生成一系列此类片段的static for惯用语。相反,必须使用模板元编程技术,例如:

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

whereF是一个带有模板调用运算符的函子。直接或间接地,至少需要定义一个类或函数模板,因为缺乏static for强制程序员编写必须为每种类型通用重复的例程。

考虑 2:[根据地方需要]

所需解决方案的限制之一(问题文本中“ CONSTRAINTS ”部分的要求 1)是不需要在函数调用所在的范围之外的任何其他范围内添加全局声明或任何其他声明完毕。因此,无论是宏扩展还是模板元编程,需要做的事情都必须在函数调用发生的地方做

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

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

因此,为了完成这项工作,我们必须定义的通用例程必须在调用站点以外的其他地方定义,并且必须在调用站点使用适当的参数进行实例化。

考虑 3:[关于函数名称]

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

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

结论:

综合以上三个考虑,证明这个问题在今天的 C++ 中是无法解决的。它需要使用名称作为模板参数的可能性或定义本地模板的可能性。虽然第一件事至少是不可取的,但第二件事可能是有道理的,但标准化委员会没有考虑到这一点。但是,可能会承认一个例外

未来的机会:

为进入下一个 C++ 标准而大力推动的通用 lambdas实际上是具有模板调用运算符的本地类

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

// 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 的这篇论文似乎表明,无关类型的动态多态性对于实现编程中重要的(在我看来是基本的和不可避免的)范式转变至关重要。

于 2013-01-12T11:06:38.490 回答
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 事件。

于 2013-01-12T02:44:40.830 回答