通过函数指针调用C ++类方法


113

如何获取类成员函数的函数指针,然后再使用特定对象调用该成员函数?我想写:

class Dog : Animal
{
    Dog ();
    void bark ();
}


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

另外,如果可能的话,我也想通过指针来调用构造函数:

NewAnimalFunction pNew = &Dog::Dog;
Animal* pAnimal = (*pNew)();    

这可能吗?如果可以,这样做的首选方式是什么?


1
如果您要调用对象成员函数然后仅将指针传递给该对象,我仍然不太明白“为什么”?如果人们抱怨,因为它使您可以更好地封装类,为什么不创建所有类都继承自的接口类呢?
乍得

尽管许多人会使用boost :: function来隐藏原始成员指针机制,但它在实现类似命令模式的过程中可能很有用。
CB Bailey

9
您为什么动态分配那只狗?然后,您也必须手动删除该对象。这看起来很像您来自Java,C#或其他某种可比较的语言,并且仍在使用C ++。一个普通的自动对象(Dog dog;)更可能是您想要的。
2009年

1
@乍得:我大部分都会同意,但有时候通过引用会花费更多。考虑一个循环遍历某种类型的数据(解析,计算等)的循环,而不是能够基于某些if / else计算来调用一个函数,这会带来成本,而仅调用指向的函数也可以避免这种if / then / else在进入循环之前检查是否可以进行这些检查。
埃里克(Eric)

Answers:


126

阅读这个详细:

// 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');

23
令他们惊讶的是,他们决定这样做:*this.*pt2Member会起作用。*具有更高的优先级.*。。。就我个人而言,我仍然会写this->*pt2Member,那就是少一个运算符。
Alexis Wilke

7
为什么必须初始化pt2ConstMemberNULL
西罗Santilli郝海东冠状病六四事件法轮功2015年

@AlexisWilke为什么会令人惊讶?对于直接对象(不是指针),它是(object.*method_pointer),因此我们希望的*优先级更高。
Ciro Santilli郝海东冠状病六四事件法轮功2015年

@TomášZato,如果我没有记错的话(也许是),this它只是用来说明您要应用的任何内容.*都应该是指向(子)类的实例的指针。但这对我来说是新语法,我只是根据其他答案和此处链接的资源进行猜测。我建议进行修改以使其更清晰。
c1moore

1
哦,快点,恭喜100!
乔纳森·米

57

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

以开头最简单typedef。对于成员函数,可以在类型声明中添加类名:

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

然后,使用->*操作符来调用该方法:

(pDog->*pBark)();

另外,如果可能的话,我也想通过指针来调用构造函数。这可能吗?如果可以,这样做的首选方式是什么?

我不相信您可以使用像这样的构造函数-ctor和dtor非常特殊。实现这种目标的正常方法是使用工厂方法,该方法基本上只是一个静态函数,该函数会为您调用构造函数。有关示例,请参见下面的代码。

我已经修改了您的代码,基本上可以执行您所描述的内容。以下是一些警告。

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

现在,尽管您通常可以使用a Dog*代替Animal*多态性,但是函数指针的类型并不遵循类层次结构的查找规则。因此,Animal方法指针与Dog方法指针不兼容,换句话说,您不能将a分配给Dog* (*)()type变量Animal* (*)()

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

在回答了以上问题之后,我确实想知道是否有更好的方法来实现您的需求。在某些特定情况下,您会做这种事情,但是您可能会发现还有其他模式可以更好地解决您的问题。如果您以更笼统的方式描述您要实现的目标,那么蜂巢式思维可能会变得更加有用!

与上述相关,您无疑会发现Boost绑定库和其他相关模块非常有用。


10
我已经使用C ++十多年了,并定期学习新知识。我以前从未听说->*过,但是现在我希望我永远都不需要它了:)
Thomas

31

我认为这里没有人解释过一个问题是您需要“ 成员指针 ”而不是普通的函数指针。

指向函数的成员指针不仅仅是函数指针。用实现术语来说,编译器不能使用简单的函数地址,因为通常来说,在知道要取消引用的对象之前(不知道虚函数),您不知道要调用的地址。当然,您还需要了解对象以便提供this隐式参数。

