成员函数指针的实际用途是什么?

14

我已阅读了这篇文章,我的理解是当你想调用一个成员函数指针时,你需要一个实例(可以是指向实例的指针或堆栈引用),并进行如下调用:

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

我的问题基于此:既然您有该实例,为什么不直接调用成员函数,像这样:

instance.mem_func(..) //or: instance->mem_func(..)
指向成员函数的指针在实际中有什么合理/实用的用途?
[编辑]
我正在进行X开发,并到达了实现小部件的阶段;将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()中,方法名必须被硬编码,这意味着任何地方使用该方法都必须进行更改才能应用任何更改。使用成员函数指针,唯一需要进行的额外更改是创建指针的位置。

[edit] 从答案中获得的实际用途:

来自Chubsdad的答案:
什么: 使用专门的'Caller'函数来调用mem-func-ptr;
好处: 保护使用其他对象提供的功能的代码
如何: 如果特定的函数在许多地方使用且名称和/或参数发生变化,则只需更改分配指针的位置的名称,并在'Caller'-function中适应调用即可。(如果函数被用作instance.function(),则必须在所有地方进行更改。)

来自Matthew Flaschen的答案:
什么: 类中的本地特化
好处: 使代码非常清晰,简单易用且易于维护
如何: 使用直接指向特化的指针替换通常使用复杂逻辑实现的代码,这些代码具有(潜在)大型开关()/if-then语句,与上述'Caller'-function非常相似。


4
整个意思是,mem_func不一定是静态的,因此我们使用mem_func_ptr。你需要一个实例与函数指针或它们的使用无关。你正在为无关紧要的事情分心。如果你理解“普通”函数指针的用途,那么你就理解了成员函数指针的用途,完全相同。所以,你理解普通函数指针的用途吗? - GManNickG
2
@GMan:你明白我的问题吗? - slashmais
@slashmais:是的...我承认它们通常不太有用,但它们的使用方式仍然基本相同。 - GManNickG
1
我同意它们并不常用,但在需要它们时非常有用。同时,与@slashmais所说的一样,它类似于函数指针,但用于成员方法。我想我曾经在C++中使用它们来实现状态机。 - kenny
"a.b(c)" 表示的是 "a.(b(c))",而不是你想要的 "(a.b)(c)"。 "->" 同理。 - Roger Pate
@Roger Pate:正在修复,谢谢。 - slashmais
12个回答

12

与使用任何函数指针相同的原因是:在调用它之前,您可以使用任意程序逻辑来设置函数指针变量。您可以使用switch、if/else、将其传递到函数中等等。

编辑:

问题中的示例确实显示了您有时可以使用虚拟函数作为指向成员函数的替代方法。这并不奇怪,因为编程通常有多种方法。

这里是一个示例,说明虚拟函数可能不合适的情况。与OP中的代码类似,这个示例旨在说明,而不是特别现实。它展示了一个具有公共测试函数的类。它们使用内部的私有函数。只有在设置完成后才能调用内部函数,并且必须在此之后调用撤消操作。

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

在我提供的文章中,作者展示了成员函数指针的实际使用有些晦涩难懂,详见“成员函数指针的用途”一节。 - slashmais
+: 如果你想调用成员函数指针,你需要一个类实例,但你也可以直接调用成员函数。 - slashmais
@slash 说它只用于“人为的例子”和“委托”。即使如此,该文章也没有反驳委托在各种应用中的有用性。 - Matthew Flaschen
3
@slash,你可以直接调用一个自由函数。在常规函数指针和成员函数指针之间,考虑的问题其实并没有太大的区别。一个常规函数指针可以通过参数调用(假设类型正确)。你可以将实例视为额外的参数(有时称为隐式参数)。无需其他解释。 - Matthew Flaschen
1
我同意Pavel的观点,我认为这个答案没有太多价值。显而易见的后续问题是:“我为什么要这样做,以及如何使用成员函数指针?” - Omnifarious
显示剩余3条评论

8
我的问题基于这个:既然你已经有了实例,为什么不直接调用成员函数?
提前说明:在15年的C++编程中,我可能只用过两三次成员指针。由于虚函数的存在,它们并没有太多用处。
如果你想在一个对象(或多个对象)上调用某个成员函数,并且你必须在找出要调用哪个成员函数之前决定要调用哪个成员函数,则可以使用它们。这是想要这样做的人的一个示例

你的例子强调了我的观点:当迭代器提供Elem*时,你已经拥有了实例,并且现在可以直接调用方法——不需要成员指针。 - slashmais
4
请再仔细阅读一遍,我甚至标出了重点!当你需要在决定调用哪个成员函数之前就已经决定好要调用哪个对象时,你就需要使用成员函数指针。在我提供的示例中,有关函数的决策是在std::for_each()外部进行的,而与对象有关的决策则是在其内部进行的。请注意不要改变原文意思,使翻译更加通俗易懂。 - sbi
@slash,首先,实例是指向Selector的指针,而不是Elem。其次,for_each肯定不会像“直接”执行instance->prepare_elem(elem);这样的操作,因为它只知道如何执行UnaryFunction。要获取UnaryFunction,您可以首先将成员函数指针传递给mem_fun,然后将结果传递给bind1st - Matthew Flaschen
我从未直接使用过它们,但我经常通过::boost::function::std::tr1::function使用它们。 - Omnifarious
我在我的问题中添加了一个示例程序,以展示我的意思。 - slashmais

