Wywoływanie metod klasy C ++ za pośrednictwem wskaźnika funkcji

113

Jak uzyskać wskaźnik funkcji dla funkcji składowej klasy, a następnie wywołać tę funkcję składową z określonym obiektem? Chciałbym napisać:

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


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

Ponadto, jeśli to możliwe, chciałbym również wywołać konstruktor za pomocą wskaźnika:

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

Czy jest to możliwe, a jeśli tak, jaki jest preferowany sposób, aby to zrobić?

Tony the Pony
źródło
1
Nadal nie rozumiem „dlaczego”, jeśli chcesz wywołać funkcję składową obiektów, a następnie po prostu przekazać wskaźnik do obiektu? Jeśli ludzie narzekają na to, ponieważ umożliwia to lepsze hermetyzowanie klasy, dlaczego nie utworzyć klasy interfejsu, z której dziedziczy wszystkie klasy?
Czad
Może to być przydatne przy implementacji czegoś takiego jak wzorzec polecenia, chociaż wiele osób użyłoby boost :: function do ukrycia mechaniki wskaźnika surowego elementu członkowskiego.
CB Bailey
9
Dlaczego przydzielasz tego psa dynamicznie? Następnie musisz ręcznie usunąć obiekt. Wygląda na to, że pochodzisz z Javy, C # lub innego porównywalnego języka i nadal walczysz z C ++. Zwykły automatyczny obiekt ( Dog dog;) jest bardziej prawdopodobny.
sbi
1
@Chad: W większości się zgadzam, ale są chwile, w których przekazanie referencji byłoby bardziej kosztowne. Rozważ pętlę, która iteruje po jakimś typie danych (parsowanie, obliczenia itp.), Niż możliwość wywołania funkcji na podstawie niektórych obliczeń if / else nakłada koszt, w przypadku gdy samo wywołanie wskazanej funkcji może uniknąć takiej sytuacji, jeśli / to / else sprawdza, czy te sprawdzenia można wykonać przed wejściem do pętli.
Eric

Odpowiedzi:

127

Przeczytaj to, aby uzyskać szczegółowe informacje:

// 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');
Satbir
źródło
24
Zaskakujące, że zdecydowali, że to: *this.*pt2Memberzadziała. *ma wyższy priorytet nad .*… Osobiście nadal bym napisał this->*pt2Member, że to jeden operator mniej.
Alexis Wilke,
7
Dlaczego trzeba zainicjować pt2ConstMemberdo NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke dlaczego jest to zaskakujące? Tak jest w przypadku obiektów bezpośrednich (nie wskaźników) (object.*method_pointer), dlatego chcemy, *aby miały one większy priorytet.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@ TomášZato, jeśli się nie mylę (a może się mylę), thisjest używane tylko do zademonstrowania, że ​​cokolwiek zastosujesz, .*powinno być wskaźnikiem do instancji (pod) klasy. Jest to jednak dla mnie nowa składnia, zgaduję tylko na podstawie innych odpowiedzi i zasobów połączonych tutaj. Proponuję edycję, aby było to bardziej zrozumiałe.
c1moore
1
O rany, gratulacje na 100!
Jonathan Mee
59

Jak uzyskać wskaźnik funkcji dla funkcji składowej klasy, a następnie wywołać tę funkcję składową z określonym obiektem?

Najłatwiej jest zacząć od pliku typedef. W przypadku funkcji składowej dodaj nazwę klasy w deklaracji typu:

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

Następnie, aby wywołać metodę, użyj ->*operatora:

(pDog->*pBark)();

Ponadto, jeśli to możliwe, chciałbym również wywołać konstruktor za pomocą wskaźnika. Czy jest to możliwe, a jeśli tak, jaki jest preferowany sposób, aby to zrobić?

Nie wierzę, że można pracować z takimi konstruktorami - lekarze i lekarze są wyjątkowi. Normalnym sposobem osiągnięcia tego typu rzeczy byłoby użycie metody fabrycznej, która jest po prostu statyczną funkcją, która wywołuje konstruktor za Ciebie. Zobacz poniższy kod jako przykład.

Zmodyfikowałem Twój kod, aby zasadniczo robił to, co opisujesz. Poniżej znajdują się zastrzeżenia.

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

