如何创建一个指向成员函数的指针并调用它?

156
如何获取类成员函数的函数指针,并在稍后使用特定对象调用该成员函数?我想写成这样:
class Dog : Animal
{
    Dog ();
    void bark ();
}

…
Dog* pDog = new Dog ();
BarkFunction pBark = &Dog::bark;
(*pBark) (pDog);
…

如果可能的话,我也想通过指针调用构造函数。
NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

这个可能吗?如果可以的话,最好的方法是什么?

1
我仍然不太明白为什么如果你想调用对象的成员函数,就只需传递指向该对象的指针?如果有人抱怨说这样做使类的封装更好,为什么不创建一个所有类都继承的接口类呢? - Chad
1
它可以在实现类似命令模式的东西时非常有用,尽管许多人会使用boost::function来隐藏原始成员指针机制。 - CB Bailey
10
为什么你要动态分配那个狗对象?这样你也必须手动删除它。看起来你可能是从Java、C#或其他类似语言转到C++,还没有完全适应。一个普通的自动对象(Dog dog;)更符合你的需求。 - sbi
2
@Chad:我大多数时候会同意,但有时传递引用的成本会更高。考虑一个正在迭代某种类型数据(解析、计算等)的循环,能够基于一些if/else计算调用函数会带来成本,如果这些检查可以在进入循环之前完成,那么只需调用指向的函数即可避免这些if/then/else检查。 - Eric
1
另请参阅成员函数的函数指针 - jww
显示剩余6条评论
11个回答

165

阅读此链接了解详情:

// 1 define a function pointer and initialize to NULL

int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;

// C++

class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* more of TMyClass */
};
pt2ConstMember = &TMyClass::DoIt; // note: <pt2Member> may also legally point to &DoMore

// Calling Function using Function Pointer

(*this.*pt2ConstMember)(12, 'a', 'b');

32
他们决定使用这个:*this.*pt2Member可能会让人感到惊讶。 * 的优先级高于.*...就我个人而言,我仍然会写成this->*pt2Member,这样可以少用一个运算符。 - Alexis Wilke
8
为什么必须将 pt2ConstMember 初始化为 NULL - Ciro Santilli OurBigBook.com
1
@AlexisWilke 为什么这让人惊讶?对于直接对象(非指针),它是 (object.*method_pointer),因此我们希望 * 具有更高的优先级。 - Ciro Santilli OurBigBook.com
1
@TomášZato,如果我没有弄错的话(也许我会),this只是用来演示你应该将.*应用于指向(子)类实例的指针。然而,这对我来说是新语法,我只是根据其他答案和链接资源猜测。我建议进行编辑以使其更清晰明了。 - c1moore
我们为什么要通过 *this 来调用函数?那真的有必要吗? this->*pt2ConstMember)(12, 'a', 'b') 呢?那样可行吗?this->pt2ConstMember)(12, 'a', 'b') 呢?那样可行吗?请参见我的问题末尾的答案摘要。我相信在将其作为函数调用时,在指向 const 成员函数的指针 pt2ConstMember 前面的 * 是可选的。 - Gabriel Staples
此外,我们必须通过类或子类调用吗(我认为答案是肯定的),还是可以以独立形式调用此方法函数而不需要对象(我认为答案是否定的)? - Gabriel Staples

70

如何获取类成员函数的函数指针,并在稍后使用特定对象调用该成员函数?

最简单的方法是使用typedef。对于成员函数,您需要在类型声明中添加类名:

typedef void(Dog::*BarkFunction)(void);

然后要调用该方法,您可以使用->*运算符:

(pDog->*pBark)();

另外,如果可能的话,我也想通过指针调用构造函数。这是可能的吗?如果可以,那么最好的方法是什么?

我认为你无法像这样使用构造函数 - 构造函数和析构函数是特殊的。达到这种目的的正常方法是使用工厂模式,它基本上只是调用构造函数的静态函数。下面的代码提供了一个示例。

