[英]Map of function pointers to member functions
我已经在 SO 上尝试了各种解决方案来解决这个问题,但我一定是做错了什么。
我有几个类,其中每个类中的方法都具有相同的方法签名:
typedef int (*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> ¶ms);
还有一个示例类,它有一些使用该签名的方法:
class StaticContentController {
public:
int handleStaticContentRequest(const std::string &data, const std::unordered_map<std::string, std::string> ¶ms) {
return 1;
}
}
现在我尝试创建一个指向成员函数的指针映射:
std::map<std::string, ControllerMethod> operations;
operations.emplace("staticContent", &StaticContentController::handleStaticContentRequest);
std::string d("test.txt");
ControllerMethod f = operations["staticContent"];
auto s = ((_staticContentController).*f)(d, pooledQueries); // <- compile error here
但是调用该方法会导致编译错误
Right hand operand to .* has non-pointer-to-member type 'web::server::ControllerMethod'
我错过了什么?
更新:
我现在有一个空的 Controller 基类,其他控制器类继承自:
namespace web { namespace server {
class Controller {
};
typedef ControllerResponse (Controller::*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> ¶ms);
}}
现在我在 Operations.emplace() 收到以下错误:
No matching constructor for initialization of 'std::__1::pair<const std::__1::basic_string<char>, web::server::ControllerResponse
更新答案
您在这里尝试使用两种不同的范式,但它们实际上并不兼容。 如果我正确解释了您的编辑,则您正在尝试创建调用其他类的函数映射,并且您希望将此映射声明为一组函数指针。
函数指针是一个汇编级构造,由 C 公开。语法反映了这一点——如果没有帮助,就不可能让 C++ 类符合这一点——即,添加一个与每个函数指针关联的上下文指针参数,并转换指向调用成员函数的类实例的上下文指针。
那么,我们如何解决这个问题呢?
在接下来的两种方法中,我们都需要一个与函数表关联的上下文对象。 这涉及创建一个结构来保存成员函数和上下文:
template<typename T> struct FunctionTableEntry
{
ControllerMethod Function;
T* Context;
};
我们的函数指针变成了以下内容:
typedef ControllerResponse (T::*ControllerMethod)(const std::string &data, const StringMap ¶ms);
这里, StringMap
是std::unordered_map<std::string, std::string>
。
我们现在的主要问题是移除模板参数 T,因为我们无法制作运行时定义的模板的映射(模板的类型只能在运行时知道)。
有两种主要的方法可以解决这个问题,两者都有需要考虑的问题。 第一个是使用指针和非常小心的关联来执行 C 风格的类型擦除。 第二种是放弃函数指针,转而使用 C++ 函数对象。
C 型类型擦除
此选项涉及使用 C 样式转换将类实例指针转换为其基类类型,将成员函数指针转换为函数声明所期望的类型,然后进行调用,就像基类定义方法一样。 这需要使用指针,没有它们就无法完成。
为此,我们的FunctionTableEntry
结构更改为以下内容:
struct FunctionTableEntry
{
ControllerMethod Function;
Controller* Context;
}
和我们的函数指针:
typedef ControllerResponse (Controller::*ControllerMethod)(const std::string &data, const StringMap ¶ms);
要添加新条目,我们执行以下操作:
std::map<std::string, FunctionTableEntry> operations;
FunctionTableEntry Entry;
Entry.Function = (ControllerMethod)&StaticContentController::handleStaticContentRequest;
Entry.Context = (Controller*)&_staticContentController;
operations.emplace("staticContent", Entry);
并称之为:
FunctionTableEntry f = operations["staticContent"];
auto s = ((f.Context)->*f.Function)(d, pooledQueries);
这种方法有一些缺点——首先,你别无选择,只能使用指针来引用你的控制器对象——否则转换将无法正常工作。 您可以使用 std::shared_ptr 使其对 C++ 更加友好,但除此之外,无法替换它。 这也意味着您需要仔细管理控制器对象的生命周期。 如果在函数表仍在引用它们时释放它们,您几乎肯定会导致系统崩溃。
其次,转换可能会导致复杂继承层次结构的问题。 此方法仅在(Controller*)_staticContentController == _staticContentController
,即转换到基类给出相同的数字指针值。 否则,被调用的方法将失败,因为它将无法正确引用其本地数据。
然而,这种方法的优点是速度非常快。 除了查表之外没有任何函数开销,生成的程序集也不过是正常调用函数而已。 它也是独立于运行时的——只要上面的等式表达式对于控制器系统的所有用户都是真的,任何拥有 C++ 编译器的人都可以创建一个新的控制器,这个系统将能够调用他们的函数,即使他们使用完全不同的运行时库。
此外,如果您知道控制器实例将与多个函数一起使用,您可以修改结构以提供与一个Context
值关联的函数映射,从而减少一些内存开销。 这对于您的设计来说可能是不可能的,但如果内存是一个问题,则值得研究。
C++ 函数对象
第二种解决方案是完全取消 C 风格的函数指针并使用std::function
。 由于std::function
可以包含实例数据作为其自身的一部分,并且可以放置到映射中,这允许您std::bind
成员函数,创建部分指定的函数调用(我相信在函数式编程中这就是所谓的关闭)。
在这种情况下,没有FunctionTableEntry
结构 - 相反,我们使用以下内容:
typedef std::function<ControllerResponse(const std::string&, const StringMap&)> ControllerMethod;
要添加新方法,我们执行以下操作:
std::map<std::string, ControllerMethod> operations;
operations.emplace("staticContent", std::bind(&StaticContextController::handleStaticContentRequest, &_staticContentController, std::placeholders::_1, std::placeholders::_2);
这将创建一个闭包,它使用所需的控制器实例调用成员函数。
要调用它,我们执行以下操作:
std::string d("test.txt");
ControllerMethod f = operations["staticContent"];
auto s = f(d, pooledQueries);
C++ 函数对象覆盖operator ()
,这允许它们像静态函数一样工作。
此方法允许成员函数和静态函数存在于同一映射中。 它还允许发生复杂的继承层次结构,因为没有强制转换来使事情发挥作用 - 一切都发生在模板函数中。
这种方法的缺点是你仍然需要处理对象的生命周期——在清除函数映射之前,内容控制器对象不能被销毁。 此外,由于使用带有占位符参数的 std::function 会产生一些开销(尽管这可能取决于使用的运行时库,但我的测试表明它在 x86-64 GCC 9.3 中生成了更多代码)。
这个方法也不是运行时独立的——你在此处选择使用的任何运行时也必须由使用此代码的每个程序员使用,否则每个库创建和存储std::function
方式的不兼容性将导致奇怪的失败。 这意味着没有编译器混合 - 如果您使用 MSVC 2019 来构建 API,则使用此库的其他所有人都必须使用 MSVC2019 来构建他们的控制器组件。 如果您没有在此处提供 API,那么这不是问题。
原答案
您的函数指针声明是错误的 - 成员指针与普通函数指针 typedef 的语法不同。
普通函数指针使用您当前的语法:
typedef int (*foo)(int x, int y);
指向成员函数 typedef 的指针如下所示:
typedef int (SomeClass::*foo)(int x, int y);
SomeClass::
部分是必需的,因为指向成员的指针有一个附加参数,称为this
。 在 C++ 中, this
指针作为第一个参数传递给函数,这使得函数声明有所不同(因为调用该函数所需的实际汇编代码不同,请参阅 MSVC 生成的汇编以获得真实示例)。
为了解决这个问题,您需要提供一个可以用于声明typedef的基类,然后从该类继承以允许调用该方法。 这实际上与使用继承相同,除非您在同一类型中有多个具有相同签名但做不同事情的方法。
DirectX 11 效果框架使用这种精确的范例来避免在图形管道中配置不同的着色器类型时出现分支 - 请参阅此处,第 590 行。
正如所指出的,类StaticContentController
的非静态成员函数的类型不是:
typedef int (*ControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> ¶ms);
相反,它是:
typedef int (StaticContentController::*StaticContentControllerMethod)(const std::string &data, const std::unordered_map<std::string, std::string> ¶ms);
这是你最初的错误。
这是有道理的,因为您需要一个实例来调用成员函数,并且该实例也有一个类型。 如果你有一个Base::*Function
指针,你可以用一个类的实例来调用它,并且明确地从 Base 公开且明确地派生出来,这是有道理的,因为派生的指针可以隐式地转换为基指针。
不能将Derived::*Function
指针分配给Base::*Function
指针也是有道理的,因为可以使用任何Base
实例调用结果,而不必是Derived
实例。 这是问题更新中的错误。
在这种非常有限的情况下,C++ 的行为完全符合逻辑。
通过对正确类型的修改,您的代码段将编译:
std::map<std::string, StaticContentControllerMethod> operations;
operations.emplace("staticContent",
&StaticContentController::handleStaticContentRequest);
std::string d("test.txt");
StaticContentControllerMethod f = operations["staticContent"];
auto s = ((_staticContentController).*f)(d, pooledQueries); // <- works
所以大概你的实际问题是如何在这个映射中存储多个类的成员函数指针,而不仅仅是StaticContentController
。 但这是错误的问题。 您必须拥有实例 ( _staticContentController
) 来调用成员函数指针,因此您已经知道类型。
所以也许你想问如何擦除类型。 一种方法是存储不需要实例的东西:为此,使用std::function
作为映射类型并在插入映射时绑定实例。 如果您在创建地图时拥有控制器,这将起作用并且很简单。 第二种方法是对映射类型使用像std::any
这样的类型擦除类型,并在使用点使用any_cast
将其返回到其初始类型。 第三种方法是使用公共基类和在您的类中覆盖的虚函数。 由于可以使用基指针调用虚函数,因此您可以存储基类的成员函数指针。
或者,也许您想询问如何拥有类型索引集合:该类型在查找时已知(因为您有一个实例)并且您需要查找其类型(成员函数指针)取决于“键”的值类型。
最简单的方法是使用模板化类,并让编译器处理映射:
template<typename T>
struct operations {
static std::map<std::string, void (T::*)(etc.)> pointers;
};
// use:
operations<StaticContentController>::pointers["staticContent"];
另一个版本的类型索引可能具有以下接口:
template<template<typename> typename Value>
class type_keyed_map
{
public:
template<typename T>
void insert(std::unique_ptr<Value<T>> value);
template<typename T>
auto find() -> Value<T>*; // return null if not found
};
您可以在实现中使用std::map
,但std::map
不允许多个值类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.