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

时间:2019-07-25 09:30:56

标签: c++ member-function-pointers

我继承了一些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。)

4 个答案:

答案 0 :(得分:6)

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

UIApplication.Main(args, null, "AppDelegate");

然后

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

在C ++ 17中,具有某些特征,您甚至可能拥有void(*myStoredFunction)(void) = &AsFunc<MyClass, &MyClass::myMemberFunc>; template <auto *M> void AsFunc(void* p)

答案 1 :(得分:3)

要回答标题中的问题,不,您不能合法地将指向成员函数的指针转换为指向函数的指针。大概就是那个强制转换行上的“编译器警告”。

当遇到格式错误的代码(有点过分简化)时,需要一个合格的编译器发出诊断,而这个诊断确实如此。它发出了警告。完成此操作后,编译器可以自由地执行特定于实现的操作,这似乎已经完成了:它将代码编译为可以实现您希望的功能。

编译器可以自由地以任何可行的方式表示成员函数的指针,而对于非虚拟函数,则可以只是函数的“正常”指针。但是尝试使用虚函数;我敢打赌后果会更加严峻。

答案 2 :(得分:1)

由于您显然需要在一些“未类型化的”对象(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-当然,每个函数有多个函数主题)。每个主题都有自己的{@ {1}}字符串转换功能。这些函数不是函数指针,而是map实例。每个对象都应调用对象的相应成员函数,并传入所需的参数。参数需要来自某种通用数据表示形式-在这里,我选择了一个简单的std::function<void (SubjectType&,const std::string&)>。根据数据的来源,它可能是JSON或XML对象。 std::string实例需要反序列化数据并将其作为参数传递。 std::function被创建为每个主题类中的map变量,其中static实例中填充了lambda。 std::function类查找BaseSubject实例并调用它。由于主题类应始终直接从function派生,因此类型BaseSubject<Subject>的指针可以直接安全地强制转换为BaseSubject<Subject>*

请注意,根本没有不安全的类型转换-所有这些都由虚函数处理。因此,这应该是完全可移植的。每个学科课都有一个Subject*,这是打字密集型的,但可以让您在不同的课中使用同名的函数。由于无论如何都需要对每个函数分别进行某种数据拆包,因此我们在map中有单独的拆包lambda。

如果函数的参数仅是抽象数据结构,即map,则可以省略lambda并执行以下操作:

const std::string&

这是通过const FunctionMap<Cat> Cat::functionMap = { { "meow", &Cat::meow } }; 的魔术(通过第一个参数传递std::function)来实现的,与函数指针相反,魔术是定义明确的并且允许的。如果所有功能都具有相同的签名,这将特别有用。实际上,我们甚至可以省去this并插入Jarod42的建议。

PS:只是为了好玩,这是一个将成员函数指针转换为函数指针失败的示例:

std::function

在我的机器上,此打印:

#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 类不应显示“ x = A”!发生这种情况是因为成员函数指针带有一个额外的偏移量,该偏移量会在调用之前添加到B中,以防多重继承起作用。投放会失去此补偿。因此,在调用强制转换的函数指针时,this自动引用第一个基础对象,而this是第二个基础对象,打印错误的值。

PPS:更加有趣: 如果我们插入Jarod42的建议:

B

程序正确打印:

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的{​​{1}}基类this。为了在B函数中实现此目标(相反,该函数隐藏在X中),我引入了AsFunc模板参数,该参数使main参数为派生类型{{ 1}},这样Obj就必须进行添加。

答案 3 :(得分:1)

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;
}