Teraz, chociaż normalnie można użyć a Dog*zamiast Animal*podziękowania dzięki magii polimorfizmu, typ wskaźnika funkcji nie jest zgodny z regułami wyszukiwania hierarchii klas. Dlatego wskaźnik metody Animal nie jest zgodny ze wskaźnikiem metody Dog, innymi słowy nie można przypisać Dog* (*)()zmiennej typu a do zmiennej typu Animal* (*)().

Metoda statyczna newDogto prosty przykład fabryki, która po prostu tworzy i zwraca nowe instancje. Będąc funkcją statyczną, ma zwykłą typedef(bez kwalifikatora klasy).

Po udzieleniu odpowiedzi na powyższe pytanie zastanawiam się, czy nie ma lepszego sposobu na osiągnięcie tego, czego potrzebujesz. Jest kilka konkretnych scenariuszy, w których można by to zrobić, ale może się okazać, że istnieją inne wzorce, które lepiej sprawdzają się w przypadku Twojego problemu. Jeśli bardziej ogólnie opiszesz to, co próbujesz osiągnąć, umysł roju może okazać się jeszcze bardziej przydatny!

W związku z powyższym bez wątpienia biblioteka Boost bind i inne powiązane moduły będą bardzo przydatne.

gavinb
źródło
10
Używam C ++ od ponad 10 lat i regularnie uczę się czegoś nowego. Nigdy wcześniej o tym nie słyszałem ->*, ale teraz mam nadzieję, że nigdy nie będę tego potrzebować :)
Thomas
31

Nie sądzę, aby ktokolwiek tutaj wyjaśnił, że jednym z problemów jest to, że potrzebujesz „ wskaźników do elementów członkowskich ”, a nie zwykłych wskaźników funkcji.

Wskaźniki składowe do funkcji nie są po prostu wskaźnikami funkcji. W terminologii implementacji kompilator nie może użyć prostego adresu funkcji, ponieważ generalnie nie znasz adresu do wywołania, dopóki nie wiesz, do którego obiektu należy wyłuskać (pomyśl o funkcjach wirtualnych). Oczywiście musisz znać obiekt, aby podać thisukryty parametr.

Powiedziałem, że ich potrzebujesz, teraz powiem, że naprawdę musisz ich unikać. Poważnie, wskazówki dla członków są uciążliwe. O wiele rozsądniej jest spojrzeć na wzorce projektowe zorientowane obiektowo, które osiągają ten sam cel, lub użyć boost::functionczegoś, co wspomniano powyżej - to znaczy zakładając, że dokonasz tego wyboru.

Jeśli dostarczasz ten wskaźnik funkcji do istniejącego kodu, więc naprawdę potrzebujesz prostego wskaźnika funkcji, powinieneś napisać funkcję jako statyczny element członkowski klasy. Statyczna funkcja członkowska nie rozumie this, więc musisz przekazać obiekt jako jawny parametr. Kiedyś istniał nie tak niezwykły idiom do pracy ze starym kodem C, który wymaga wskaźników funkcji

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

    static void myfunction (myclass *p);
}

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

Ponieważ myfunctionjest to tak naprawdę zwykła funkcja (pomijając kwestie zakresu), wskaźnik funkcji można znaleźć w normalnym języku C.

EDYCJA - ten rodzaj metody nazywany jest „metodą klasową” lub „statyczną funkcją składową”. Główną różnicą w stosunku do funkcji niebędącej składową jest to, że jeśli odwołujesz się do niej spoza klasy, musisz określić zakres za pomocą ::operatora rozpoznawania zakresu. Na przykład, aby uzyskać wskaźnik funkcji, użyj &myclass::myfunctioni, aby wywołać użycie myclass::myfunction (arg);.

Tego rodzaju rzeczy są dość powszechne w przypadku używania starych interfejsów API Win32, które pierwotnie były zaprojektowane dla języka C, a nie C ++. Oczywiście w takim przypadku parametr jest zwykle LPARAM lub podobny, a nie wskaźnik, i potrzebne jest pewne rzutowanie.