我修改了你的代码,以实现你基本上描述的内容。以下有一些注意事项。

#include <iostream>

class Animal
{
public:

    typedef Animal*(*NewAnimalFunction)(void);

    virtual void makeNoise()
    {
        std::cout << "M00f!" << std::endl;
    }
};

class Dog : public Animal
{
public:

    typedef void(Dog::*BarkFunction)(void);

    typedef Dog*(*NewDogFunction)(void);

    Dog () {}

    static Dog* newDog()
    {
        return new Dog;
    }

    virtual void makeNoise ()
    {
        std::cout << "Woof!" << std::endl;
    }
};

int main(int argc, char* argv[])
{
    // Call member function via method pointer
    Dog* pDog = new Dog ();
    Dog::BarkFunction pBark = &Dog::makeNoise;

    (pDog->*pBark)();

    // Construct instance via factory method
    Dog::NewDogFunction pNew = &Dog::newDog;

    Animal* pAnimal = (*pNew)();

    pAnimal->makeNoise();

    return 0;
}
现在虽然你通常可以使用一个 Dog* 代替一个 Animal*,多态的魔力使这成为可能,但函数指针的类型不遵循类层次结构的查找规则。因此,一个 Animal 方法指针与 Dog 方法指针不兼容,换句话说,你不能将 Dog* (*)() 赋给类型为 Animal* (*)() 的变量。

newDog 静态方法是一个简单的工厂示例,它只创建并返回新实例。作为静态函数,它具有常规的 typedef(没有类限定符)。

回答以上问题后,我想知道是否有更好的方法来实现你的需求。有一些特定的场景需要这样做,但你可能会发现其他模式适用于你的问题更好。如果你更一般地描述你要实现什么,智慧的群体可能会更有用!

与上述相关的是,你肯定会发现 Boost bind 库和其他相关模块非常有用。


15
我已经使用C++超过10年,并定期学习新知识。我以前从未听说过->*,但现在希望永远不需要用到它 :) - Thomas
@Thomas 你可能永远不会“需要”它,但使用它有很好的理由。例如,在模拟器中实现指令解码/分派时,使用函数指针数组并不罕见,对于一个类,通常希望这些函数指针是成员函数指针,以便捕获所有必要的上下文。 - Pharap

39

我认为没有人在这里解释过一个问题,那就是你需要 "成员指针" 而不是普通函数指针。

函数的成员指针并不仅仅是函数指针。在实现方面,编译器不能使用简单的函数地址,因为一般来说,在你不知道要引用哪个对象时你不知道要调用哪个地址(想想虚函数)。当然,你还需要知道对象以提供“this”隐式参数。

话虽如此,你确实需要它们,现在我会说你真的需要避免它们。说真的,成员指针很麻烦。更明智的做法是查看实现相同目标的面向对象设计模式,或者像上面提到的使用 boost::function 或其他东西 - 当然前提是你能做出这个选择。

如果你正在向现有代码提供该函数指针,所以你真的需要一个简单的函数指针,你应该写一个类的静态成员函数。静态成员函数不理解“this”,所以你需要将对象作为显式参数传递进去。曾经有一个不那么不寻常的习惯用法,就是用这种方式处理需要函数指针的旧 C 代码。

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

由于myfunction实际上只是一个普通的函数(除了作用域问题),因此可以按照普通的C方式找到函数指针。

编辑 - 这种方法称为“类方法”或“静态成员函数”。与非成员函数的主要区别在于,如果您从类外引用它,则必须使用::作用域解析运算符指定作用域。例如,要获取函数指针,请使用&myclass::myfunction,要调用它,请使用myclass::myfunction(arg)

在使用旧的Win32 API时,这种情况相当常见,该API最初是为C而设计的,而不是C ++。当然,在这种情况下,参数通常是LPARAM或类似的指针,需要进行一些转换。


