Gọi các phương thức lớp C ++ thông qua một con trỏ hàm


113

Làm cách nào để lấy con trỏ hàm cho một hàm thành viên lớp và sau này gọi hàm thành viên đó với một đối tượng cụ thể? Tôi muốn viết:

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


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

Ngoài ra, nếu có thể, tôi cũng muốn gọi hàm tạo thông qua một con trỏ:

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

Điều này có khả thi không, và nếu vậy, cách ưu tiên để làm điều này là gì?


1
Tôi vẫn không thực sự hiểu 'tại sao' nếu bạn muốn gọi một hàm thành viên đối tượng sau đó chỉ cần truyền một con trỏ đến đối tượng? Nếu mọi người phàn nàn rằng vì nó cho phép bạn đóng gói lớp tốt hơn, tại sao không tạo một lớp giao diện mà tất cả lớp kế thừa từ đó?
Chad

Nó có thể hữu ích trong việc thực hiện một cái gì đó như mẫu lệnh mặc dù nhiều người sẽ sử dụng hàm boost :: để ẩn cơ chế con trỏ thành viên thô.
CB Bailey

9
Tại sao bạn lại phân bổ động con chó đó? Sau đó, bạn cũng phải xóa đối tượng theo cách thủ công. Điều này trông giống như bạn đến từ Java, C # hoặc một số ngôn ngữ tương đương khác và vẫn chiến đấu với C ++. Một đối tượng tự động thuần túy ( Dog dog;) có nhiều khả năng là thứ bạn muốn.
sbi 28/09/09

1
@Chad: Tôi hầu như đồng ý nhưng đôi khi việc chuyển một tham chiếu sẽ tốn kém hơn. Hãy xem xét một vòng lặp đang lặp lại trên một số loại dữ liệu (phân tích cú pháp, tính toán, v.v.) so với việc có thể gọi một hàm dựa trên một số phép tính if / else đặt ra một chi phí mà chỉ cần gọi hàm quá trỏ có thể tránh được if / then như vậy / else kiểm tra xem những kiểm tra này có thể được thực hiện trước khi vào vòng lặp hay không.
Eric

Câu trả lời:


126

Đọc cái này để biết chi tiết:

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

23
Ngạc nhiên rằng họ quyết định rằng điều này: *this.*pt2Membersẽ hoạt động. *có quyền ưu tiên cao hơn .*... Cá nhân tôi vẫn viết this->*pt2Member, đó là một toán tử ít hơn.
Alexis Wilke

7
Tại sao bạn phải khởi tạo pt2ConstMemberthành NULL?
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@AlexisWilke tại sao nó đáng ngạc nhiên? Đối với các đối tượng trực tiếp (không phải con trỏ) (object.*method_pointer), vì vậy chúng tôi muốn các *đối tượng có mức độ ưu tiên lớn hơn.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

@ TomášZato, nếu tôi không nhầm (và tôi có thể là vậy), thischỉ đang được sử dụng để chứng minh rằng bất cứ thứ gì bạn áp dụng .*phải là một con trỏ đến một thể hiện của lớp (con). Tuy nhiên, đây là cú pháp mới đối với tôi, tôi chỉ đoán dựa trên các câu trả lời và tài nguyên khác được liên kết ở đây. Tôi đang đề xuất một chỉnh sửa để làm cho điều đó rõ ràng hơn.
c1moore

1
Oh snap, chúc mừng 100!
Jonathan Mee

57

Làm cách nào để lấy con trỏ hàm cho một hàm thành viên lớp và sau này gọi hàm thành viên đó với một đối tượng cụ thể?

Dễ nhất là bắt đầu bằng a typedef. Đối với một hàm thành viên, bạn thêm tên lớp vào khai báo kiểu:

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

Sau đó, để gọi phương thức, bạn sử dụng ->*toán tử:

(pDog->*pBark)();

Ngoài ra, nếu có thể, tôi cũng muốn gọi hàm tạo thông qua một con trỏ. Điều này có khả thi không, và nếu vậy, cách ưu tiên để làm điều này là gì?

Tôi không tin rằng bạn có thể làm việc với các hàm tạo như thế này - ctors và dtors là đặc biệt. Cách thông thường để đạt được điều đó là sử dụng phương thức factory, về cơ bản chỉ là một hàm tĩnh gọi hàm tạo cho bạn. Xem đoạn mã dưới đây để biết ví dụ.

