14

我已经阅读了这篇文章,我从中得到的是,当你想调用一个指向成员函数的指针时,你需要一个实例(指向一个或堆栈引用的指针)并这样调用它:

(instance.*mem_func_ptr)(..)
or
(instance->*mem_func_ptr)(..)

我的问题是基于这个:既然你实例,为什么不直接调用成员函数,像这样:

instance.mem_func(..) //or: instance->mem_func(..)

指向成员函数的指针的合理/实际用途是什么?

[编辑]

我正在玩 X-development 并达到了我正在实现小部件的阶段;当事件到达时,用于将 X 事件转换为我的类和小部件的事件循环线程需要为每个小部件/窗口启动线程;为了正确地做到这一点,我认为我需要指向我的类中的事件处理程序的函数指针。

不是这样:我发现我可以通过简单地使用虚拟基类以更清晰、更整洁的方式做同样的事情。不需要指向成员函数的指针。正是在开发上述内容时,出现了对成员函数指针的实际可用性/意义的怀疑。

您需要一个实例的引用才能使用成员函数指针这一简单的事实已经过时了。

[编辑-@sbi & 其他]

这是一个示例程序来说明我的观点:(特别注意'Handle_THREE()')

#include <iostream>
#include <string>
#include <map>


//-----------------------------------------------------------------------------
class Base
{
public:
    ~Base() {}
    virtual void Handler(std::string sItem) = 0;
};

//-----------------------------------------------------------------------------
typedef void (Base::*memfunc)(std::string);

//-----------------------------------------------------------------------------
class Paper : public Base
{
public:
    Paper() {}
    ~Paper() {}
    virtual void Handler(std::string sItem) { std::cout << "Handling paper\n"; }
};

//-----------------------------------------------------------------------------
class Wood : public Base
{
public:
    Wood() {}
    ~Wood() {}
    virtual void Handler(std::string sItem) { std::cout << "Handling wood\n"; }
};


//-----------------------------------------------------------------------------
class Glass : public Base
{
public:
    Glass() {}
    ~Glass() {}
    virtual void Handler(std::string sItem) { std::cout << "Handling glass\n"; }
};

//-----------------------------------------------------------------------------
std::map< std::string, memfunc > handlers;
void AddHandler(std::string sItem, memfunc f) { handlers[sItem] = f; }

//-----------------------------------------------------------------------------
std::map< Base*, memfunc > available_ONE;
void AddAvailable_ONE(Base *p, memfunc f) { available_ONE[p] = f; }

//-----------------------------------------------------------------------------
std::map< std::string, Base* > available_TWO;
void AddAvailable_TWO(std::string sItem, Base *p) { available_TWO[sItem] = p; }

//-----------------------------------------------------------------------------
void Handle_ONE(std::string sItem)
{
    memfunc f = handlers[sItem];
    if (f)
    {
        std::map< Base*, memfunc >::iterator it;
        Base *inst = NULL;
        for (it=available_ONE.begin(); ((it != available_ONE.end()) && (inst==NULL)); it++)
        {
            if (it->second == f) inst = it->first;
        }
        if (inst) (inst->*f)(sItem);
        else std::cout << "No instance of handler for: " << sItem << "\n";
    }
    else std::cout << "No handler for: " << sItem << "\n";
}

//-----------------------------------------------------------------------------
void Handle_TWO(std::string sItem)
{
    memfunc f = handlers[sItem];
    if (f)
    {
        Base *inst = available_TWO[sItem];
        if (inst) (inst->*f)(sItem);
        else std::cout << "No instance of handler for: " << sItem << "\n";
    }
    else std::cout << "No handler for: " << sItem << "\n";
}

//-----------------------------------------------------------------------------
void Handle_THREE(std::string sItem)
{
    Base *inst = available_TWO[sItem];
    if (inst) inst->Handler(sItem);
    else std::cout << "No handler for: " << sItem << "\n";
}


//-----------------------------------------------------------------------------
int main()
{
    Paper p;
    Wood w;
    Glass g;


    AddHandler("Paper", (memfunc)(&Paper::Handler));
    AddHandler("Wood", (memfunc)(&Wood::Handler));
    AddHandler("Glass", (memfunc)(&Glass::Handler));

    AddAvailable_ONE(&p, (memfunc)(&Paper::Handler));
    AddAvailable_ONE(&g, (memfunc)(&Glass::Handler));

    AddAvailable_TWO("Paper", &p);
    AddAvailable_TWO("Glass", &g);

    std::cout << "\nONE: (bug due to member-function address being relative to instance address)\n";
    Handle_ONE("Paper");
    Handle_ONE("Wood");
    Handle_ONE("Glass");
    Handle_ONE("Iron");

    std::cout << "\nTWO:\n";
    Handle_TWO("Paper");
    Handle_TWO("Wood");
    Handle_TWO("Glass");
    Handle_TWO("Iron");

    std::cout << "\nTHREE:\n";
    Handle_THREE("Paper");
    Handle_THREE("Wood");
    Handle_THREE("Glass");
    Handle_THREE("Iron");
}