如果你所说的“正常函数”是指C风格函数,那么'myfunction'不是一个正常函数。更准确地说,'myfunction'被称为'myclass'的一个方法。类的方法与普通函数不同,因为它们具有C风格函数所没有的'this'指针。 - Eric
3
建议使用boost是非常严厉的做法。使用方法指针有实际的好处。我不介意提到boost作为另一种选择,但讨厌有人在不知道所有事实的情况下说别人应该使用它。Boost有代价!如果这是一个嵌入式平台,那么可能无法选择boost。除此之外,我真的很喜欢你的写作风格。 - Eric
@Eric - 在你的第二点上,我并不是想说“必须使用Boost”,事实上我自己从未使用过Boost。我的意图(至少在3年后)是让人们寻找替代方案,并列举一些可能性。“或者其他什么”表示列表并不是要穷尽所有可能。成员指针会影响可读性。它们简洁的源代码表示也可能掩盖运行时成本——特别是成员指向方法的指针必须处理非虚拟和虚拟方法,并且必须知道哪个是哪个。 - user180247
@Eric - 不仅如此,这些问题还是成员指针不可移植的原因之一 - 至少在过去,Visual C++ 需要一些额外的线索来表示成员指针类型。对于嵌入式系统,我会使用静态函数方法 - 指针的表示与任何其他函数指针相同,成本是显而易见的,并且没有可移植性问题。并且由静态成员函数包装的调用知道(在编译时)调用是否为虚拟的 - 除了虚拟方法的常规 vtable 查找之外,不需要运行时检查。 - user180247
@Eric - 在你的第一点上 - 我知道静态成员函数并不完全等同于 C 风格函数(因此“作用域问题除外”),但我可能应该包括名称。 - user180247

21

最小化可运行的示例

main.cpp

#include <cassert>

class C {
    public:
        int i;
        C(int i) : i(i) {}
        int m(int j) { return this->i + j; }
};

int main() {
    // Get a method pointer.
    int (C::*p)(int) = &C::m;

    // Create a test object.
    C c(1);
    C *cp = &c;

    // Operator .*
    assert((c.*p)(2) == 3);

    // Operator ->*
    assert((cp->*p)(2) == 3);
}

编译并运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

在Ubuntu 18.04中测试过。

您不能更改括号的顺序或省略它们。以下方法不起作用:

c.*p(2)
c.*(p)(2)

GCC 9.2在此处无法通过:

main.cpp: In function ‘int main()’:
main.cpp:19:18: error: must use ‘.*’ or ‘->*’ to call pointer-to-member function in ‘p (...)’, e.g. ‘(... ->* p) (...)’
   19 |     assert(c.*p(2) == 3);
      |

C++11标准

.*->*是C++引入的用于此目的的单一运算符,在C语言中不存在。

C++11 N3337标准草案

  • 2.13 "运算符和标点符号" 列出了所有的运算符,其中包括了 .*->*
  • 5.5 "成员指针运算符" 解释了它们的作用。

21
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference

2
如果pDog是一个指针,则使用(pDog->doSomething)(); 如果pDog是一个引用,则使用(pDog.doSomething)(); 这是因为()运算符的优先级高于 -> 和 .. - Tomek

16

我来这里学习如何从方法中创建一个函数指针(而不是方法指针),但这里的答案都没有提供解决方案。以下是我想出的解决方法:

