代码之家  ›  专栏  ›  技术社区  ›  slashmais

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

  •  13
  • slashmais  · 技术社区  · 14 年前

    我看完了 this article ,我从中得出的结论是,当您要调用指向成员函数的指针时,需要一个实例(指向一个实例的指针或堆栈引用),并这样调用它:

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

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

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

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

    [编辑]

    我正在玩x-development,已经到了实现小部件的阶段;将x-events转换为类的事件循环线程&widget需要在每个小部件/窗口的事件到达时启动线程;要正确执行此操作,我认为需要指向类中事件处理程序的函数指针。

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

    简单的事实是,为了使用成员函数指针,需要引用一个实例,这就取消了对实例的引用。

    [编辑-@sbi&others]

    下面是一个示例程序来说明我的观点: (特别注意“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");
    }
    

    {编辑〕 上述示例中直接调用的潜在问题 :
    在handler_3()中,方法的名称必须硬编码,强制在使用它的任何地方进行更改,以便对该方法应用任何更改。使用指向成员函数的指针时,唯一要做的附加更改是创建指针的位置。

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

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

    answer by Matthew Flaschen :
    什么:一个班的本地专业
    好处:使代码更清晰、更简单、更易于使用和维护
    如何:用大型switch()/if-then语句替换通常使用复杂逻辑实现的代码,这些语句带有指向专门化的直接指针;与上面的“caller”函数非常相似。

    13 回复  |  直到 13 年前
        1
  •  3
  •   Chubsdad    14 年前

    有许多实际用途。我想到的是:

    假设核心功能如下(适当定义myfoo和mfn)

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

    在指向成员函数的指针存在的情况下,这样的函数将打开进行扩展,并关闭进行修改。( OCP )

    也指 Safe bool idiom 它巧妙地使用指向成员的指针。

        2
  •  11
  •   Matthew Flaschen    14 年前

    使用任何函数指针的原因相同:在调用函数指针变量之前,可以使用任意程序逻辑来设置它。你可以使用一个开关,一个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();
    }
    
        3
  •  8
  •   Community noseratio    7 年前

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

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

    如果要对一个对象(或多个对象)调用某个成员函数,并且必须决定要调用哪个成员函数,则可以使用它们。 在找出调用它的对象之前 . Here 就是有人想这样做的一个例子。

        4
  •  4
  •   Community noseratio    7 年前

    我发现指向成员函数的指针的真正用途是在您查看更高级别的构造(如 boost::bind() . 这将允许您将函数调用包装为一个对象,该对象稍后可以绑定到特定的对象实例,然后作为可复制对象传递。这是一个非常强大的习语,允许延迟回调、委托和复杂的谓词操作。请参阅我以前的文章中的一些示例:

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

        5
  •  4
  •   CashCow    14 年前

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

    一个常见的用法是算法。在std::for_each中,我们可能希望调用集合中每个成员的类的成员函数。我们还可能希望对集合的每个成员调用自己类的成员函数-后者需要boost::bind才能实现,前者可以通过stl mem_fun类家族来实现(如果我们没有共享的_ptr集合,在这种情况下,我们也需要boost::bind)。在某些查找或排序算法中,我们也可以使用成员函数作为谓词。(这就不需要编写一个重载operator()的自定义类来调用类的成员,我们只需直接将其传递给boost::bind)。

    正如我提到的,另一个用途是回调,通常是在事件驱动的代码中。当一个操作完成后,我们需要调用类的一个方法来处理完成。这通常可以包装成boost::bind函数。在这种情况下,我们必须非常小心地管理这些对象的生命周期和它们的线程安全(特别是当发生错误时,很难进行调试)。尽管如此,它仍然可以避免我们编写大量的“包装器”代码。

        6
  •  3
  •   BЈовић    14 年前

    对成员函数指针的最佳使用是断开依赖关系。

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

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

        7
  •  3
  •   Omnifarious    14 年前

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

    当然 ::std::tr1::函数 非常复杂。所以我给你举一个简单的例子,如果你有 ::std::tr1::函数 可用:

    // 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 是在一个库中,不能被您更改,我很高兴看到您使用一个虚拟的基类,而不使用 switch if else if 在某处建造。

        8
  •  3
  •   AnT stands with Russia    14 年前

    指向成员函数类型的指针的整个指针点是它们作为 运行时间 引用特定方法的方法。当使用“常用”语法访问方法时

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

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

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

    int a[100];
    

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

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

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

    a[i];
    

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

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

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

        9
  •  2
  •   user180326    14 年前

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

    不是你每天都用的东西…

        10
  •  2
  •   Goz    14 年前

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

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

    如果您有很多不同的选项,跳转表可以是一种更干净的方式来安排您的代码…

    但这取决于个人喜好。switch语句和跳转表或多或少对应于相同的编译代码:)

        11
  •  2
  •   Community noseratio    7 年前

    成员指针+模板=纯赢。

    例如 How to tell if class contains a certain member function in compile time

    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);
    
        12
  •  1
  •   Vijay Mathew Chor-ming Lung    14 年前

    This article 描述一些指向成员函数的指针很有用的场景。

        13
  •  1
  •   Tony Delroy    14 年前

    要调用它,您需要一个对实例的引用,然后您可以调用func direct,而不需要指向它的指针。

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

    • 稍后采取什么行动
    • 对哪个对象执行该操作

    引用实例满足第二个要求。指向成员函数的指针指向第一个:它们是一种非常直接的记录方法——在程序执行的某个点上——应该在执行的某个后期阶段(可能是由程序的另一部分)执行该操作。

    例子

    比如说你有一只猴子,它能亲吻或挠人。下午6点,你的程序应该释放猴子,并知道猴子应该访问谁,但大约下午3点你的用户将键入应该采取的行动。

    初学者的方法

    所以,下午3点你可以设置一个变量“Enum action kiss,tickle action;”,然后在下午6点你可以做一些类似“if(action==kiss)monkey->kiss(person);else monkey->tickle(person)”的事情。

    问题

    但是,这引入了一个额外的编码级别(为支持这种内置类型而引入的操作类型可以被使用,但更容易出错,而且不具有内在的意义)。然后-在确定了下午3点、下午6点应该采取什么行动之后,您必须冗余地参考该编码值来决定要采取的行动,这将需要另一个if/else或打开编码值。这些都是笨拙、冗长、缓慢和容易出错的。

    成员函数指针

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

    要调用它,您需要一个对实例的引用,然后您可以调用func direct,而不需要指向它的指针。

    回到这里。所以,如果您在编译时决定要采取什么操作(即程序中的一个点X),这是很好的。函数指针用于不确定的情况,并希望将操作设置与这些操作的调用分离。