Steve314
źródło
„moja funkcja” nie jest funkcją normalną, jeśli przez normalną rozumie się funkcję w stylu C. „moja funkcja” jest dokładniej nazywana metodą myklasy. Metody klasy nie są podobne do zwykłych funkcji, ponieważ mają coś, czego funkcja w stylu C nie ma, czyli wskaźnik „this”.
Eric
3
zalecenie użycia boostu jest drakońskie. Istnieją praktyczne dobre powody, dla których warto używać wskaźników do metod. Nie mam nic przeciwko wspominaniu o wzmocnieniu jako alternatywie, ale nienawidzę, gdy ktoś mówi, że ktoś inny powinien go użyć, nie znając wszystkich faktów. Zwiększenie kosztuje! A jeśli jest to platforma wbudowana, wybór może nie być możliwy. Poza tym naprawdę podoba mi się twój opis.
Eric,
@Eric - Jeśli chodzi o drugą kwestię, nie zamierzałem mówić „powinieneś używać Boost”, a właściwie nigdy sam nie korzystałem z Boost. Intencją (o ile wiem po 3 latach) było to, aby ludzie szukali alternatyw i wymieniali kilka możliwości. „Albo cokolwiek” oznacza, że ​​lista nie jest wyczerpująca. Wskaźniki członkowskie mają swoją czytelność. Ich zwięzła reprezentacja źródła może również maskować koszty czasu wykonywania - w szczególności wskaźnik elementu członkowskiego do metody musi radzić sobie zarówno z metodami niewirtualnymi, jak i wirtualnymi, i musi wiedzieć, które.
Steve314
@Eric - nie tylko to, ale te problemy są przyczyną nieprzenośności ze wskaźnikami składowymi - Visual C ++, przynajmniej w przeszłości, potrzebował dodatkowych wskazówek dotyczących reprezentowania typów wskaźników składowych. Użyłbym statycznego podejścia do funkcji dla systemu osadzonego - reprezentacja wskaźnika jest taka sama jak każdego innego wskaźnika funkcji, koszty są oczywiste i nie ma problemu z przenośnością. A wywołanie opakowane przez statyczną funkcję składową wie (w czasie kompilacji), czy wywołanie jest wirtualne, czy nie - żadne kontrole w czasie wykonywania nie są potrzebne poza zwykłymi wyszukaniami w tabeli vtable dla metod wirtualnych.
Steve314
@Eric - po pierwsze - zdaję sobie sprawę, że statyczna funkcja członkowska nie jest dokładnie tym samym, co funkcja w stylu C (stąd „pomijając kwestie zakresu”), ale prawdopodobnie powinienem był dołączyć nazwę.
Steve314
18
typedef void (Dog::*memfun)();
memfun doSomething = &Dog::bark;
....
(pDog->*doSomething)(); // if pDog is a pointer
// (pDog.*doSomething)(); // if pDog is a reference
AraK
źródło
2
Powinno być: (pDog -> * doSomething) (); // jeśli pDog jest wskaźnikiem // (pDog. * doSomething) (); // jeśli pDog jest odwołaniem, operator as () ma wyższy priorytet, to -> * i. *.
Tomek
12

Minimalny działający przykład

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

Skompiluj i uruchom:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Testowane w Ubuntu 18.04.

Nie możesz zmienić kolejności nawiasów ani ich pominąć. Następujące elementy nie działają:

c.*p(2)
c.*(p)(2)

GCC 9.2 nie powiedzie się z:

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

Standard C ++ 11

.*i ->*pojedynczymi operatorami wprowadzonymi w tym celu w C ++ i nie występują w C.

Wersja robocza standardu C ++ 11 N3337 :

  • 2.13 „Operatory i znaki interpunkcyjne” zawiera listę wszystkich operatorów, która zawiera .*i ->*.
  • 5.5 „Operatory wskaźnika do elementu członkowskiego” wyjaśnia, co robią
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło
11

Przyszedłem tutaj, aby dowiedzieć się, jak utworzyć wskaźnik funkcji (nie wskaźnik metody) na podstawie metody, ale żadna z odpowiedzi tutaj nie zapewnia rozwiązania. Oto co wymyśliłem:

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>

Więc dla twojego przykładu zrobiłbyś teraz:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = METHOD_FP(&Dog::bark);
(*bark)(&dog); // or simply bark(&dog)

Edycja:
Używając C ++ 17, istnieje jeszcze lepsze rozwiązanie:

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

