Memanggil metode kelas C ++ melalui penunjuk fungsi

113

Bagaimana cara mendapatkan penunjuk fungsi untuk fungsi anggota kelas, dan kemudian memanggil fungsi anggota tersebut dengan objek tertentu? Saya ingin menulis:

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


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

Juga, jika memungkinkan, saya juga ingin memanggil konstruktor melalui pointer:

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

Apakah ini mungkin, dan jika demikian, cara apa yang lebih disukai untuk melakukan ini?

Tony the Pony
sumber
1
Saya masih tidak begitu mengerti 'mengapa' jika Anda ingin memanggil fungsi anggota objek maka cukup berikan penunjuk ke objek? Jika orang mengeluh karena itu memungkinkan Anda untuk merangkum kelas dengan lebih baik mengapa tidak membuat kelas antarmuka yang mewarisi semua kelas?
Chad
Ini dapat berguna dalam mengimplementasikan sesuatu seperti pola perintah meskipun banyak orang akan menggunakan boost :: function untuk menyembunyikan mekanisme raw member pointer.
CB Bailey
9
Mengapa Anda mengalokasikan anjing itu secara dinamis? Anda kemudian harus menghapus objek tersebut secara manual juga. Sepertinya Anda berasal dari Java, C # atau bahasa lain yang sebanding dan masih bertarung dengan C ++. Objek otomatis biasa ( Dog dog;) kemungkinan besar adalah yang Anda inginkan.
sbi
1
@ Chad: Saya sangat setuju tetapi ada kalanya menyampaikan referensi akan lebih mahal. Pertimbangkan loop yang mengulang beberapa jenis data (parsing, kalkulasi, dll ..) daripada dapat memanggil fungsi berdasarkan beberapa kalkulasi if / else membebankan biaya di mana hanya memanggil fungsi point too dapat menghindari hal seperti itu jika / kemudian / else memeriksa apakah pemeriksaan ini bisa dilakukan sebelum memasuki loop.
Eric

Jawaban:

126

Baca ini untuk detailnya:

// 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
sumber
23
Mengejutkan bahwa mereka memutuskan bahwa ini: *this.*pt2Memberakan berhasil. *memiliki prioritas yang lebih tinggi daripada .*... Secara pribadi, saya masih akan menulis this->*pt2Member, itu satu operator lebih sedikit.
Alexis Wilke
7
Mengapa Anda harus menginisialisasi pt2ConstMemberke NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@AlexisWilke mengapa ini mengejutkan? Untuk objek langsung (bukan pointer) (object.*method_pointer), jadi kami ingin *memiliki prioritas yang lebih besar.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
@ TomášZato, jika saya tidak salah (dan saya mungkin salah), thishanya digunakan untuk menunjukkan bahwa apa pun yang Anda terapkan .*harus menjadi penunjuk ke instance dari (sub) kelas. Namun ini adalah sintaks baru bagi saya, saya hanya menebak berdasarkan jawaban dan sumber daya lain yang ditautkan di sini. Saya menyarankan pengeditan untuk membuatnya lebih jelas.
c1moore
1
Aduh, selamat atas 100!
Jonathan Mee
57

Bagaimana cara mendapatkan penunjuk fungsi untuk fungsi anggota kelas, dan kemudian memanggil fungsi anggota tersebut dengan objek tertentu?

Paling mudah untuk memulai dengan a typedef. Untuk fungsi anggota, Anda menambahkan nama kelas di deklarasi type:

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

Kemudian untuk memanggil metode tersebut, Anda menggunakan ->*operator:

(pDog->*pBark)();

Juga, jika memungkinkan, saya juga ingin memanggil konstruktor melalui pointer. Apakah ini mungkin, dan jika demikian, cara apa yang lebih disukai untuk melakukan ini?

Saya tidak percaya Anda dapat bekerja dengan konstruktor seperti ini - pemberi dan penjual itu istimewa. Cara normal untuk mencapai hal semacam itu akan menggunakan metode pabrik, yang pada dasarnya hanyalah fungsi statis yang memanggil konstruktor untuk Anda. Lihat kode di bawah ini sebagai contoh.

