Виклик методів класу C ++ через покажчик функції


113

Як отримати вказівник на функцію члена класу та пізніше викликати цю функцію члена певним об'єктом? Я хотів би написати:

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


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

Також, якщо можливо, я б також хотів викликати конструктор через покажчик:

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

Чи можливо це, і якщо так, то який найкращий спосіб це зробити?


1
Я все ще не розумію "чому", якщо ви хочете викликати функцію члена об'єкта, а потім просто передати вказівник на об'єкт? Якщо люди скаржаться на те, що це дозволяє краще інкапсулювати клас, чому б не зробити клас інтерфейсу, від якого успадковується весь клас?
Чад

Це може бути корисно для реалізації чогось подібного шаблону команд, хоча багато людей використовуватимуть функцію boost ::, щоб приховати механіку вказівника необробленого члена.
CB Bailey

9
Чому ти динамічно виділяєш цю собаку? Потім вам також доведеться видалити об’єкт вручну. Це схоже на те, що ви приїжджаєте з Java, C # або іншої порівнянної мови і все ще ведете боротьбу з C ++. Простий автоматичний об'єкт (Dog dog; ) швидше те, що ви хочете.
sbi

1
@Chad: Я в основному погоджуюся, але бувають випадки, коли передача посилання буде дорожчою. Розглянемо цикл, який повторюється над деяким типом даних (розбір, обчислення тощо), ніж можливість викликати функцію на основі деяких обчислень if / else накладає вартість, коли просто виклик загостреної функції також може уникнути такого, якщо / тоді / else перевіряє, чи ці перевірки можна було зробити перед входом у цикл.
Ерік

Відповіді:


127

Прочитайте це докладно:

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

24
Дивно, що вони вирішили, що це: *this.*pt2Memberбуде працювати. *має більший пріоритет над .*... Особисто я все одно писав би this->*pt2Member, що це один оператор менш.
Алексіс Вілке

7
Чому ви повинні ініціалізувати pt2ConstMemberдо NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@AlexisWilke чому це дивно? Для прямих об'єктів (а не покажчиків) це (object.*method_pointer)так, тому ми хочемо, *щоб вони мали більший пріоритет.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@ TomášZato, якщо я не помиляюся (і я міг би бути), thisякраз використовується, щоб продемонструвати, що все, .*до чого ви звертаєтесь, має бути покажчиком на екземпляр (під) класу. Однак це новий синтаксис для мене, я лише здогадуюсь, грунтуючись на інших відповідях та ресурсах, пов'язаних тут. Я пропоную редагування, щоб зробити це більш зрозумілим.
c1moore

1
О, хапай, вітаю на 100!
Джонатан Мі

59

Як я можу отримати вказівник функції для функції члена класу, а пізніше викликати цю функцію члена певним об'єктом?

Найпростіше почати з а typedef. Для функції члена ви додаєте ім'я класу в декларацію про тип:

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

Потім для виклику методу ви використовуєте ->*оператор:

(pDog->*pBark)();

Також, якщо можливо, я б також хотів викликати конструктор через покажчик. Чи можливо це, і якщо так, то який найкращий спосіб це зробити?

Я не вірю, що ти можеш працювати з такими конструкторами - електродвигуни та електродвигуни особливі. Нормальним способом досягти подібних речей було б використання фабричного методу, який в основному є лише статичною функцією, яка викликає конструктор для вас. Див. Приклад нижче.

Я змінив ваш код, щоб в основному робити те, що ви описуєте. Нижче є деякі застереження.

#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* (*)() a змінній типу Animal* (*)().

Статичний newDogметод - простий приклад фабрики, який просто створює та повертає нові екземпляри. Будучи статичною функцією, вона має регулярнуtypedef (без класифікатора класу).

Відповівши на вищезазначене, мені цікаво, чи немає кращого способу досягнення того, що вам потрібно. Існує кілька конкретних сценаріїв, коли ви б робили подібні речі, але ви можете знайти інші моделі, які краще працюють для вашої проблеми. Якщо описати більш загальними термінами, чого ви намагаєтесь досягти, вулик-розум може виявитися ще кориснішим!