które mogą być używane bezpośrednio bez makra:

Dog dog;
using BarkFunction = void (*)(Dog*);
BarkFunction bark = MethodHelper<&Dog::bark>::call;
(*bark)(&dog); // or simply bark(&dog)

W przypadku metod z modyfikatorami, takimi jak constTy, możesz potrzebować dodatkowych specjalizacji, takich jak:

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...);
    }
};
rzęsa
źródło
6

Powodem, dla którego nie można używać wskaźników funkcji do wywoływania funkcji składowych, jest to, że zwykłe wskaźniki do funkcji są zwykle po prostu adresami pamięci funkcji.

Aby wywołać funkcję członkowską, musisz wiedzieć dwie rzeczy:

  • Którą funkcję członkowską wywołać
  • Która instancja ma być używana (której funkcja członkowska)

Zwykłe wskaźniki funkcji nie mogą przechowywać obu. Wskaźniki funkcji składowej C ++ są używane do przechowywania a), dlatego należy jawnie określić instancję podczas wywoływania wskaźnika funkcji składowej.

hrnt
źródło
1
Głosowałem za tym, ale chciałbym dodać wyjaśnienie na wypadek, gdyby PO nie wiedział, do czego się odnosisz przez „który przypadek”. Rozwinąłbym się, aby wyjaśnić nieodłączny wskaźnik „tego”.
Eric
6

Wskaźnik funkcji do elementu klasy to problem, który naprawdę pasuje do używania funkcji boost :: function. Mały przykład:

#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);
}
Benzoes
źródło
2

Aby utworzyć nowy obiekt, możesz użyć umieszczania nowego obiektu, jak wspomniano powyżej, lub pozwolić klasie zaimplementować metodę clone (), która tworzy kopię obiektu. Następnie możesz wywołać tę metodę klonowania za pomocą wskaźnika funkcji składowej, jak wyjaśniono powyżej, aby utworzyć nowe wystąpienia obiektu. Zaletą clone jest to, że czasami możesz pracować ze wskaźnikiem do klasy bazowej, w której nie znasz typu obiektu. W takim przypadku metoda clone () może być łatwiejsza w użyciu. Ponadto clone () pozwoli ci skopiować stan obiektu, jeśli tego chcesz.

Corwin Joy
źródło
klony mogą być drogie i OP może chcieć ich uniknąć, jeśli wydajność jest problemem lub budzi obawy.
Eric
0

Zrobiłem to za pomocą std :: function i std :: bind ..

Napisałem tę klasę EventManager, która przechowuje wektor programów obsługi w unordered_map, która mapuje typy zdarzeń (które są po prostu const unsigned int, mam ich duże wyliczenie w zakresie przestrzeni nazw) do wektora programów obsługi dla tego typu zdarzenia.

W mojej klasie EventManagerTests konfiguruję procedurę obsługi zdarzeń, taką jak ta:

auto delegate = std::bind(&EventManagerTests::OnKeyDown, this, std::placeholders::_1);
event_manager.AddEventListener(kEventKeyDown, delegate);

Oto funkcja 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;
}

Oto definicja typu EventHandler:

typedef std::function<void(Event *)> EventHandler;

Następnie w EventManagerTests :: RaiseEvent robię to:

Engine::KeyDownEvent event(39);
event_manager.RaiseEvent(1, (Engine::Event*) & event);

Oto kod dla 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);
            }
        );
    }
}

To działa. Otrzymuję wezwanie w EventManagerTests :: OnKeyDown. Muszę usunąć wektory, przyjść na czas czyszczenia, ale kiedy to zrobię, nie ma wycieków. Podniesienie zdarzenia zajmuje na moim komputerze około 5 mikrosekund, czyli około 2008 roku. Nie do końca superszybkie, ale. W porządku, o ile wiem o tym i nie używam go w bardzo gorącym kodzie.

Chciałbym to przyspieszyć, zwijając własne std :: function i std :: bind i być może używając tablicy tablic zamiast unordered_map wektorów, ale nie do końca wymyśliłem, jak przechowywać funkcję składową wskaźnik i wywołaj go z kodu, który nic nie wie o wywoływanej klasie. Odpowiedź rzęs wygląda bardzo interesująco.

Shavais
źródło