Saya telah memodifikasi kode Anda untuk melakukan apa yang pada dasarnya Anda gambarkan. Ada beberapa peringatan di bawah.

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

Sekarang meskipun Anda biasanya dapat menggunakan a Dog*sebagai pengganti Animal*berkat keajaiban polimorfisme, jenis penunjuk fungsi tidak mengikuti aturan pencarian hierarki kelas. Jadi penunjuk metode Hewan tidak kompatibel dengan penunjuk metode Anjing, dengan kata lain Anda tidak dapat menetapkan a Dog* (*)()ke variabel tipe Animal* (*)().

newDogMetode statis adalah contoh sederhana dari sebuah pabrik, yang hanya membuat dan mengembalikan instance baru. Menjadi fungsi statis, ia memiliki reguler typedef(tanpa kualifikasi kelas).

Setelah menjawab pertanyaan di atas, saya bertanya-tanya apakah tidak ada cara yang lebih baik untuk mencapai apa yang Anda butuhkan. Ada beberapa skenario khusus di mana Anda akan melakukan hal semacam ini, tetapi Anda mungkin menemukan ada pola lain yang bekerja lebih baik untuk masalah Anda. Jika Anda mendeskripsikan dalam istilah yang lebih umum apa yang ingin Anda capai, pikiran sarang mungkin terbukti lebih berguna!

Terkait dengan hal di atas, Anda pasti akan menemukan pustaka Boost bind dan modul terkait lainnya sangat berguna.

gavinb.dll
sumber
10
Saya telah menggunakan C ++ selama lebih dari 10 tahun, dan terus mempelajari hal baru secara teratur. Saya belum pernah mendengarnya ->*sebelumnya, tetapi sekarang saya harap saya tidak akan pernah membutuhkannya :)
Thomas
31

Saya tidak berpikir ada orang yang menjelaskan di sini bahwa satu masalah adalah Anda memerlukan " pointer anggota " daripada pointer fungsi normal.

Pointer anggota ke fungsi bukan sekadar pointer fungsi. Dalam istilah implementasi, kompilator tidak dapat menggunakan alamat fungsi sederhana karena, secara umum, Anda tidak tahu alamat yang akan dipanggil sampai Anda mengetahui objek mana yang akan didereferensi (pikirkan fungsi virtual). Anda juga perlu mengetahui objeknya untuk memberikan thisparameter implisit, tentunya.

Setelah mengatakan bahwa Anda membutuhkannya, sekarang saya akan mengatakan bahwa Anda benar-benar perlu menghindarinya. Serius, petunjuk anggota menyebalkan. Jauh lebih masuk akal untuk melihat pola desain berorientasi objek yang mencapai tujuan yang sama, atau menggunakan boost::functionatau apa pun seperti yang disebutkan di atas - dengan asumsi Anda bisa membuat pilihan itu.

Jika Anda menyediakan penunjuk fungsi tersebut ke kode yang sudah ada, sehingga Anda benar - benar membutuhkan penunjuk fungsi sederhana, Anda harus menulis fungsi sebagai anggota statis kelas. Fungsi anggota statis tidak mengerti this, jadi Anda harus meneruskan objek sebagai parameter eksplisit. Pernah ada idiom yang tidak biasa di sepanjang baris ini untuk bekerja dengan kode C lama yang membutuhkan pointer fungsi

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

    static void myfunction (myclass *p);
}

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

Karena myfunctionbenar-benar hanya fungsi normal (masalah lingkup selain), penunjuk fungsi dapat ditemukan dengan cara C normal.

EDIT - metode semacam ini disebut "metode kelas" atau "fungsi anggota statis". Perbedaan utama dari fungsi non-anggota adalah, jika Anda mereferensikannya dari luar kelas, Anda harus menentukan cakupan menggunakan ::operator resolusi cakupan. Misalnya, untuk mendapatkan penunjuk fungsi, gunakan &myclass::myfunctiondan untuk memanggilnya gunakan myclass::myfunction (arg);.