Tôi đã sửa đổi mã của bạn để làm về cơ bản những gì bạn mô tả. Có một số lưu ý bên dưới.

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

Bây giờ, mặc dù thông thường bạn có thể sử dụng a Dog*thay cho dấu Animal*nhờ phép thuật đa hình, nhưng kiểu con trỏ hàm không tuân theo các quy tắc tra cứu của hệ thống phân cấp lớp. Vì vậy, một con trỏ phương thức Animal không tương thích với một con trỏ phương thức Dog, nói cách khác, bạn không thể gán Dog* (*)()một biến kiểu Animal* (*)().

newDogPhương thức static là một ví dụ đơn giản về một nhà máy, nó chỉ đơn giản là tạo và trả về các thể hiện mới. Là một hàm tĩnh, nó có một hàm thông thường typedef(không có định nghĩa lớp).

Sau khi trả lời những điều trên, tôi tự hỏi liệu không có cách nào tốt hơn để đạt được những gì bạn cần. Có một vài tình huống cụ thể mà bạn sẽ làm điều này, nhưng bạn có thể thấy có những mẫu khác phù hợp hơn với vấn đề của bạn. Nếu bạn mô tả một cách tổng quát hơn những gì bạn đang cố gắng đạt được, thì tư duy tổ ong có thể còn hữu ích hơn nữa!

Liên quan đến những điều trên, chắc chắn bạn sẽ thấy thư viện liên kết Boost và các mô-đun liên quan khác rất hữu ích.


10
Tôi đã sử dụng C ++ trong hơn 10 năm và tiếp tục học những điều mới một cách thường xuyên. Tôi chưa bao giờ nghe nói về ->*trước đó, nhưng bây giờ tôi hy vọng tôi sẽ không bao giờ cần đến nó :)
Thomas

31

Tôi không nghĩ rằng có ai giải thích ở đây rằng một vấn đề là bạn cần " con trỏ thành viên " hơn là con trỏ hàm bình thường.

Con trỏ thành viên đến các hàm không chỉ đơn giản là con trỏ hàm. Trong điều kiện triển khai, trình biên dịch không thể sử dụng một địa chỉ hàm đơn giản bởi vì, nói chung, bạn không biết địa chỉ để gọi cho đến khi bạn biết đối tượng nào cần tham chiếu (hãy nghĩ đến các hàm ảo). Tất nhiên, bạn cũng cần biết đối tượng để cung cấp thistham số ngầm định.

Đã nói rằng bạn cần chúng, bây giờ tôi sẽ nói rằng bạn thực sự cần phải tránh chúng. Nghiêm túc mà nói, chỉ dẫn thành viên là một nỗi đau. Sẽ tốt hơn nhiều khi xem xét các mẫu thiết kế hướng đối tượng để đạt được cùng một mục tiêu, hoặc sử dụng một boost::functionhoặc bất cứ điều gì như đã đề cập ở trên - giả sử bạn phải đưa ra lựa chọn đó.

Nếu bạn đang cung cấp con trỏ hàm đó cho mã hiện có, vì vậy bạn thực sự cần một con trỏ hàm đơn giản, bạn nên viết một hàm dưới dạng thành viên tĩnh của lớp. Một hàm thành viên tĩnh không hiểu this, vì vậy bạn sẽ cần chuyển đối tượng vào dưới dạng một tham số rõ ràng. Đã từng có một thành ngữ không phải là bất thường dọc theo những dòng này để làm việc với mã C cũ cần con trỏ hàm

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

    static void myfunction (myclass *p);
}

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

myfunctionthực sự chỉ là một hàm bình thường (bỏ qua vấn đề phạm vi), một con trỏ hàm có thể được tìm thấy theo cách C bình thường.

EDIT - loại phương thức này được gọi là "phương thức lớp" hoặc "hàm thành viên tĩnh". Sự khác biệt chính so với một hàm không phải thành viên là, nếu bạn tham chiếu nó từ bên ngoài lớp, bạn phải chỉ định phạm vi bằng cách sử dụng ::toán tử phân giải phạm vi. Ví dụ, để lấy con trỏ hàm, sử dụng &myclass::myfunctionvà gọi nó là use myclass::myfunction (arg);.