template <class T> struct MethodHelper;
template <class C, class Ret, class... Args> struct MethodHelper<Ret (C::*)(Args...)> {
    using T = Ret (C::*)(Args...);
    template <T m> static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

#define METHOD_FP(m) MethodHelper<decltype(m)>::call<m>

所以针对您的例子,现在您需要执行以下操作:
Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

编辑:
使用 C++17,有一个更好的解决方案:

template <auto m> struct MethodHelper;
template <class C, class Ret, class... Args, Ret (C::*m)(Args...)> struct MethodHelper<m> {
    static Ret call(C* object, Args... args) {
        return (object->*m)(args...);
    }
};

这可以直接使用,无需宏。

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

对于带有像 const 这样的修饰符的方法,您可能需要一些更多的专业知识,例如:

template <class C, class Ret, class... Args, Ret (C::*m)(Args...) const> struct MethodHelper<m> {
    static Ret call(const C* object, Args... args) {
        return (object->*m)(args...);
    }
};

9

类成员的函数指针问题很适合使用boost::function。以下是一个小例子:

#include <boost/function.hpp>
#include <iostream>

class Dog 
{
public:
   Dog (int i) : tmp(i) {}
   void bark ()
   {
      std::cout << "woof: " << tmp << std::endl;
   }
private:
   int tmp;
};



int main()
{
   Dog* pDog1 = new Dog (1);
   Dog* pDog2 = new Dog (2);

   //BarkFunction pBark = &Dog::bark;
   boost::function<void (Dog*)> f1 = &Dog::bark;

   f1(pDog1);
   f1(pDog2);
}

8
普通的函数指针通常只是函数的内存地址,所以不能用函数指针调用成员函数。
要调用成员函数,需要知道两件事情: 1. 调用哪个成员函数 2. 使用哪个实例(成员函数属于哪个实例)
普通的函数指针无法同时保存这两个信息。C++成员函数指针用于保存第一项,所以当调用成员函数指针时需要显式指定实例。

1
我点赞了这个,但是如果OP不知道你所指的“哪个实例”,我会添加一个澄清点。我会扩展说明内在的“this”指针。 - Eric

2
为了创建一个新的对象,你可以使用上面提到的放置new(placement new),或者让你的类实现一个clone()方法来创建对象的副本。然后,你可以像上面解释的那样使用成员函数指针调用这个clone方法来创建对象的新实例。clone的优点在于,有时你可能会使用指向基类的指针,而不知道对象的类型。在这种情况下,使用clone()方法可能更容易。此外,如果需要,clone()方法还可以让你复制对象的状态。

克隆可能很昂贵,如果性能是一个问题或者有一些顾虑的话,原始发布者可能希望避免使用它们。 - Eric

2
我使用了std::function和std::bind实现了这个功能。
我编写了一个EventManager类,它将处理程序存储在无序映射中,并将事件类型(只是const unsigned int,在命名空间范围内有一个大的枚举)映射到该事件类型的处理程序向量。
在我的EventManagerTests类中,我设置了一个事件处理程序,如下所示:
auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

这是AddEventListener函数:

std::vector<EventHandler>::iterator EventManager::AddEventListener(EventType _event_type, EventHandler _handler)
{
    if (listeners_.count(_event_type) == 0) 
    {
        listeners_.emplace(_event_type, new std::vector<EventHandler>());
    }
    std::vector<EventHandler>::iterator it = listeners_[_event_type]->end();
    listeners_[_event_type]->push_back(_handler);       
    return it;
}

以下是 EventHandler 类型定义:

typedef std::function<void(Event *)> EventHandler;

然后在EventManagerTests :: RaiseEvent中,我这样做:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

这是EventManager::RaiseEvent的代码:
void EventManager::RaiseEvent(EventType _event_type, Event * _event)
{
    if (listeners_.count(_event_type) > 0)
    {
        std::vector<EventHandler> * vec = listeners_[_event_type];
        std::for_each(
            begin(*vec), 
            end(*vec), 
            [_event](EventHandler handler) mutable 
            {
                (handler)(_event);
            }
        );
    }
}

这个方法有效。我在EventManagerTests :: OnKeyDown中收到了调用。在清理时,我必须删除向量,但是一旦我这样做,就没有泄漏。在我的计算机上,引发事件大约需要5微秒,那是2008年左右。虽然不是非常快,但只要我知道这一点并且不在超热代码中使用它,就足够公平。

我想通过编写自己的std :: function和std :: bind以及使用数组而不是向量的无序映射来加速它,但我还没有完全弄清楚如何存储成员函数指针并从不知道被调用的类的代码中调用它。Eyelash的答案看起来非常有趣。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接