Hal semacam ini cukup umum ketika menggunakan Win32 API lama, yang awalnya dirancang untuk C daripada C ++. Tentu saja dalam kasus itu, parameternya biasanya LPARAM atau serupa daripada sebuah pointer, dan diperlukan beberapa casting.

Steve314
sumber
'fungsi saya' bukanlah fungsi normmal jika yang Anda maksud adalah fungsi gaya C. 'myfunction' lebih tepat disebut metode myclass. Metode kelas tidak seperti fungsi normal karena mereka memiliki sesuatu yang tidak dimiliki oleh fungsi gaya C yang merupakan penunjuk 'ini'.
Eric
3
menyarankan untuk menggunakan dorongan itu kejam. Ada alasan praktis yang bagus untuk menggunakan pointer metode. Saya tidak keberatan menyebutkan dorongan sebagai alternatif tetapi benci ketika seseorang mengatakan orang lain harus menggunakannya tanpa mengetahui semua fakta. Boost ada harganya! Dan jika ini adalah platform yang disematkan maka itu mungkin bukan pilihan yang memungkinkan. Di luar ini, saya sangat suka tulisan Anda.
Eric
@ Eric - Pada poin kedua Anda, saya tidak bermaksud mengatakan "Anda harus menggunakan Boost", dan sebenarnya saya tidak pernah menggunakan Boost sendiri. Maksudnya (sejauh yang saya tahu setelah 3 tahun) adalah bahwa orang harus mencari alternatif, dan membuat daftar beberapa kemungkinan. "Atau apa pun" menunjukkan bahwa daftar tidak dimaksudkan untuk lengkap. Pointer anggota memiliki biaya dalam keterbacaan. Representasi sumber yang ringkas juga dapat menyamarkan biaya run-time - khususnya penunjuk anggota ke suatu metode harus mengatasi metode non-virtual dan virtual, dan harus tahu yang mana.
Steve314
@Eric - Tidak hanya itu, tetapi masalah ini adalah alasan untuk non-portabilitas dengan pointer anggota - Visual C ++, setidaknya di masa lalu, memerlukan beberapa petunjuk tambahan tentang cara merepresentasikan tipe pointer anggota. Saya akan menggunakan pendekatan fungsi statis untuk sistem tertanam - representasi penunjuk sama dengan penunjuk fungsi lainnya, biayanya jelas, dan tidak ada masalah portabilitas. Dan panggilan yang dibungkus oleh fungsi anggota statis mengetahui (pada waktu kompilasi) apakah panggilan itu virtual atau tidak - tidak diperlukan pemeriksaan run-time di luar pencarian vtable biasa untuk metode virtual.
Steve314
@Eric - pada poin pertama Anda - Saya sadar bahwa fungsi anggota statis tidak persis sama dengan fungsi C-style (karena itu "mengesampingkan masalah lingkup"), tetapi saya mungkin harus menyertakan namanya.
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
sumber
2
Seharusnya: (pDog -> * doSomething) (); // jika pDog adalah sebuah pointer // (pDog. * doSomething) (); // jika pDog adalah referensi karena operator () memiliki prioritas lebih tinggi maka -> * dan. *.
Tomek
12

Contoh minimal runnable

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

Kompilasi dan jalankan:

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

Diuji di Ubuntu 18.04.

Anda tidak dapat mengubah urutan tanda kurung atau menghilangkannya. Berikut ini tidak berhasil:

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

GCC 9.2 akan gagal dengan:

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

Standar C ++ 11

.*dan ->*merupakan operator tunggal yang diperkenalkan di C ++ untuk tujuan ini, dan tidak ada di C.

Draf standar C ++ 11 N3337 :

  • 2.13 "Operator dan punctuator" memiliki daftar semua operator, yang berisi .*dan ->*.
  • 5.5 "Operator penunjuk-ke-anggota" menjelaskan apa yang mereka lakukan
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
11

Saya datang ke sini untuk mempelajari cara membuat penunjuk fungsi (bukan penunjuk metode) dari sebuah metode tetapi tidak ada jawaban di sini yang memberikan solusi. Inilah yang saya dapatkan:

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>