Зв'язане з вищезазначеним, ви, без сумніву, вважаєте, що бібліотека Boost bind та інші пов'язані модулі дуже корисні.


10
Я використовую C ++ більше 10 років і постійно навчаюсь чомусь новому. Я ніколи не чув про це ->*, але зараз, сподіваюся, мені це ніколи не знадобиться :)
Томас,

31

Я не думаю, що тут хтось пояснив, що одне питання полягає в тому, що вам потрібні " покажчики членів ", а не звичайні покажчики функцій.

Покажчики членів на функції - це не просто функціональні покажчики. З точки зору реалізації, компілятор не може використовувати просту адресу функції, оскільки, як правило, ви не знаєте адреси для виклику, поки не дізнаєтесь, для якого об'єкта потрібно відкинути (подумайте, віртуальних функцій). Вам також потрібно знати об'єкт, щоб this, звичайно, надати імпліцитний параметр.

Сказавши, що вони вам потрібні, зараз я скажу, що вам справді потрібно уникати їх. Серйозно, покажчики членів - це біль. Набагато розумніше дивитись на об'єктно-орієнтовані шаблони дизайну, які досягають тієї самої мети, або використовувати boost::functionабо що-небудь, як було сказано вище - якщо припустити, що ви зробите такий вибір, тобто.

Якщо ви постачаєте цей покажчик функції на існуючий код, тож вам дійсно потрібен простий вказівник функції, ви повинні написати функцію як статичний член класу. Функція статичного члена не розуміє this, тому вам потрібно буде передати об'єкт як явний параметр. Колись у цих напрямках існувала не така незвична ідіома для роботи зі старим кодом С, який потребує функціональних покажчиків

class myclass
{
  public:
    virtual void myrealmethod () = 0;

    static void myfunction (myclass *p);
}

void myclass::myfunction (myclass *p)
{
  p->myrealmethod ();
}

Оскільки myfunctionце дійсно лише нормальна функція (питання щодо обсягу вбік), то вказівник на функцію може бути знайдений звичайним способом C.

EDIT - такий метод називається "метод класу" або "статична функція члена". Основна відмінність від функції, яка не є членом, полягає в тому, що якщо ви посилаєтесь на неї поза класом, ви повинні вказати область, використовуючи ::оператор роздільної здатності. Наприклад, щоб отримати вказівник функції, використовуйте &myclass::myfunctionта називайте його use myclass::myfunction (arg);.

Такі речі є досить поширеними при використанні старих API Win32, які спочатку були розроблені для C, а не C ++. Звичайно, у цьому випадку параметр, як правило, є LPARAM або подібний, а не покажчик, і потрібне деяке кастинг.


"моя функція" не є нормальною функцією, якщо під нормою ви маєте на увазі функцію стилю C. 'myfunction' більш точно називається методом myclass. Методи класу не схожі на звичайні функції в тому, що у них є щось, що функція стилю C не є, що є покажчиком "цього".
Ерік

3
радити використовувати бустер - драконічний. Існують практичні вагомі причини використання покажчиків методів. Я не проти згадки про посилення як альтернативу, але ненавиджу, коли хтось каже, що хтось інший повинен використовувати це, не знаючи всіх фактів. Підвищення приходить за вартістю! І якщо це вбудована платформа, то це може бути не можливим вибором. Крім цього, мені дуже подобається ваш запис.
Ерік

@Eric - По вашому другому моменту я не збирався говорити "ти будеш використовувати Boost", а насправді я ніколи не використовував Boost. Намір (наскільки я це знаю через 3 роки) полягав у тому, що люди повинні шукати альтернативи та перелічити декілька можливостей. "Або що завгодно" означає, що список не має бути вичерпним. Покажчики членів мають вартістю читабельності. Їх коротке представлення джерела також може замаскувати витрати на час виконання - зокрема, вказівник учасника на метод повинен справлятися як з невіртуальними, так і з віртуальними методами, і повинен знати, який.
Steve314