Điều này khá phổ biến khi sử dụng các API Win32 cũ, ban đầu được thiết kế cho C thay vì C ++. Tất nhiên trong trường hợp đó, thông số thường là LPARAM hoặc tương tự hơn là một con trỏ và cần phải truyền một số.


'my Chức năng' không phải là một hàm bình thường nếu thông thường bạn muốn nói đến một hàm kiểu C. 'my Chức năng' được gọi chính xác hơn là một phương thức của lớp tôi. Các phương thức của một lớp không giống như các hàm bình thường ở chỗ chúng có một cái gì đó mà một hàm kiểu C không phải là con trỏ 'this'.
Eric

3
khuyên sử dụng boost là hà khắc. Có những lý do chính đáng để sử dụng con trỏ phương pháp. Tôi không ngại việc đề cập đến boost như một giải pháp thay thế nhưng ghét khi ai đó nói rằng ai đó nên sử dụng nó mà không biết tất cả sự thật. Tăng giá phải trả! Và nếu đây là một nền tảng nhúng thì nó có thể không phải là một lựa chọn khả thi. Ngoài ra, tôi thực sự thích bài viết của bạn.
Eric

@Eric - Về điểm thứ hai của bạn, tôi không định nói "bạn sẽ sử dụng Boost", và thực tế là bản thân tôi chưa bao giờ sử dụng Boost. Ý định (theo như tôi biết sau 3 năm) là mọi người nên tìm kiếm các giải pháp thay thế và liệt kê một vài khả năng. "Hoặc bất cứ điều gì" chỉ ra rằng một danh sách không có nghĩa là đầy đủ. Con trỏ thành viên có giá ở mức dễ đọc. Biểu diễn nguồn ngắn gọn của chúng cũng có thể che giấu chi phí thời gian chạy - cụ thể là một con trỏ thành viên đến một phương thức phải đối phó với cả phương thức không ảo và ảo, và phải biết điều đó.
Steve314,

@Eric - Không chỉ vậy, những vấn đề này là lý do cho sự không di động với con trỏ thành viên - Visual C ++, ít nhất là trong quá khứ, cần một số manh mối bổ sung về cách biểu diễn các loại con trỏ thành viên. Tôi sẽ sử dụng phương pháp tiếp cận hàm tĩnh cho một hệ thống nhúng - việc biểu diễn một con trỏ giống như bất kỳ con trỏ hàm nào khác, chi phí là rõ ràng và không có vấn đề về tính di động. Và cuộc gọi được bao bọc bởi hàm thành viên tĩnh biết (tại thời điểm biên dịch) liệu cuộc gọi có phải là ảo hay không - không cần kiểm tra thời gian chạy ngoài các tra cứu vtable thông thường cho các phương thức ảo.
Steve314

@Eric - về điểm đầu tiên của bạn - tôi biết rằng một hàm thành viên tĩnh không hoàn toàn giống với một hàm kiểu C (do đó "các vấn đề phạm vi sang một bên"), nhưng tôi có lẽ nên đưa vào tên.
Steve314,

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

2
Nên là: (pDog -> * doSomething) (); // nếu pDog là con trỏ // (pDog. * doSomething) (); // nếu pDog là một tham chiếu vì toán tử () có mức ưu tiên cao hơn thì -> * và. *.
Tomek 28/09/09

12

Ví dụ tối thiểu có thể chạy được

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

Biên dịch và chạy:

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

Đã thử nghiệm trong Ubuntu 18.04.

Bạn không thể thay đổi thứ tự của dấu ngoặc đơn hoặc bỏ qua chúng. Những điều sau đây không hoạt động:

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

GCC 9.2 sẽ không thành công với:

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

Chuẩn C ++ 11

.*->*là một toán tử đơn lẻ được giới thiệu trong C ++ cho mục đích này và không có trong C.

Bản nháp tiêu chuẩn C ++ 11 N3337 :

  • 2.13 "Toán tử và dấu chấm câu" có danh sách tất cả các toán tử, trong đó có .*->*.
  • 5.5 "Toán tử trỏ đến thành viên" giải thích những gì họ làm

11

Tôi đến đây để tìm hiểu cách tạo một con trỏ hàm (không phải con trỏ phương thức) từ một phương thức nhưng không có câu trả lời nào ở đây cung cấp giải pháp. Đây là những gì tôi nghĩ ra:

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>