Jadi untuk contoh Anda, Anda sekarang akan melakukan:

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

Sunting:
Menggunakan C ++ 17, ada solusi yang lebih baik:

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

yang dapat digunakan secara langsung tanpa makro:

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

Untuk metode dengan pengubah seperti constAnda mungkin memerlukan beberapa spesialisasi seperti:

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...);
    }
};
bulu mata
sumber
6

Alasan mengapa Anda tidak dapat menggunakan penunjuk fungsi untuk memanggil fungsi anggota adalah karena penunjuk fungsi biasa biasanya hanya alamat memori dari fungsi tersebut.

Untuk memanggil fungsi anggota, Anda perlu mengetahui dua hal:

  • Anggota mana yang berfungsi untuk dihubungi
  • Contoh mana yang harus digunakan (yang fungsi anggotanya)

Penunjuk fungsi biasa tidak dapat menyimpan keduanya. Pointer fungsi anggota C ++ digunakan untuk menyimpan a), itulah sebabnya Anda perlu menentukan instance secara eksplisit saat memanggil penunjuk fungsi anggota.

hrnt
sumber
1
Saya memilih ini tetapi akan menambahkan poin klarifikasi jika OP tidak tahu apa yang Anda maksud dengan "contoh yang mana". Saya akan memperluas untuk menjelaskan penunjuk 'ini' yang melekat.
Eric
6

Penunjuk fungsi ke anggota kelas adalah masalah yang sangat cocok untuk menggunakan boost :: function. Contoh kecil:

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

Untuk membuat objek baru, Anda dapat menggunakan penempatan baru, seperti yang disebutkan di atas, atau meminta kelas Anda mengimplementasikan metode clone () yang membuat salinan objek. Anda kemudian dapat memanggil metode klon ini menggunakan penunjuk fungsi anggota seperti yang dijelaskan di atas untuk membuat instance baru dari objek. Keuntungan dari kloning adalah terkadang Anda mungkin bekerja dengan pointer ke kelas dasar di mana Anda tidak mengetahui tipe objeknya. Dalam hal ini metode clone () bisa lebih mudah digunakan. Selain itu, clone () akan membiarkan Anda menyalin status objek jika itu yang Anda inginkan.

Corwin Joy
sumber
klon bisa mahal dan OP mungkin ingin menghindarinya jika kinerja menjadi masalah atau kekhawatiran.
Eric
0

Saya melakukan ini dengan std :: function dan std :: bind ..

Saya menulis kelas EventManager ini yang menyimpan vektor penangan dalam unordered_map yang memetakan jenis peristiwa (yang hanya const unsigned int, saya memiliki enum lingkup namespace besar dari mereka) ke vektor penangan untuk jenis peristiwa itu.

Di kelas EventManagerTests saya, saya menyiapkan penangan peristiwa, seperti ini:

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

Inilah fungsi 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;
}

Berikut definisi tipe EventHandler:

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

Kemudian kembali ke EventManagerTests :: RaiseEvent, saya melakukan ini:

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

Berikut kode untuk 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);
            }
        );
    }
}

Ini bekerja. Saya mendapatkan panggilan di EventManagerTests :: OnKeyDown. Saya harus menghapus vektor datang waktu pembersihan, tetapi begitu saya melakukannya, tidak ada kebocoran. Meningkatkan acara membutuhkan waktu sekitar 5 mikrodetik di komputer saya, yaitu sekitar tahun 2008. Tidak bisa dibilang super cepat, tapi. Cukup adil selama saya mengetahuinya dan saya tidak menggunakannya dalam kode ultra hot.

Saya ingin mempercepatnya dengan menggulung std :: function dan std :: bind saya sendiri, dan mungkin menggunakan array array daripada unordered_map vektor, tetapi saya belum cukup menemukan cara menyimpan fungsi anggota pointer dan memanggilnya dari kode yang tidak tahu apa-apa tentang kelas yang dipanggil. Jawaban bulu mata terlihat Sangat Menarik ..

Shavais
sumber