{edit]上面示例中直接调用的潜在问题
在 Handler_THREE() 中,方法的名称必须是硬编码的,强制在使用它的任何地方进行更改,以将任何更改应用于方法。使用指向成员函数的指针唯一要做的额外更改是创建指针的位置。

[编辑]从答案中收集到的实际用途:来自 Chubsdad 的回答

什么
专用的“调用者”函数用于调用 mem-func-ptr;
好处:使用其他对象提供的函数来保护代码
如何:如果特定函数在许多地方使用并且名称和/或参数发生变化,那么您只需将其分配的名称更改为指针,并在“调用者”函数中调整调用。(如果该函数用作 instance.function() 则必须在任何地方进行更改。)

来自Matthew Flaschen 的回答
什么:类中的局部特
化 好处:使代码更清晰、更简单、更易于使用和维护
如何:用(可能)大型 switch()/if 替换通常使用复杂逻辑实现的代码-then 直接指向特化的语句;与上面的“调用者”功能非常相似。

4

12 回答 12

12

使用任何函数指针的原因相同:您可以使用任意程序逻辑在调用函数指针变量之前对其进行设置。您可以使用开关、if/else,将其传递给函数,等等。

编辑:

问题中的示例确实表明您有时可以使用虚函数来替代指向成员函数的指针。这应该不足为奇,因为编程中通常有多种方法。

这是一个虚函数可能没有意义的例子。就像 OP 中的代码一样,这是为了说明,而不是特别现实。它显示了一个具有公共测试功能的类。这些使用内部的私有函数。内部函数只能在 setup 之后调用,并且之后必须调用 teardown。

#include <iostream>

class MemberDemo;
typedef void (MemberDemo::*MemberDemoPtr)();

class MemberDemo
{
    public:
    void test1();
    void test2();

    private:
    void test1_internal();
    void test2_internal();
    void do_with_setup_teardown(MemberDemoPtr p);
};

void MemberDemo::test1()
{
    do_with_setup_teardown(&MemberDemo::test1_internal);
}

void MemberDemo::test2()
{
    do_with_setup_teardown(&MemberDemo::test2_internal);
}

void MemberDemo::test1_internal()
{
    std::cout << "Test1" << std::endl;
}

void MemberDemo::test2_internal()
{
    std::cout << "Test2" << std::endl;
}

void MemberDemo::do_with_setup_teardown(MemberDemoPtr mem_ptr)
{
    std::cout << "Setup" << std::endl;
    (this->*mem_ptr)();
    std::cout << "Teardown" << std::endl;
}

int main()
{
    MemberDemo m;
    m.test1();
    m.test2();
}
于 2010-10-18T07:53:46.173 回答
8

我的问题是基于此:既然你有实例,为什么不直接调用成员函数[?]

前期:在超过 15 年的 C++ 编程中,我使用过成员指针可能两次或三次。有了虚拟功能,它就没有那么多用处了。

如果您想在一个对象(或多个对象)上调用某个成员函数,并且您必须决定要调用哪个成员函数,然后才能找出要在哪个对象上调用它,您会使用它们。是一个想要这样做的人的例子。

于 2010-10-18T08:03:25.187 回答
4

