繁体   English   中英

我可以合法地将成员函数指针转换为函数指针吗?

[英]Can I legally cast a member function pointer to a function pointer?

我继承了一些 C++ 代码,并且我的任务是消除警告。

这里我们有一个成员函数指针被强制转换为函数指针。 我知道成员函数指针与函数指针“不同”,因为在引擎盖下有一个隐式的“this”参数。 然而,我的前任似乎已经明确利用了这一事实,将成员函数指针转换为插入了附加第一个参数的函数指针。

我的问题是:

A) 我可以摆脱编译器警告吗?

B)这段代码在多大程度上保证工作?

出于这个问题的目的,我已将其缩减为一个小的 main.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)。这是类似的到上面的 myStoredFunction。然后在调用时将它们转换为单个签名,类似于上面的 myExtractedFunction。)

如何创建完全避免强制转换的函数:

template <typename C, void (C::*M)()>
void AsFunc(void* p)
{
    (static_cast<C*>(p)->*M)();
}

然后

void(*myStoredFunction)(void) = &AsFunc<MyClass, &MyClass::myMemberFunc>;

在 C++17 中,有一些特性,你甚至可能有template <auto *M> void AsFunc(void* p)void(*myStoredFunction)(void) = &AsFunc<&MyClass::myMemberFunc>;

要回答标题中的问题,不,您不能合法地将指向成员函数的指针转换为指向函数的指针。 据推测,这就是该演员阵容上的“编译器警告”所说的。

当遇到格式错误的代码(这有点过于简化)时,需要符合标准的编译器发出诊断信息,而这个人做到了。 它发出了警告。 这样做之后,编译器可以自由地做一些特定于实现的事情,它似乎已经完成了:它将代码编译成可以做你希望做的事情。

编译器可以以任何有效的方式自由地表示指向成员函数的指针,对于非虚拟函数,这可能只是指向函数的“正常”指针。 但是用虚函数试试; 我敢打赌后果更严重。

由于您显然需要在某些“无类型”对象 ( 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 )派生。 这些类(此处: CatDog ,我们称它们为“主体”)具有不同参数的不同函数( barkmeow - 当然每个主体可能有多个函数)。 每个主题都有自己的字符串到函数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> BaseSubject<Subject>*类型的指针可以直接且安全地转换为Subject*

请注意,根本没有不安全的强制转换——它都是由虚函数处理的。 因此,这应该是完全便携的。 每个学科类有一个map是打字密集型的,但允许您在不同的类中有相同命名的函数。 由于无论如何都需要为每个函数单独进行某种数据解包,因此我们在map有单独的解包 lambdas。

如果函数的参数只是抽象数据结构,即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()>

编译器自动生成将this指针加1代码,这样调用B::foothis指向XB基类。 为了在AsFunc函数中实现这一点(与埋在main相反),我引入了Obj模板参数,它让p参数是派生类型X这样AsFunc必须进行添加。

A) 我可以摆脱编译器警告吗?

是 - 将成员函数包装在来自静态函数的调用中

(这是@Jarod42 基于模板的答案的低技术变体)

B)这段代码在多大程度上保证工作?

不是(总结@Pete Becker 的回答)。 直到你摆脱警告。

这是我们的目标。 我们保持简单以尽量减少对代码的干扰。 我们避免使用高级 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;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM