Каково практическое использование указателей на функции-члены?

Я прочитал эту статью, и я понял, что когда вы хотите вызвать указатель на функцию-член, вам нужен экземпляр (либо указатель на одну из них, либо ссылка на стек), и вызовите его так:

(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() имя метода должно быть жестко запрограммировано, заставляя вносить изменения везде, где он используется, чтобы применить любое изменение к методу. Используя указатель на функцию-член, единственное дополнительное изменение - это создание указателя.

[править] Практическое использование почерпнуло из ответов:

Из ответа Чубсдада:
Что: Специальная функция 'Caller' используется для вызова mem-func-ptr;
Преимущество: защита кода с использованием функций, предоставляемых другими объектами.
Как: если определенная функция (функции) используется во многих местах, и имя и / или параметры меняются, то вам нужно только изменить имя, в котором оно расположено в качестве указателя, и адаптировать вызов в функции 'Caller'. (Если функция используется в качестве instance.function(), ее необходимо изменить везде.)

Из ответа Мэтью Флашена:
Что: Локальная специализация в классе
Преимущество: делает код намного понятнее, проще и проще в использовании и обслуживании
Как: заменяет код, который обычно реализуется с использованием сложной логики, с (потенциально) большими операторами switch()/if-then с прямыми указателями на специализацию; довольно похоже на функцию "Caller" выше.

13 ответов

Решение

Есть много практических применений. Вот что мне приходит в голову:

Предположим, что основная функция, такая как ниже (соответственно определены myfoo и MFN)

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

Такая функция при наличии указателя на функции-члены становится открытой для расширения и закрытой для модификации ( OCP)

Также обратитесь к идиоме "Безопасный бул", которая разумно использует указатель на членов.

По той же причине, по которой вы используете любой указатель на функцию: вы можете использовать произвольную программную логику для установки переменной указателя на функцию перед ее вызовом. Вы можете использовать переключатель 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();
}

Мой вопрос основан на следующем: поскольку у вас есть экземпляр, почему бы не вызвать функцию-член напрямую [?]

Предварительный: За более чем 15-летний опыт программирования на C++ я использовал указатели членов, может быть, два или три раза. С виртуальными функциями, которые есть вокруг, не так уж много пользы для этого.

Вы будете использовать их, если вы хотите вызывать определенные функции-члены для объекта (или многих объектов), и вам нужно решить, какую функцию-член вызывать, прежде чем вы сможете узнать, для каких объектов вызывать ее. Вот пример того, кто хочет это сделать.

Я нахожу реальную полезность указателей на функции-члены, когда вы смотрите на конструкцию более высокого уровня, такую ​​как boost::bind(), Это позволит вам обернуть вызов функции как объект, который впоследствии можно будет связать с конкретным экземпляром объекта, а затем передать как копируемый объект. Это действительно мощная идиома, которая допускает отложенные обратные вызовы, делегаты и сложные операции с предикатами. Смотрите мой предыдущий пост для некоторых примеров:

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

Функции-члены, как и многие указатели на функции, действуют как обратные вызовы. Вы можете обойтись без них, создав некоторый абстрактный класс, который вызывает ваш метод, но это может быть много дополнительной работы.

Одним из распространенных применений являются алгоритмы. В std::for_each нам может потребоваться вызвать функцию-член класса каждого члена нашей коллекции. Мы также можем захотеть вызывать функцию-член нашего собственного класса для каждого члена коллекции - последний требует для выполнения boost::bind, первый можно сделать с помощью семейства классов STL mem_fun (если у нас нет коллекция shared_ptr, в этом случае нам нужно увеличить boost::bind и в этом случае). Мы также можем использовать функцию-член в качестве предиката в определенных алгоритмах поиска или сортировки. (Это избавляет нас от необходимости писать собственный класс, который перегружает operator() для вызова члена нашего класса, мы просто передаем его напрямую в boost::bind).

Другое использование, как я уже говорил, это обратные вызовы, часто в коде, управляемом событиями. Когда операция завершена, мы хотим, чтобы метод нашего класса вызывался для обработки завершения. Это часто можно поместить в функтор boost::bind. В этом случае мы должны быть очень осторожны, чтобы правильно управлять временем жизни этих объектов и их безопасностью потоков (особенно потому, что может быть очень трудно отлаживать, если что-то пойдет не так). Тем не менее, это еще раз может спасти нас от написания большого количества кода-обертки.

На мой взгляд, указатели на функции-члены не очень полезны для среднего программиста в их исходном виде. 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 находится в библиотеке и не подлежит изменению вами, мне бы очень хотелось, чтобы вы реализовали это чисто, используя виртуальный базовый класс, не прибегая к switch или же if else if построить где-нибудь.

Лучшее использование указателей на функции-члены - ломать зависимости.

Хороший пример, где необходим указатель на функцию-член, это шаблон подписчика / издателя:

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

Весь смысл указателей на тип функции указатель на член состоит в том, что они действуют как способ выполнения для ссылки на конкретный метод. Когда вы используете "обычный" синтаксис для доступа к методу

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