@Eric - Мало того, але ці проблеми є причиною непереносимості вказівників-членів - Visual C ++, принаймні, у минулому, потребував додаткових підказок про те, як представити типи вказівників членів. Я б застосував підхід статичної функції для вбудованої системи - подання покажчика таке ж, як і будь-який інший вказівник функції, витрати очевидні, і проблеми з переносом немає. І виклик, завершений функцією статичного члена, знає (під час компіляції), чи є виклик віртуальним чи ні - не потрібно перевіряти час виконання, окрім звичайних віртуальних пошукових даних для віртуальних методів.
Стів314,

@Eric - по-першому, я знаю, що статична функція члена не є точно такою ж, як функція в стилі C (отже, "питання щодо обсягу вбік"), але я, мабуть, повинен був включити ім'я.
Стів314,

18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference

2
Повинно бути: (pDog -> * doSomething) (); // якщо pDog - покажчик // (pDog. * doSomething) (); // якщо pDog є посиланням, оскільки оператор () має більший пріоритет, то -> * і. *.
Томек

12

Приклад мінімальної експлуатації

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 стандарт

.*і ->*є єдиними операторами, введеними для цієї мети на C ++, а не в C.

C ++ 11 стандартний чернетка N3337 :

  • 2.13 "Оператори та пунктуатори" має список усіх операторів, який містить .*і->* .
  • 5.5 "Оператори вказівника до члена" пояснюють, що вони роблять

11

Я прийшов сюди, щоб дізнатися, як створити функцію вказівника (не методу вказівника), але жоден з відповідей тут не пропонує рішення. Ось що я придумав:

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

6

Причина, чому не можна використовувати покажчики функцій для виклику функцій учасників, полягає в тому, що звичайні покажчики функцій зазвичай є лише адресою пам'яті функції.

Для виклику функції члена потрібно знати дві речі:

  • Який функція учасника дзвонити
  • Який екземпляр слід використовувати (чия функція-член)

Звичайні покажчики функцій не можуть зберігати обидва. Покажчики функцій члена C ++ використовуються для зберігання a), тому вам потрібно чітко вказати екземпляр під час виклику вказівника функції члена.


1
Я проголосував за це, але додав би пояснення, якщо ОП не знає, про що ви маєте на увазі "який екземпляр". Я б розширив пояснення притаманного «цього» вказівника.
Ерік

6

Вказівник функції на члена класу - проблема, яка дійсно підходить для використання функції boost ::. Невеликий приклад:

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

2

Щоб створити новий об’єкт, ви можете використовувати нове місце розташування, як було сказано вище, або ж ваш клас реалізує метод clone (), який створює копію об'єкта. Потім ви можете викликати цей метод клонування, використовуючи вказівник функції члена, як пояснено вище, для створення нових екземплярів об'єкта. Перевага клону в тому, що іноді ви можете працювати з вказівником на базовий клас, де ви не знаєте тип об'єкта. У цьому випадку метод clone () може бути простішим у використанні. Також clone () дозволить вам скопіювати стан об'єкта, якщо це саме те, що ви хочете.


клони можуть бути дорогими і ОП може захотіти їх уникнути, якщо результативність викликає проблеми або викликає певне занепокоєння.
Ерік

0

Я робив це за допомогою std :: function та std :: bind ..

Я написав цей клас EventManager, який зберігає вектор обробників у невпорядкованому_мапі, який відображає типи подій (які просто const без підписаного 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 :: та std :: bind, а можливо, використовуючи масив масивів, а не невпорядковану_мапу векторів, але я не зовсім зрозумів, як зберігати функцію члена покажчик і викликайте його з коду, який нічого не знає про клас, що викликається. Відповідь вій виглядає дуже цікаво ..

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.