当您查看更高级别的构造(例如boost::bind(). 这将允许您将函数调用包装为一个对象,该对象可以稍后绑定到特定对象实例,然后作为可复制对象传递。这是一个非常强大的习惯用法,它允许延迟回调、委托和复杂的谓词操作。有关示例,请参阅我之前的帖子:

https://stackoverflow.com/questions/1596139/hidden-features-and-dark-corners-of-stl/1596626#1596626

于 2010-10-18T08:28:00.050 回答
4

成员函数,就像许多函数指针一样,充当回调函数。您可以通过创建一些调用您的方法的抽象类来管理没有它们,但这可能是很多额外的工作。

一种常见的用途是算法。在 std::for_each 中,我们可能想要调用集合中每个成员的类的成员函数。我们也可能想在集合的每个成员上调用我们自己类的成员函数——后者需要 boost::bind 来实现,前者可以用 STL mem_fun 系列类来完成(如果我们没有shared_ptr 的集合,在这种情况下我们也需要 boost::bind)。我们还可以在某些查找或排序算法中使用成员函数作为谓词。(这消除了我们编写一个重载 operator() 来调用我们类的成员的自定义类的需要,我们只需将它直接传递给 boost::bind)。

正如我所提到的,另一种用途是回调,通常在事件驱动的代码中。当一个操作完成时,我们希望调用我们类的一个方法来处理完成。这通常可以包装到 boost::bind 仿函数中。在这种情况下,我们必须非常小心地正确管理这些对象的生命周期和它们的线程安全性(尤其是如果出现问题,很难调试)。尽管如此,它还是可以让我们免于编写大量的“包装”代码。

于 2010-10-18T09:31:32.567 回答
3

有很多实际用途。我想到的一个如下:

假设一个核心函数如下(适当定义了 myfoo 和 MFN)

void dosomething(myfoo &m, MFN f){   // m could also be passed by reference to 
                                     // const
   m.*f();
}

在存在指向成员函数的指针的情况下,这样的函数对扩展开放,对修改关闭(OCP

另请参阅安全布尔成语,它巧妙地使用指向成员的指针。

于 2010-10-18T07:56:19.553 回答
3

指向成员函数的指针的最佳用途是打破依赖关系。

需要指向成员函数的指针的好例子是订阅者/发布者模式:

http://en.wikipedia.org/wiki/Publish/subscribe

于 2010-10-18T08:51:14.023 回答
3

在我看来,原始形式的成员函数指针对普通程序员来说并不是非常有用。OTOH,::std::tr1::function将成员函数指针与指向它们应该操作的对象的指针一起包装的结构非常有用。

当然::std::tr1::function是非常复杂的。所以我会给你一个简单的例子,如果你有的话,你实际上不会在实践中使用它::std::tr1::function

// Button.hpp
#include <memory>

class Button {
 public:
   Button(/* stuff */) : hdlr_(0), myhandler_(false) { }
   ~Button() {
      // stuff
      if (myhandler_) {
         delete hdlr_;
      }
   }
   class PressedHandler {
    public:
      virtual ~PressedHandler() = 0;

      virtual void buttonPushed(Button *button) = 0;
   };

   // ... lots of stuff

   // This stores a pointer to the handler, but will not manage the
   // storage.  You are responsible for making sure the handler stays
   // around as long as the Button object.
   void setHandler(const PressedHandler &hdlr) {
      hdlr_ = &hdlr;
      myhandler_ = false;
   }

   // This stores a pointer to an object that Button does not manage.  You
   // are responsible for making sure this object stays around until Button
   // goes away.
   template <class T>
   inline void setHandlerFunc(T &dest, void (T::*pushed)(Button *));

 private:
   const PressedHandler *hdlr_;
   bool myhandler_;

   template <class T>
   class PressedHandlerT : public Button::PressedHandler {
    public:
      typedef void (T::*hdlrfuncptr_t)(Button *);

      PressedHandlerT(T *ob, hdlrfuncptr_t hdlr) : ob_(ob), func_(hdlr) { }
      virtual ~PressedHandlerT() {}

      virtual void buttonPushed(Button *button) { (ob_->*func_)(button); }

    private:
      T * const ob_;
      const hdlrfuncptr_t func_;
   };
};

template <class T>
inline void Button::setHandlerFunc(T &dest, void (T::*pushed)(Button *))
{
   PressedHandler *newhandler = new PressedHandlerT<T>(&dest, pushed);
   if (myhandler_) {
      delete hdlr_;
   }
   hdlr_ = newhandler;
   myhandler_ = true;
}

// UseButton.cpp
#include "Button.hpp"
#include <memory>

class NoiseMaker {
 public:
   NoiseMaker();
   void squee(Button *b);
   void hiss(Button *b);
   void boo(Button *b);

 private:
   typedef ::std::auto_ptr<Button> buttonptr_t;
   const buttonptr_t squeebutton_, hissbutton_, boobutton_;
};


NoiseMaker::NoiseMaker()
     : squeebutton_(new Button), hissbutton_(new Button), boobutton_(new Button)
{
   squeebutton_->setHandlerFunc(*this, &NoiseMaker::squee);
   hissbutton_->setHandlerFunc(*this, &NoiseMaker::hiss);
   boobutton_->setHandlerFunc(*this, &NoiseMaker::boo);
}

假设Button在库中并且您无法更改,我很乐意看到您使用虚拟基类干净利落地实现该功能,而无需借助 aswitch或在if else if某处构造。

于 2010-10-18T09:06:20.303 回答
3

指向成员函数类型的指针的全部意义在于它们充当引用特定方法的运行时方式。当您使用“常规”语法进行方法访问时

object.method();
pointer->method();

该部分是您要调用的方法method的固定编译时规范。它被硬编码到您的程序中。它永远无法改变。但是通过使用指向成员函数类型的指针,您可以将固定部分替换为变量,该变量在方法的运行时规范中可以更改。

为了更好地说明这一点,让我做以下简单的类比。假设你有一个数组

int a[100];

您可以使用固定的编译时索引访问其元素

a[5]; a[8]; a[23];

在这种情况下,特定索引被硬编码到您的程序中。但是您也可以使用运行时索引访问数组的元素 - 一个整数变量i

a[i];

的值i不是固定的,它可以在运行时更改,从而允许您在运行时选择数组的不同元素。这与指向成员函数类型的指针让您执行的操作非常相似。

你问的问题(“既然你有实例,为什么不直接调用成员函数”)可以翻译成这个数组上下文。您基本上是在问:“当我们有像anda[i]这样的直接编译时常量访问时,为什么我们需要变量索引访问?” 我希望你知道这个问题的答案,并意识到特定数组元素的运行时选择的价值。a[1]a[3]

这同样适用于指向成员函数类型的指针:它们再次允许您执行特定类方法的运行时选择。

于 2010-10-19T02:20:57.360 回答
2

用例是您有几个具有相同签名的成员方法,并且您想要构建在给定情况下应该调用的逻辑。这有助于实现状态机算法。

不是你每天都用的东西...

于 2010-10-18T07:55:54.687 回答
2

想象一下,您有一个函数可以根据传递的参数调用几个不同函数中的一个。

你可以使用一个巨大的 if/else if 语句
你可以使用一个 switch 语句
或者你可以使用一个函数指针表(一个跳转表)

如果您有很多不同的选项,跳转表可能是一种更简洁的代码排列方式......

不过,这取决于个人喜好。无论如何,switch语句和跳转表都或多或少对应于相同的编译代码:)

于 2010-10-18T07:56:51.357 回答
2

会员指针+模板=纯赢。

例如,如何在编译时判断类是否包含某个成员函数

或者

template<typename TContainer,
         typename TProperty,
         typename TElement = decltype(*Container().begin())>
TProperty grand_total(TContainer& items, TProperty (TElement::*property)() const)
{
   TProperty accum = 0;
   for( auto it = items.begin(), end = items.end(); it != end; ++it) {
       accum += (it->*property)();
   }
   return accum;
}

auto ship_count = grand_total(invoice->lineItems, &LineItem::get_quantity);
auto sub_total = grand_total(invoice->lineItems, &LineItem::get_extended_total);
auto sales_tax = grand_total(invoice->lineItems, &LineItem::calculate_tax);
于 2010-10-19T02:38:24.620 回答
1

要调用它,您需要对实例的引用,但是您可以直接调用 func 并且不需要指向它的指针。

这完全没有抓住重点。这里有两个独立的问题:

  • 在以后的某个时间点采取什么行动
  • 对什么对象执行该操作

引用实例满足第二个要求。指向成员函数的指针解决了第一个问题:它们是一种非常直接的方式来记录 - 在程序执行的某个时刻 - 在执行的某个后期阶段应该采取哪些行动,可能由程序的另一部分执行。

例子

假设你有一只可以亲吻或挠人的猴子。在下午 6 点,您的程序应该让猴子放松,并且知道猴子应该访问谁,但是在下午 3 点左右,您的用户将输入应该采取的行动。

初学者的方法

所以,在下午 3 点你可以设置一个变量“enum Action { Kiss, Tickle } action;”,然后在下午 6 点你可以做类似“if (action == Kiss) monkey->kiss(person); else monkey->tickle (人)”。

问题

但是引入了额外的编码级别(引入了 Action 类型来支持这一点 - 可以使用内置类型,但更容易出错并且本质上意义不大)。然后 - 在确定应该在下午 3 点采取什么行动之后,在下午 6 点,您必须重复查阅该编码值来决定要采取哪个行动,这将需要另一个 if/else 或打开编码值。这一切都很笨拙、冗长、缓慢且容易出错。

成员函数指针

更好的方法是使用更专业的变量——成员函数指针——直接记录下午 6 点要执行的操作。这就是成员函数指针。这是一个较早设置的接吻或挠痒痒选择器,为猴子创建一个“状态”——它是挠痒痒的还是接吻的——可以稍后使用。后面的代码只是调用任何已设置的函数,而不必考虑可能性或有任何 if/else-if 或 switch 语句。

要调用它,您需要对实例的引用,但是您可以直接调用 func 并且不需要指向它的指针。

回到这个。因此,如果您决定在编译时采取何种操作(即程序中的点 X,肯定会很麻烦),这很好。函数指针适用于您不确定并希望将操作设置与这些操作的调用分离的情况。

于 2010-10-18T09:05:18.200 回答