Vì vậy, đối với ví dụ của bạn, bây giờ bạn sẽ làm:

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

Chỉnh sửa:
Sử dụng C ++ 17, có một giải pháp thậm chí còn tốt hơn:

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

có thể được sử dụng trực tiếp mà không cần macro:

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

Đối với các phương thức có bổ ngữ như constbạn có thể cần thêm một số chuyên môn như:

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

Lý do tại sao bạn không thể sử dụng con trỏ hàm để gọi các hàm thành viên là các con trỏ hàm thông thường thường chỉ là địa chỉ bộ nhớ của hàm.

Để gọi một hàm thành viên, bạn cần biết hai điều:

  • Gọi chức năng thành viên nào
  • Phiên bản nào nên được sử dụng (có chức năng thành viên)

Con trỏ hàm thông thường không thể lưu trữ cả hai. Con trỏ hàm thành viên trong C ++ được sử dụng để lưu trữ a), đó là lý do tại sao bạn cần chỉ định thể hiện một cách rõ ràng khi gọi một con trỏ hàm thành viên.


1
Tôi đã bỏ phiếu điều này nhưng sẽ thêm một điểm làm rõ trong trường hợp OP không biết bạn đang đề cập đến điều gì bằng "trường hợp nào". Tôi sẽ mở rộng để giải thích con trỏ 'this' vốn có.
Eric

6

Một con trỏ hàm đến một thành viên lớp là một vấn đề thực sự phù hợp với việc sử dụng hàm boost ::. Ví dụ nhỏ:

#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

Để tạo một đối tượng mới, bạn có thể sử dụng vị trí mới, như đã đề cập ở trên, hoặc yêu cầu lớp của bạn triển khai phương thức clone () để tạo bản sao của đối tượng. Sau đó, bạn có thể gọi phương thức sao chép này bằng cách sử dụng một con trỏ hàm thành viên như đã giải thích ở trên để tạo các thể hiện mới của đối tượng. Ưu điểm của clone là đôi khi bạn có thể đang làm việc với một con trỏ đến một lớp cơ sở mà bạn không biết kiểu của đối tượng. Trong trường hợp này, phương thức clone () có thể dễ sử dụng hơn. Ngoài ra, clone () sẽ cho phép bạn sao chép trạng thái của đối tượng nếu đó là những gì bạn muốn.


nhái có thể đắt và OP có thể muốn tránh chúng nếu hiệu suất là một vấn đề hoặc một số mối quan tâm.
Eric

0

Tôi đã làm điều này với std :: function và std :: bind ..

Tôi đã viết lớp EventManager này lưu trữ một vectơ của các trình xử lý trong một bản đồ không có thứ tự ánh xạ các loại sự kiện (chỉ là const unsigned int, tôi có một enum không gian tên lớn trong số đó) vào một vector trình xử lý cho loại sự kiện đó.

Trong lớp EventManagerTests của mình, tôi đã thiết lập một trình xử lý sự kiện, như sau:

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

Đây là hàm 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;
}

Đây là định nghĩa kiểu EventHandler:

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

Sau đó, quay lại EventManagerTests :: RaiseEvent, tôi thực hiện điều này:

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

Đây là mã cho 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);
            }
        );
    }
}

Những công việc này. Tôi nhận được cuộc gọi trong EventManagerTests :: OnKeyDown. Tôi phải xóa các vectơ đến thời gian dọn dẹp, nhưng một khi tôi làm điều đó thì không có rò rỉ. Việc tạo một sự kiện mất khoảng 5 micro giây trên máy tính của tôi, vào khoảng năm 2008. Không chính xác là siêu nhanh, nhưng. Đủ công bằng miễn là tôi biết điều đó và tôi không sử dụng nó trong mã cực kỳ nóng.

Tôi muốn tăng tốc nó bằng cách cuộn std :: function và std :: bind của riêng mình, và có thể sử dụng một mảng mảng thay vì một bản đồ vectơ không có thứ tự, nhưng tôi chưa tìm ra cách lưu trữ một hàm thành viên con trỏ và gọi nó từ mã không biết gì về lớp được gọi. Câu trả lời của Eyelash trông rất thú vị ..

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.