如何在不相关的类型上实现动态多态(运行时调用调度)?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何在不相关的类型上实现动态多态(运行时调用调度)?相关的知识,希望对你有一定的参考价值。
目标:
我想在不相关的类型上实现类型安全的动态多态(即函数调用的运行时调度) - 即在没有公共基类的类型上。在我看来,这是可以实现的,或者至少在理论上是合理的。我会尝试更正式地定义我的问题。
问题定义:
鉴于以下内容:
- 两个或多个不相关的类型
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, ...);
,并满足以下约束:
- 对于必须以这种方式调用的每个函数
f
(例如ENABLE_CALL_ON_VARIANT(f)
)或任何可以在代码中的其他位置以多态方式处理的无关类型A1, ..., An
列表,特别是在全局范围内,不需要任何类型的单独声明; - 在进行调用时(例如
ENABLE_VARIANT_CALL(A1, ..., An)
),不需要显式命名输入参数的类型。命名返回类型是可以的,所以例如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);
}();
),哪种方法可以完美地工作。有没有人知道如何解决这个问题,或者提出另一种方法?
一段时间过去了,C ++ 14正在最终确定,编译器正在增加对新功能的支持,比如通用lambda。
通用lambda与下面显示的机制一起,允许用不相关的类实现所需的(动态)多态性:
this question
让我们付诸实践。假设有以下两个不相关的类:
#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;
}
该计划已于2013年11月的CTP上在VC12上进行了测试。不幸的是,我不知道任何支持通用lambda的在线编译器,所以我不能发布一个实例。
好的,这是一个疯狂的镜头:
A::foo(42, 3.14, Hello)
1
B::foo(1337, 6.28, World)
2
用法:
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);
}
我已经隐藏了一些通过解包元组来调用函数所需的工作,但我相信这已经在其他地方完成了。
此外,如果您需要存储引用而不是参数的副本,则可以这样做,但需要更加小心。 (你可以有一个引用元组。但你必须考虑是否也想要允许临时对象。)
不幸的是,这不能用C ++完成(但是 - 见结论)。遵循证据。
考虑因素1:[关于模板的需要]
为了确定在满足表达式R result = call_on_variant<R>(my_var, 12, "Hello", true);
(或其任何等效形式)时在运行时调用的正确成员函数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(...);
}
是编译时常量。不幸的是,C ++不允许使用I
,它允许编译器基于编译时for循环生成一系列这样的片段。相反,必须使用模板元编程技术,例如:
static for
idiom
其中mpl::for_each<types>(F());
是一个带模板调用操作符的仿函数。直接或间接地,需要定义至少一个类或函数模板,因为缺少F
迫使程序员编写必须针对每种类型一般重复的例程。
考虑2:[关于地方的需要]
期望解决方案的约束之一(问题文本中“CONSTRAINTS”部分的要求1)是没有必要在函数调用所在的任何其他范围内添加全局声明或任何其他声明。完成。因此,无论是涉及宏扩展还是模板元编程,都必须在函数调用发生的地方完成。
这是有问题的,因为上面的“考虑1”已经证明需要定义至少一个模板来执行任务。问题是C ++不允许在本地范围定义模板。类模板和函数模板也是如此,并且无法克服此限制。根据§14/ 2:
“模板声明只能作为命名空间范围或类范围声明出现”
因此,为了完成工作我们必须定义的通用例程必须在调用站点以外的地方定义,并且必须在具有适当参数的调用站点处实例化。
考虑3:[关于功能名称]
由于static for
宏(或任何等效构造)必须能够处理任何可能的函数call_on_variant()
,因此必须将f
的名称作为参数传递给基于模板的类型解析机制。重要的是要强调只传递函数的名称,因为需要调用的特定函数f
必须由模板机制确定。
但是,名称不能是模板参数,因为它们不属于类型系统。
结论:
上述三个考虑因素的组合证明,到目前为止,这个问题在C ++中无法解决。它需要使用名称作为模板参数的可能性或者定义本地模板的可能性。虽然第一件事至少是不可取的,但第二件事可能有意义,但标准化委员会并没有考虑到这一点。但是,可能会有一个例外。
未来的机会:
通用lambda被强烈推进到下一个C ++标准,实际上是带有模板调用运算符的本地类。
因此,即使我在问题文本末尾发布的宏仍然不起作用,另一种方法似乎是可行的(处理返回类型需要一些调整):
Ai::f
最后的评论:
这是一个有趣的类型安全调用案例,(遗憾的是)C ++不支持。 Mat Marcus,Jaakko Jarvi和Sean Parent的// 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);
似乎表明,不相关类型的动态多态性对于实现编程中重要的(在我看来,基本的和不可避免的)范式转换至关重要。
我曾经通过模拟.NET代理解决了这个问题:
This paper
您可以像.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();
}
Del以上是关于如何在不相关的类型上实现动态多态(运行时调用调度)?的主要内容,如果未能解决你的问题,请参考以下文章
不使用 instanceof 的重载方法的动态调度(运行时多态性)