【问题标题】:Calling C++ member functions via a function pointer通过函数指针调用 C++ 成员函数
【发布时间】:2010-12-01 22:35:21
【问题描述】:

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

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

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

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

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

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

【问题讨论】:

  • 我还是不太明白'为什么'如果你想调用一个对象的成员函数然后简单地传递一个指向对象的指针?如果人们抱怨因为它使您能够更好地封装类,为什么不创建一个所有类都继承自的接口类?
  • 虽然很多人会使用 boost::function 来隐藏原始成员指针机制,但它在实现命令模式之类的东西时很有用。
  • 为什么动态分配那条狗?然后,您也必须手动删除该对象。这看起来很像您来自 Java、C# 或其他类似的语言,但仍在使用 C++。一个普通的自动对象 (Dog dog;) 更有可能是您想要的。
  • @Chad:我基本同意,但有时传递参考的成本会更高。考虑一个在某种类型的数据(解析、计算等)上迭代的循环,而不是能够基于某些 if/else 计算调用函数会带来成本,其中仅调用指向的函数可以避免这种 if/then /else 检查是否可以在进入循环之前完成这些检查。

标签: c++ function-pointers class-method


【解决方案1】:

阅读this了解详情:

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

【讨论】:

  • 令人惊讶的是,他们决定这样做:*this.*pt2Member 会起作用。 *.* 具有更高的优先级...就我个人而言,我仍然会写 this-&gt;*pt2Member,这是少了一个运算符。
  • 为什么一定要把pt2ConstMember初始化为NULL
  • @AlexisWilke 为什么会令人惊讶?对于直接对象(不是指针),它是(object.*method_pointer),所以我们希望* 具有更高的优先级。
  • @TomášZato,如果我没记错的话(我可能是这样),this 只是用来证明无论你应用 .* 应该是一个指向(子)类。但是,这对我来说是新语法,我只是根据此处链接的其他答案和资源进行猜测。我建议进行编辑以使其更加清晰。
  • 哦,快,恭喜你 100!
【解决方案2】:

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

typedef 开始是最简单的。对于成员函数,在类型声明中添加类名:

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

然后要调用该方法,请使用-&gt;* 运算符:

(pDog->*pBark)();

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

我不相信你可以使用这样的构造函数——ctors 和 dtors 是特殊的。实现这种事情的正常方法是使用工厂方法,它基本上只是一个为您调用构造函数的静态函数。示例见下面的代码。

我已经修改了您的代码,基本上按照您的描述进行。下面有一些注意事项。

#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 库和其他相关模块非常有用。

【讨论】:

  • 我使用 C++ 已经 10 多年了,并且不断地学习新东西。我以前从未听说过-&gt;*,但现在我希望我永远不需要它:)
【解决方案3】:

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

指向函数的成员指针不仅仅是函数指针。在实现方面,编译器不能使用简单的函数地址,因为通常在知道要取消引用哪个对象(想想虚函数)之前,您不知道要调用的地址。当然,您还需要知道对象才能提供 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方式中找到函数指针。

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

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

【讨论】:

  • 'myfunction' 不是一个正常的函数,如果你通常指的是 C 风格的函数。 'myfunction' 更准确地称为 myclass 的方法。类的方法与普通函数不同,因为它们具有 C 样式函数所没有的东西,即“this”指针。
  • 建议使用 boost 是严厉的。使用方法指针有实际的充分理由。我不介意提到 boost 作为替代方案,但讨厌有人说其他人应该在不了解所有事实的情况下使用它。提升是有代价的!如果这是一个嵌入式平台,那么它可能不是一个可能的选择。除此之外,我真的很喜欢你的文章。
  • @Eric - 关于你的第二点,我并不打算说“你应该使用 Boost”,事实上我自己从未使用过 Boost。目的(据我所知,3 年后)是人们应该寻找替代品,并列出一些可能性。 “或其他”表示列表并非详尽无遗。成员指针的可读性是有代价的。它们简洁的源代码表示还可以掩盖运行时成本——特别是指向方法的成员指针必须同时处理非虚拟和虚拟方法,并且必须知道哪个。
  • @Eric - 不仅如此,这些问题也是成员指针不可移植的一个原因 - 至少在过去,Visual C++ 需要一些关于如何表示成员指针类型的额外线索。我会为嵌入式系统使用静态函数方法 - 指针的表示与任何其他函数指针相同,成本显而易见,并且不存在可移植性问题。并且由静态成员函数包装的调用(在编译时)知道调用是否是虚拟的 - 除了虚拟方法的常规 vtable 查找之外,不需要运行时检查。
  • @Eric - 关于你的第一点 - 我知道静态成员函数与 C 样式函数不完全相同(因此“抛开范围问题”),但我可能应该已包含名称。
【解决方案4】:
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference

【讨论】:

  • 应该是:(pDog->*doSomething)(); // 如果 pDog 是指针 // (pDog.*doSomething)(); // 如果 pDog 是一个引用,因为 () 运算符具有更高的优先级然后 ->* 和 .*.
【解决方案5】:

最小的可运行示例

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 标准

.*-&gt;* 是 C++ 中为此目的引入的单个运算符,而 C 中不存在。

C++11 N3337 standard draft:

  • 2.13 “运算符和标点符号”有一个所有运算符的列表,其中包含.*-&gt;*
  • 5.5 “指向成员的运算符”解释了它们的作用

【讨论】:

    【解决方案6】:

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

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

    【讨论】:

      【解决方案7】:

      指向类成员的函数指针是一个非常适合使用 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】:

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

        要调用成员函数,你需要知道两件事:

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

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

        【讨论】:

        • 我对此投了赞成票,但会添加一个澄清点,以防 OP 不知道您所指的“哪个实例”。我将展开解释固有的“this”指针。
        【解决方案9】:

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

        【讨论】:

        • 克隆可能很昂贵,如果性能是一个问题或某些问题,OP 可能希望避免它们。
        【解决方案10】:

        我用 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,但我还没有完全弄清楚如何存储一个成员函数指针,并从对所调用的类一无所知的代码中调用它。睫毛的答案看起来很有趣..

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-08-24
          相关资源
          最近更新 更多