说完您需要它们之后,现在我要说您确实需要避免使用它们。严重的是,成员指针很痛苦。看达到相同目标的面向对象设计模式,或者使用boost::function上面提到的a 或其他方法,要理智得多,那就是理智得多。

如果要提供指向现有代码的函数指针,则确实需要一个简单的函数指针,则应将函数编写为类的静态成员。静态成员函数不了解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(最初是为C而不是C ++设计的)时,这种情况相当普遍。当然,在那种情况下,参数通常是LPARAM或类似参数,而不是指针,并且需要一些转换。


如果正常情况下,您是指C风格的函数,则“ myfunction”不是标准函数。“ myfunction”更准确地称为myclass方法。类的方法与普通函数不同,因为它们具有C样式函数所不具有的东西(即“ this”指针)。
埃里克

3
建议使用boost是严厉的。使用方法指针有很多实际的理由。我不介意提到boost作为替代方案,但是当有人说别人应该在不了解所有事实的情况下使用它时,我会讨厌。加速需要付出代价!而且,如果这是一个嵌入式平台,那么这可能不是一个选择。除此之外,我真的很喜欢你的文章。
埃里克(Eric)

@Eric-关于您的第二点,我无意说“您应该使用Boost”,实际上我从未使用过Boost。目的(据我三年后所知)是人们应该寻找替代品,并列举一些可能性。“或其他内容”表示列表并非详尽无遗。成员指针会增加可读性。它们的简洁的源表示形式也可以掩盖运行时的成本-特别是指向方法的成员指针必须同时处理非虚拟方法和虚拟方法,并且必须知道哪种方法。
2012年

@Eric-不仅如此,而且这些问题也是不能使用成员指针进行移植的原因-Visual C ++至少在过去需要关于如何表示成员指针类型的其他线索。对于嵌入式系统,我将使用静态函数方法-指针的表示形式与任何其他函数指针相同,开销显而易见,并且不存在可移植性问题。静态成员函数包装的调用知道(在编译时)该调用是否是虚拟的-除了对虚拟方法的常规vtable查找外,不需要运行时检查。
2012年

@Eric-在您的第一点上,我知道静态成员函数与C样式函数并不完全相同(因此,“作用域放在一边”),但是我可能应该包括该名称。
Steve314,2012年

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

2
应该是:(pDog-> * doSomething)(); //如果pDog是一个指针// //(pDog。* doSomething)(); //如果pDog是引用,因为()运算符具有更高的优先级,则-> *和。*。
Tomek

12

最小的可运行示例

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“指针到成员运算符”说明了他们的工作

11

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

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

6

之所以不能使用函数指针来调用成员函数,是因为普通的函数指针通常只是函数的内存地址。

要调用成员函数,您需要了解两件事:

  • 调用哪个成员函数
  • 应该使用哪个实例(其成员函数)

普通函数指针不能同时存储两者。C ++成员函数指针用于存储a),这就是为什么在调用成员函数指针时需要显式指定实例的原因。


1
我对此投了赞成票,但要在OP不知道“哪个实例”指的是什么的情况下,请增加一个澄清点。我将扩展解释固有的“ this”指针。
艾瑞克(Eric)

6

指向类成员的函数指针确实是一个非常适合使用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);
}

2

要创建一个新对象,您可以使用如上所述的new放置,或者让您的类实现可创建对象副本的clone()方法。然后,您可以使用成员函数指针调用此克隆方法,如上所述,以创建对象的新实例。clone的优点是,有时您可能正在使用不知道对象类型的基类的指针。在这种情况下,clone()方法可以更易于使用。另外,如果需要的话,clone()将允许您复制对象的状态。


克隆可能会很昂贵,如果性能是一个问题或引起某些关注,OP可能希望避免使用它们。
埃里克(Eric)

0

我用std :: function和std :: bind做到了。

我写了这个EventManager类,将处理程序的向量存储在unordered_map中,该映射器将事件类型(只是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来加快速度,也许使用数组数组而不是矢量的unordered_map,但我还没有弄清楚如何存储成员函数指针并从对被调用类一无所知的代码中调用它。睫毛的答案看起来很有趣。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.