4
我发现指向成员函数的指针真正有用的地方在于,当你看到一个更高级的构造时,例如 boost::bind()。这将允许您将函数调用包装为一个对象,稍后可以将其绑定到特定的对象实例上,然后作为可复制的对象传递。这是一个非常强大的习语,允许延迟回调、委托和复杂的谓词操作。请参见我以前的帖子中的一些示例: https://stackoverflow.com/questions/1596139/hidden-features-and-dark-corners-of-stl/1596626#1596626

这是一个用途,但在那个点上你仍然可以直接调用成员 - 不需要指针; 虚基类将允许相同的操作。 - slashmais
2
其实这种方法的好处在于调用代码不需要知道要调用哪个函数,因为你是将函数作为参数传递的,例如可以看看我之前使用 find_if() 的例子。 - the_mandrill

4
成员函数,和许多函数指针一样,可以作为回调函数。如果您创建一个某个抽象类来调用方法,也可以不使用成员函数,但这可能需要很多额外的工作。
其中一个常见的用法是算法。在std :: for_each中,我们可能要调用集合中每个成员的类的成员函数。我们还可以在集合的每个成员上调用自己类的成员函数 - 后者需要通过boost :: bind实现,前者可以使用STL mem_fun系列的类来完成(如果我们没有shared_ptr集合,在这种情况下我们也需要使用boost :: bind)。我们还可以在某些查找或排序算法中使用成员函数作为谓词。(这样就省去了我们编写自定义类来重载operator()以调用我们类的成员的需求,我们直接将其传递给boost :: bind即可)
另一个用法,正如我之前提到的,是回调函数,通常用于事件驱动代码。当操作完成时,我们希望调用我们类的某个方法来处理完成。这通常可以包装为boost::bind函数对象。在这种情况下,我们必须非常小心地管理这些对象的生命周期和线程安全性(特别是如果出现问题,调试可能非常困难)。尽管如此,它仍然可以节省我们编写大量“包装器”代码的时间。

3

有许多实际应用。我想到的一个是:

假设有以下核心函数(适当定义myfoo和MFN):

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

当存在指向成员函数的指针时,这样的函数变得对扩展开放,对修改关闭(OCP)。

还可以参考安全布尔习惯用法,该方法巧妙地使用了指向成员的指针。


要调用它,您需要一个实例的引用,但然后您可以直接调用该函数而不需要指向它的指针。 - slashmais
获得OCP的关键是从核心函数中的实际函数名称中解除链接。 - Chubsdad
@slashmais:如果您将成员函数重命名为其他名称,会怎样呢?如果将其绑定到名称上,则此核心函数会更改。但是,通过我展示的方式,函数的名称并不重要。它对修改关闭,对扩展开放。 - Chubsdad
我理解你的意思。使用您的方法的好处是,如果特定函数在多个地方使用,则只需要更改指向它的指针变量名称;如果将函数用作实例.function(),则必须在所有用到该函数的地方进行更改。好的,这至少是一个明智的用法(+1)。 - slashmais
是的,这就是OCP的核心。核心功能被封闭以防修改,同时对扩展开放。 - Chubsdad
接受了这个答案,因为它是唯一一个展示成员函数指针真实且实用用途的答案。 - slashmais

3
在我看来,成员函数指针本身对于普通程序员来说并不是非常有用的。然而,将成员函数指针与它们所要操作的对象的指针一起包装在一起的类似于 ::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 是一个库,不可由您更改,我希望您利用虚基类实现它的清晰性,而不是在某处使用 switchif else if 构造来实现。

你的代码实际上给了我一些在与X-dev玩耍时使用的想法。我肯定会再仔细研究你的代码示例。 - slashmais
我之前不知道std::tr1 - 谢谢。看起来可以做很多事情 :) - slashmais

3
指向成员函数类型的指针的整个重点在于,它们作为一种运行时引用特定方法的方式。当您使用“通常”的方法访问语法时,这些指针可以派上用场。
object.method();
pointer->method();

method部分是一个固定的、编译时的方法调用说明,它被硬编码到你的程序中,永远不会改变。但是通过使用指向成员函数指针的指针,你可以用一个可变的,在运行时可以改变的方法调用说明替换掉这个固定的部分。

为了更好地说明这一点,让我举一个简单的比喻。假设你有一个数组:

int a[100];

你可以通过固定的编译时索引访问它的元素。
a[5]; a[8]; a[23];

在这种情况下,特定的索引已经硬编码到您的程序中。但是,您也可以使用运行时索引(整数变量i)访问数组元素。
a[i];
i的值不固定,它可以在运行时改变,因此允许您在运行时选择数组的不同元素。这与指向成员函数类型的指针所能做的非常相似。
您提出的问题(“既然您有实例,为什么不直接调用成员函数”)可以转化为这个数组上下文。您基本上在问:“当我们有直接的编译时常量访问,比如a[1]a[3],为什么我们需要一个变量索引访问a[i]?”我希望您知道这个问题的答案,并意识到对特定数组元素进行运行时选择的价值。
指向成员函数类型的指针也是如此:它们再次允许您执行特定类方法的运行时选择。

3

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

这个概念非常有用。 - slashmais

2

应用场景是您有几个具有相同签名的成员方法,并且您想构建逻辑以确定在给定情况下应调用哪个方法。这对于实现状态机算法很有帮助。

并不是每天都会使用的东西...


你的建议有一定的价值,但是... 关于我正在忙碌的X-dev:将事件、小部件、父窗口和在该父窗口中调用的方法链接起来,这可能是成员函数指针的一个适合的位置。 - slashmais

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