我继承了一些C++代码,我的任务是摆脱警告。
在这里,我们有一个成员函数指针被强制转换为函数指针。 我知道成员函数指针与函数指针"不同",因为底层涉及一个隐式的"this"参数。然而,我的前任似乎已经明确地利用了这一事实,从成员函数指针强制转换为插入了附加第一个参数的函数指针。
我的问题是:
A) 我可以摆脱编译器警告吗?
B) 这段代码在多大程度上保证有效?
为了这个问题的目的.cpp我把它削减成一个小的主:
#define GENERIC_FUNC_TYPE void(*)(void)
#define FUNC_TYPE int(*)(void *)
class MyClass
{
public:
MyClass(int a) : memberA(a) {}
int myMemberFunc()
{
return memberA;
}
private:
int memberA;
};
int main(int argc, char*argv[])
{
int (MyClass::* memberFunc) () = &MyClass::myMemberFunc;
MyClass myObject(1);
std::cout << (myObject.*memberFunc)() << std::endl;
// All good so far
// Now get naughty, store it away in a very basic fn ptr
void(*myStoredFunction)(void) = (GENERIC_FUNC_TYPE)memberFunc; // Compiler warning
// Reinterpret the fn pointer as a pointer to fn, with an extra object parameter
int (*myExtractedFunction)(void*) = (FUNC_TYPE)myStoredFunction;
// Call it
std::cout << myExtractedFunction(&myObject) << std::endl;
}
代码在 g++ 下编译时有一个警告,并按预期输出两个 1:
main.cpp: In function ‘int main(int, char**)’:
main.cpp:27:53: warning: converting from ‘int (MyClass::*)()’ to ‘void (*)()’ [-Wpmf-conversions]
void(*myStoredFunction)(void) = (GENERIC_FUNC_TYPE)memberFunc; // Compiler warning
^
恕我直言,此代码正在对编译器的底层机制做出假设。或者,也许这些假设对所有C++编译器都有效 - 任何人都可以提供帮助吗?
(在实际代码中,我们将按名称在映射中存储一大堆函数指针。这些函数都有不同的签名,这就是为什么它们都被强制转换为相同的签名 void(*)(void) 的原因。这类似于上面的myStorageFunction。然后,它们在调用时被强制转换为各个签名,类似于上面的 myExtractedFunction。
如何创建完全避免强制转换的函数:
template <typename C, void (C::*M)()>
void AsFunc(void* p)
{
(static_cast<C*>(p)->*M)();
}
然后
void (*myExtractedFunction)(void*) = &AsFunc<MyClass, &MyClass::myMemberFunc>;
在C++17,在某些特质中,你甚至可能有template <auto *M> void AsFunc(void* p)
和void(*myStoredFunction)(void*) = &AsFunc<&MyClass::myMemberFunc>;
要回答标题中的问题,不可以,您不能合法地将指向成员的指针转换为指向函数的指针。据推测,这就是该演员表行上的"编译器警告"所说的。
当遇到格式错误的代码(这有点过于简化)时,需要符合要求的编译器发出诊断,而这个编译器做到了。它发出了警告。完成此操作后,编译器可以自由地执行特定于实现的操作,它似乎已经做到了:它将代码编译成可以达到您所希望的效果。
编译器可以自由地以任何工作的方式表示指向成员函数的指针,对于非虚函数,这可能只是一个指向函数的"普通"指针。但是尝试使用虚拟函数;我敢打赌后果会更严重。
A) 我可以摆脱编译器警告吗?
是 - 将成员函数包装在来自静态函数的调用中
(这是@Jarod42基于模板的答案的低技术变体)
B) 这段代码在多大程度上保证有效?
这不是(总结@Pete贝克尔的答案)。直到你摆脱警告。
这是我们的要点。我们保持简单,以尽量减少对代码的中断。我们避免使用高级C++功能,以最大限度地增加可以处理代码的人数。
#include <iostream>
class MyClass
{
public:
MyClass(int a) : memberA(a) {}
static int myMemberFuncStatic(MyClass *obj)
{
return obj->myMemberFunc();
}
int myMemberFunc()
{
return memberA;
}
private:
int memberA;
};
typedef void(*GENERIC_FUNC_TYPE)(void);
typedef int(*FUNC_TYPE)(MyClass *);
int main(int argc, char*argv[])
{
int (* staticFunc) (MyClass *) = &MyClass::myMemberFuncStatic;
MyClass myObject(1);
std::cout << staticFunc(&myObject) << std::endl;
// All good so far
// This is actually legal, for non-member functions (like static functions)
GENERIC_FUNC_TYPE myStoredFunction = reinterpret_cast<GENERIC_FUNC_TYPE> (staticFunc); // No compiler warning
// Reinterpret the fn pointer as the static function
int (*myExtractedFunction)(MyClass*) = (FUNC_TYPE)myStoredFunction;
// Call it
std::cout << myExtractedFunction(&myObject) << std::endl;
}
由于您显然需要在某个"非类型化"对象(void*
)上按名称调用函数,同时传入许多因函数而异的参数,因此您需要某种多重调度。一个可能的解决方案是:
#include <string>
#include <iostream>
#include <stdexcept>
#include <functional>
#include <utility>
#include <map>
template <typename Subj>
using FunctionMap = std::map<std::string, std::function<void (Subj&, const std::string&)>>;
class AbstractBaseSubject {
public:
virtual void invoke (const std::string& fName, const std::string& arg) = 0;
};
template <typename Class>
class BaseSubject : public AbstractBaseSubject {
public:
virtual void invoke (const std::string& fName, const std::string& arg) {
const FunctionMap<Class>& m = Class::functionMap;
auto iter = m.find (fName);
if (iter == m.end ())
throw std::invalid_argument ("Unknown function "" + fName + """);
iter->second (*static_cast<Class*> (this), arg);
}
};
class Cat : public BaseSubject<Cat> {
public:
Cat (const std::string& name) : name(name) {}
void meow (const std::string& arg) {
std::cout << "Cat(" << name << "): meow (" << arg << ")n";
}
static const FunctionMap<Cat> functionMap;
private:
std::string name;
};
const FunctionMap<Cat> Cat::functionMap = {
{ "meow", [] (Cat& cat, const std::string& arg) { cat.meow (arg); } }
};
class Dog : public BaseSubject<Dog> {
public:
Dog (int age) : age(age) {}
void bark (float arg) {
std::cout << "Dog(" << age << "): bark (" << arg << ")n";
}
static const FunctionMap<Dog> functionMap;
private:
int age;
};
const FunctionMap<Dog> Dog::functionMap = {
{ "bark", [] (Dog& dog, const std::string& arg) { dog.bark (std::stof (arg)); }}
};
int main () {
Cat cat ("Mr. Snuggles");
Dog dog (7);
AbstractBaseSubject& abstractDog = dog; // Just to demonstrate that the calls work from the base class.
AbstractBaseSubject& abstractCat = cat;
abstractCat.invoke ("meow", "Please feed me");
abstractDog.invoke ("bark", "3.14");
try {
abstractCat.invoke ("bark", "3.14");
} catch (const std::invalid_argument& ex) {
std::cerr << ex.what () << std::endl;
}
try {
abstractCat.invoke ("quack", "3.14");
} catch (const std::invalid_argument& ex) {
std::cerr << ex.what () << std::endl;
}
try {
abstractDog.invoke ("bark", "This is not a number");
} catch (const std::invalid_argument& ex) {
std::cerr << ex.what () << std::endl;
}
}
在这里,所有具有以这种方式调用的函数的类都需要从BaseSubject
派生(这是一个 CRTP)。这些类(这里:Cat
和Dog
,我们称它们为"主体")具有不同的函数和不同的参数(bark
和meow
- 当然每个主题可以有多个函数)。每个主题都有自己的字符串到函数map
。这些函数不是函数指针,而是std::function<void (SubjectType&,const std::string&)>
实例。其中每个都应该调用对象的相应成员函数,传入所需的参数。参数需要来自某种通用数据表示 - 在这里,我选择了一个简单的std::string
。它可能是 JSON 或 XML 对象,具体取决于数据的来源。std::function
实例需要反序列化数据并将其作为参数传递。map
在每个主题类中创建为static
变量,其中std::function
实例填充了 lambda。BaseSubject
类查找function
实例并调用它。由于主题类应该总是直接从BaseSubject<Subject>
派生,因此类型BaseSubject<Subject>*
的指针可以直接安全地转换为Subject*
。
请注意,根本没有不安全的强制转换 - 它全部由虚拟函数处理。因此,这应该是完全便携的。每个主题类有一个map
是键入密集型的,但允许您在不同的类中具有名称相同的函数。由于无论如何都需要为每个函数单独进行某种数据解包,因此我们在map
内部有单独的解包 lambda。
如果函数的参数只是抽象的数据结构,即const std::string&
,我们可以把lambdas排除在外,只做:
const FunctionMap<Cat> Cat::functionMap = {
{ "meow", &Cat::meow }
};
它通过std::function
的魔术(通过第一个参数传递this
)来工作,与函数指针相反,它是明确定义和允许的。如果所有函数都具有相同的签名,这将特别有用。事实上,我们甚至可以省略std::function
并插入Jarod42的建议。
PS:只是为了好玩,这里有一个将成员-函数-指针转换为函数指针失败的示例:
#include <iostream>
struct A {
char x;
A () : x('A') {}
void foo () {
std::cout << "A::foo() x=" << x << std::endl;
}
};
struct B {
char x;
B () : x('B') {}
void foo () {
std::cout << "B::foo() x=" << x << std::endl;
}
};
struct X : A, B {
};
int main () {
void (B::*memPtr) () = &B::foo;
void (*funPtr) (X*) = reinterpret_cast<void (*)(X*)> (memPtr); // Illegal!
X x;
(x.*memPtr) ();
funPtr (&x);
}
在我的机器上,这打印:
B::foo() x=B
B::foo() x=A
B
类不应该打印"x=A"!发生这种情况是因为成员函数指针带有一个额外的偏移量,该偏移量在调用之前添加到this
,以防多重继承发挥作用。强制转换会失去此偏移量。因此,在调用强制转换函数指针时,this
自动引用第一个基对象,而B
是第二个基对象,打印错误的值。
PPS: 为了更多乐趣: 如果我们插入Jarod42的建议:
template <typename C, void (C::*M)(), typename Obj>
void AsFunc (Obj* p) {
(p->*M)();
}
int main () {
void (*funPtr) (X*) = AsFunc<B, &B::foo, X>;
X x;
funPtr (&x);
}
程序正确打印:
B::foo() x=B
如果我们看一下AsFunc
的拆解,我们会看到:
c90 <void AsFunc<B, &B::foo, X>(X*)>:
c90: 48 83 c7 01 add $0x1,%rdi
c94: e9 07 ff ff ff jmpq ba0 <B::foo()>
编译器自动生成将1
添加到this
指针的代码,以便调用B::foo
,this
指向B
基类X
。为了在AsFunc
函数中做到这一点(而不是埋在main
中),我引入了Obj
模板参数,它允许p
参数是派生类型的X
这样AsFunc
必须进行加法。