[英]Using an array of function pointers to member functions in C++
在处理“命令和控制”类型的场景时,我经常自由地使用函数指针,在这些场景中,消息被发送到一个进程,其中一个函数是根据请求执行的。 这使得实现相当有效,因为不再需要使用switch-case
来做这样的事情(除了跳转表优化)。 例如:
取而代之的是:
switch(msg.cmd){
case FUNC0:
return func0(msg);
case FUNC1:
return func1(msg);
...
}
我们可以做这样的事情来直接执行适当的处理程序(省略对msg.cmd
任何健全性检查):
(*cmd_functions[msg.cmd])(msg)
最近,我开始使用实现类似“控制”功能的 C++ 代码,但我坚持使用 switch-case 来这样做。 是否有在 C++ 中执行此操作的规范方法? 也许是在构造函数中初始化的函数指针数组实例变量?
我担心由于运行时使用类的 V 表,解决方案可能会更复杂一些。
默认解决方案确实是一个v表:使用每个消息的虚方法声明一个基类/接口。
你需要一个switch(msg.cmd)
- 语句来调用相应的函数 - 但这基本上会取代你对函数表的初始化。
你会得到更清晰的处理,switch语句甚至可以进行参数转换(所以消息处理程序得到“有意义的”参数)。
您将失去该表的某些“组合性”,即将相同的处理函数“分配”给不同的,不相关的具体对象。
另一个更通用的选项是用std::function<void(MyMsg const &)>
替换函数指针的元素 - 这不仅可以分配全局/静态函数,还可以分配任何其他类的成员函数,lambda和类似。 您可以轻松转发到签名不匹配的现有函数。
这里的缺点是初始化表的成本较高,构造std :: function的sicne可能至少在一般情况下涉及一个分配。 此外,我预计至少目前会有更高的每次通话费用,因为你会错过典型的特定于v表的优化。
此时,您可能还需要考虑不同的体系结构:至少对于每条消息有多个“侦听器”的情况,您可能需要考虑事件订阅或信号/插槽设计。
当然,你也可以坚持你在C中的方式。这很好。
感谢非常有趣的问题! 我很高兴从头开始实施我自己的解决方案。
对于常规的非模板化函数,没有返回类型和参数,将所有函数包装到std::vector<std::function<void()>> cmds
并通过索引调用必要的命令是微不足道的。
但是我决定实现一个相当复杂的解决方案,以便能够处理具有任何返回类型的任何模板化函数(方法),因为模板在 C++ 世界中很常见。 我的答案末尾的完整代码是完整且有效的,在 main() 函数中提供了许多示例。 可以复制粘贴代码以在您自己的项目中使用。
为了在单个函数中返回许多可能的类型,我使用了std::variant ,这是 C++17 的标准类。
使用我的完整代码(在答案底部),您可以执行以下操作:
using MR = MethodsRunner<
Cmd<1, &A::Add3>,
Cmd<2, &A::AddXY<long long>>,
Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
Cmd<4, &B::VoidFunc>
>;
A a;
B b;
auto result1 = MR::Run(1, a, 5); // Call cmd 1
auto result2 = MR::Run(2, a, 3, 7); // Call cmd 2
auto result3 = MR::Run(3, b); // Call cmd 3
auto result4 = MR::Run(4, b, 12, true); // Call cmd 4
auto result4_bad = MR::Run(4, b, false); // Call cmd 4, wrong arguments, exception!
auto result5 = MR::Run(5, b); // Call to unknown cmd 5, exception!
这里A
和B
是任何类。 向MethodsRunner
提供一个命令列表,包括命令 id 和指向方法的指针。 您可以提供指向任何模板化方法的指针,只要您提供他们调用的完整签名即可。
MethodsRunner
在被.Run()
调用时返回std::variant
包含所有不同类型的可能值。 您可以通过std::get(variant)访问变体的实际值,或者如果您事先不知道包含的类型,则可以使用std::visit(lambda, variant) 。
我的 MethodsRunner 的所有类型的用法都显示在完整代码的 main() 中(在答案的末尾)。
在我的课堂上,我使用了几个小助手模板化结构,这种元编程在模板化 C++ 世界中非常普遍。
我在我的解决方案中使用了switch
构造而不是std::vector<std::function<void()>>
,因为只有switch
才允许处理一组任意参数类型和计数以及任意返回类型。 只有在所有命令具有相同参数类型和相同返回值的情况下,才能使用std::function
表代替switch
。
众所周知,如果 switch 和 case 值为整数,则所有现代编译器都将switch
实现为直接跳转表。 换句话说, switch
解决方案与常规std::vector<std::function<void()>>
函数表方法一样快,甚至可能更快。
我的解决方案应该是非常高效的,虽然它看起来包含很多代码,但我的所有重模板化的代码都被折叠成非常小的实际运行时代码,基本上有一个直接调用所有方法的switch
表,加上转换为std: :variant 返回值,就是这样,几乎没有任何开销。
我预计您使用的命令 ID 在编译时是未知的,我预计它仅在运行时才知道。 如果在编译时就知道了,那么根本不需要switch
,基本上你可以直接调用给定的对象。
我的 Run 方法的语法是method_runner.Run(cmd_id, object, arguments...)
,在这里您提供仅在运行时已知的任何命令 ID,然后您提供任何对象和任何参数。 如果您只有一个实现所有命令的对象,那么您可以使用我在代码中也实现的SingleObjectRunner
,如下所示:
SingleObjectRunner<MR, A> ar(a);
ar(1, 5); // Call cmd 1
ar(2, 3, 7); // Call cmd 2
SingleObjectRunner<MR, B> br(b);
br(3); // Call cmd 3
br(4, 12, true); // Call cmd 4
其中MR
是专门用于所有命令的MethodsRunner
类型。 这里的单个对象跑步者ar
和br
都是可调用的,就像函数一样,具有签名(cmd_id, args...)
,例如br(4, 12, true)
调用意味着 cmd id 是4
, args 是12, true
和b
对象本身在构建时通过br(b);
被捕获在 SingleObjectRunner 中br(b);
代码后查看详细的控制台输出日志。 另请参阅代码和日志后的重要注释。 完整代码如下:
#include <iostream>
#include <type_traits>
#include <string>
#include <any>
#include <vector>
#include <tuple>
#include <variant>
#include <iomanip>
#include <stdexcept>
#include <cxxabi.h>
template <typename T>
inline std::string TypeName() {
// use following line of code if <cxxabi.h> unavailable, and/or no demangling is needed
//return typeid(T).name();
int status = 0;
return abi::__cxa_demangle(typeid(T).name(), 0, 0, &status);
}
struct NotCallable {};
struct VoidT {};
template <size_t _Id, auto MethPtr>
struct Cmd {
static size_t constexpr Id = _Id;
template <class Obj, typename Enable = void, typename ... Args>
struct Callable : std::false_type {};
template <class Obj, typename ... Args>
struct Callable<Obj,
std::void_t<decltype(
(std::declval<Obj>().*MethPtr)(std::declval<Args>()...)
)>, Args...> : std::true_type {};
template <class Obj, typename ... Args>
static auto Call(Obj && obj, Args && ... args) {
if constexpr(Callable<Obj, void, Args...>::value) {
if constexpr(std::is_same_v<void, std::decay_t<decltype(
(obj.*MethPtr)(std::forward<Args>(args)...))>>) {
(obj.*MethPtr)(std::forward<Args>(args)...);
return VoidT{};
} else
return (obj.*MethPtr)(std::forward<Args>(args)...);
} else {
throw std::runtime_error("Calling method '" + TypeName<decltype(MethPtr)>() +
"' with wrong object type and/or wrong argument types or count and/or wrong template arguments! "
"Object type '" + TypeName<Obj>() + "', tuple of arguments types '" + TypeName<std::tuple<Args...>>() + "'.");
return NotCallable{};
}
}
};
template <typename T, typename ... Ts>
struct HasType;
template <typename T>
struct HasType<T> : std::false_type {};
template <typename T, typename X, typename ... Tail>
struct HasType<T, X, Tail...> {
static bool constexpr value = std::is_same_v<T, X> ||
HasType<T, Tail...>::value;
};
template <typename T> struct ConvVoid {
using type = T;
};
template <> struct ConvVoid<void> {
using type = VoidT;
};
template <typename V, typename ... Ts>
struct MakeVariant;
template <typename ... Vs>
struct MakeVariant<std::variant<Vs...>> {
using type = std::variant<Vs...>;
};
template <typename ... Vs, typename T, typename ... Tail>
struct MakeVariant<std::variant<Vs...>, T, Tail...> {
using type = std::conditional_t<
HasType<T, Vs...>::value,
typename MakeVariant<std::variant<Vs...>, Tail...>::type,
typename MakeVariant<std::variant<Vs...,
typename ConvVoid<std::decay_t<T>>::type>, Tail...>::type
>;
};
template <typename ... Cmds>
class MethodsRunner {
public:
using CmdsTup = std::tuple<Cmds...>;
static size_t constexpr NumCmds = std::tuple_size_v<CmdsTup>;
template <size_t I> using CmdAt = std::tuple_element_t<I, CmdsTup>;
template <size_t Id, size_t Idx = 0>
static size_t constexpr CmdIdToIdx() {
if constexpr(Idx < NumCmds) {
if constexpr(CmdAt<Idx>::Id == Id)
return Idx;
else
return CmdIdToIdx<Id, Idx + 1>();
} else
return NumCmds;
}
template <typename Obj, typename ... Args>
using RetType = typename MakeVariant<std::variant<>, decltype(
Cmds::Call(std::declval<Obj>(), std::declval<Args>()...))...>::type;
template <typename Obj, typename ... Args>
static RetType<Obj, Args...> Run(size_t cmd, Obj && obj, Args && ... args) {
#define C(Id) \
case Id: { \
if constexpr(CmdIdToIdx<Id>() < NumCmds) \
return CmdAt<CmdIdToIdx<Id>()>::Call( \
obj, std::forward<Args>(args)... \
); \
else goto out_of_range; \
}
switch (cmd) {
C( 0) C( 1) C( 2) C( 3) C( 4) C( 5) C( 6) C( 7) C( 8) C( 9)
C( 10) C( 11) C( 12) C( 13) C( 14) C( 15) C( 16) C( 17) C( 18) C( 19)
default:
goto out_of_range;
}
#undef C
out_of_range:
throw std::runtime_error("Unknown command " + std::to_string(cmd) +
"! Number of commands " + std::to_string(NumCmds));
}
};
template <typename MR, class Obj>
class SingleObjectRunner {
public:
SingleObjectRunner(Obj & obj) : obj_(obj) {}
template <typename ... Args>
auto operator () (size_t cmd, Args && ... args) {
return MR::Run(cmd, obj_, std::forward<Args>(args)...);
}
private:
Obj & obj_;
};
class A {
public:
int Add3(int x) const {
std::cout << "Add3(" << x << ")" << std::endl;
return x + 3;
}
template <typename T>
auto AddXY(int x, T y) {
std::cout << "AddXY(" << x << ", " << y << ")" << std::endl;
return x + y;
}
};
class B {
public:
template <typename V>
std::string ToStr() {
std::cout << "ToStr(" << V{}() << ")" << std::endl;
return "B_ToStr " + std::to_string(V{}());
}
void VoidFunc(int x, bool a) {
std::cout << "VoidFunc(" << x << ", " << std::boolalpha << a << ")" << std::endl;
}
};
#define SHOW_EX(code) \
try { code } catch (std::exception const & ex) { \
std::cout << "\nException: " << ex.what() << std::endl; }
int main() {
try {
using MR = MethodsRunner<
Cmd<1, &A::Add3>,
Cmd<2, &A::AddXY<long long>>,
Cmd<3, &B::ToStr<std::integral_constant<int, 17>>>,
Cmd<4, &B::VoidFunc>
>;
auto VarInfo = [](auto const & var) {
std::cout
<< ", var_idx: " << var.index()
<< ", var_type: " << std::visit([](auto const & x){
return TypeName<decltype(x)>();
}, var)
<< ", var: " << TypeName<decltype(var)>()
<< std::endl;
};
A a;
B b;
{
auto var = MR::Run(1, a, 5);
std::cout << "cmd 1: var_val: " << std::get<int>(var);
VarInfo(var);
}
{
auto var = MR::Run(2, a, 3, 7);
std::cout << "cmd 2: var_val: " << std::get<long long>(var);
VarInfo(var);
}
{
auto var = MR::Run(3, b);
std::cout << "cmd 3: var_val: " << std::get<std::string>(var);
VarInfo(var);
}
{
auto var = MR::Run(4, b, 12, true);
std::cout << "cmd 4: var_val: VoidT";
std::get<VoidT>(var);
VarInfo(var);
}
std::cout << "------ Single object runs: ------" << std::endl;
SingleObjectRunner<MR, A> ar(a);
ar(1, 5);
ar(2, 3, 7);
SingleObjectRunner<MR, B> br(b);
br(3);
br(4, 12, true);
std::cout << "------ Runs with exceptions: ------" << std::endl;
SHOW_EX({
// Exception, wrong argument types
auto var = MR::Run(4, b, false);
});
SHOW_EX({
// Exception, unknown command
auto var = MR::Run(5, b);
});
return 0;
} catch (std::exception const & ex) {
std::cout << "Exception: " << ex.what() << std::endl;
return -1;
}
}
输出:
Add3(5)
cmd 1: var_val: 8, var_idx: 0, var_type: int, var: std::variant<int, NotCallable>
AddXY(3, 7)
cmd 2: var_val: 10, var_idx: 1, var_type: long long, var: std::variant<NotCallable, long long>
ToStr(17)
cmd 3: var_val: B_ToStr 17, var_idx: 1, var_type: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, var: std::variant<NotCallable, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >
VoidFunc(12, true)
cmd 4: var_val: VoidT, var_idx: 1, var_type: VoidT, var: std::variant<NotCallable, VoidT>
------ Single object runs: ------
Add3(5)
AddXY(3, 7)
ToStr(17)
VoidFunc(12, true)
------ Runs with exceptions: ------
Exception: Calling method 'void (B::*)(int, bool)' with wrong object type and/or wrong argument types or count and/or wrong template arguments! Object type 'B', tuple of arguments types 'std::tuple<bool>'.
Exception: Unknown command 5! Number of commands 4
注意:在我的代码中,我使用#include <cxxabi.h>
来实现TypeName<T>()
函数,这个包含的标头仅用于名称分解目的。 此头文件在 MSVC 编译器中不可用,并且可能在 Windows 版本的 CLang 中不可用。 在 MSVC 中,您可以删除#include <cxxabi.h>
并在TypeName<T>()
通过返回return typeid(T).name();
. 此标头是我的代码中唯一不可交叉编译的部分,如果需要,您可以轻松删除此标头的使用。
嗯。 这有点晚了,但我过去曾使用过以下方法之一:
经典的 OO 方法是,如果您想要的是一堆相关的(派生自同一个基类)类对象,它会为所有虚函数隐式地创建一个 vtable。
但有时,您可能希望对包含在一个类或命名空间中的一系列行为进行建模,以减少样板并获得一些额外的速度。 这将是因为:
不同类的 vtable 可以存储在内存中的任何位置,而且很可能不在连续区域中。 这会减慢查找速度,因为您要查找的表条目很可能不在缓存中。
在数组中仅保留您感兴趣的函数的 vtable,会将指针本地化到内存中的连续位置,使其更有可能在缓存中。 此外,如果幸运的话,一些编译器的优化器可能足够先进,甚至可以像在 switch case 语句中那样内联代码。
使用函数指针数组,您可以存储指向全局函数(最好封装在命名空间中)或静态成员函数(最好在类的私有部分,以便它们不能在外部直接访问)的常规函数指针班上)。
函数指针数组不限于常规函数,还可以使用成员函数指针。 这些使用起来有点棘手,但一旦掌握了它,就不会太糟糕。 请参阅.* 和 ->* 运算符之间的区别是什么? 举个例子。 直接使用函数指针是可行的,但正如那里所建议的,最好使用invoke()
函数,因为它可能会因需要额外括号的优先规则而变得混乱。
在夏天:
您所做的实际上取决于您的情况、您尝试建模的内容,尤其是您的要求。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.