method part - это фиксированная спецификация метода, который вы хотите вызвать во время компиляции. Это жестко запрограммировано в вашей программе. Это никогда не может измениться. Но используя указатель типа функции указатель на член, вы можете заменить эту фиксированную часть на переменную, которая может быть изменена во время выполнения метода.

Чтобы лучше проиллюстрировать это, позвольте мне сделать следующую простую аналогию. Допустим, у вас есть массив

int a[100];

Вы можете получить доступ к его элементам с фиксированным индексом времени компиляции

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

В этом случае конкретные показатели жестко запрограммированы в вашей программе. Но вы также можете получить доступ к элементам массива с помощью индекса времени выполнения - целочисленной переменной i

a[i];

значение i не является фиксированной, она может изменяться во время выполнения, что позволяет вам выбирать различные элементы массива во время выполнения. Это очень похоже на то, что позволяют делать указатели типа функции указатель на член.

Вопрос, который вы задаете ("поскольку у вас есть экземпляр, почему бы не вызвать функцию-член напрямую"), можно перевести в этот контекст массива. Вы в основном спрашиваете: "Зачем нам нужен переменный индекс доступа a[i] когда у нас есть прямой постоянный доступ во время компиляции, как a[1] а также a[3] "Я надеюсь, что вы знаете ответ на этот вопрос и понимаете значение выбора времени выполнения конкретного элемента массива.

То же самое относится и к указателям типа функции указатель на член: они, опять же, позволяют выполнять выбор во время выполнения определенного метода класса.

Сценарий использования состоит в том, что у вас есть несколько методов-членов с одной и той же сигнатурой, и вы хотите построить логику, которую следует вызывать при данных обстоятельствах. Это может быть полезно для реализации алгоритмов конечного автомата.

Не то, что вы используете каждый день...

Представьте на секунду, что у вас есть функция, которая может вызывать одну из нескольких функций в зависимости от переданных параметров.

Вы можете использовать гигантский оператор if/else if
Вы можете использовать оператор switch
Или вы можете использовать таблицу указателей функций (таблицу переходов)

Если у вас много разных опций, таблица переходов может быть намного более чистым способом упорядочения вашего кода...

Это до личных предпочтений, хотя. Оператор switch и таблица переходов в любом случае соответствуют более или менее одному и тому же скомпилированному коду:)

Указатели участников + шаблоны = чистый выигрыш.

Например, как определить, содержит ли класс определенную функцию-член во время компиляции

или же

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

Чтобы вызвать его, вам нужна ссылка на экземпляр, но тогда вы можете вызвать функцию func без указания указателя.

Это полностью упускает смысл. Здесь есть две независимые проблемы:

  • какое действие предпринять в более поздний момент времени
  • какой объект для выполнения этого действия

Наличие ссылки на экземпляр удовлетворяет второму требованию. Указатели на функции-члены обращаются к первому: они являются очень прямым способом записать - в какой-то момент выполнения программы - какое действие следует предпринять на более поздней стадии выполнения, возможно, другой частью программы.

ПРИМЕР

Скажем, у вас есть обезьяна, которая может целовать людей или щекотать их. В 18:00 ваша программа должна освободить обезьяну и узнать, кого она должна посетить, но около 15:00 ваш пользователь напечатает, какие действия следует предпринять.

Подход новичка

Итак, в 3 часа дня вы можете установить переменную "enum Action { Kiss, Tickle } action;", затем в 6 часов вечера вы можете сделать что-то вроде "if (action == Kiss) monkey->kiss(person); еще monkey->tickle(человек)".

вопросы

Но это вводит дополнительный уровень кодирования (для поддержки этого введен тип Action - можно использовать встроенные типы, но они будут более подвержены ошибкам и менее значимы по своей сути). Затем - после того, как вы определились, какое действие следует предпринять в 15:00, в 18:00 вы должны избыточно обратиться к этому закодированному значению, чтобы решить, какое действие предпринять, что потребует другого if / else или переключиться на закодированное значение. Это все неуклюже, многословно, медленно и подвержено ошибкам.

Указатели на функции-члены

Лучше использовать более специализированную переменную - указатель на функцию-член, которая напрямую записывает, какое действие выполнить в 18:00. Вот что такое указатель на функцию-член. Это селектор "поцелуй или щекотка", установленный ранее, создающий "состояние" для обезьяны - будь то щекотка или поцелуй, - который можно использовать позже. Более поздний код просто вызывает любую функцию, которая была установлена, без необходимости думать о возможностях или иметь какие-либо операторы if / else-if или switch.

Чтобы вызвать его, вам нужна ссылка на экземпляр, но тогда вы можете вызвать функцию func без указания указателя.

Вернемся к этому. Итак, это хорошо, если вы примете решение о том, какое действие предпринять во время компиляции (то есть точка X в вашей программе, это определенно будет щекотка). Указатели на функции предназначены для случаев, когда вы не уверены, и хотите отделить настройку действий от вызова этих действий.

В этой статье описываются некоторые сценарии, в которых полезны указатели на функции-члены.

Другие вопросы по тегам