Làm cách nào tôi có thể chuyển một hàm thành viên trong đó một hàm miễn phí được mong đợi?


122

Câu hỏi là như sau: hãy xem xét đoạn mã này:

#include <iostream>


class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a();

    function1(&test);
    function1(&aClass::aTest); // <-- How should I point to a's aClass::test function?

    return 0;
}

Làm cách nào để sử dụng a's aClass::testlàm đối số function1? Tôi bị mắc kẹt trong việc này.

Tôi muốn truy cập một thành viên của lớp học.


1
Hãy xem câu trả lời này stackoverflow.com/questions/2402579/… và cũng như Câu hỏi thường gặp về C ++ này parashift.com/c++-faq/pointers-to-members.html
amdn 30/09/12

16
Đây hoàn toàn không phải là một bản sao (ít nhất là không phải của câu hỏi cụ thể được liên kết). Câu hỏi đó là về cách khai báo một thành viên là một con trỏ đến một hàm; đây là về cách chuyển một con trỏ đến một hàm thành viên không tĩnh dưới dạng một tham số.
CarLuva

Câu trả lời:


144

Không có gì sai khi sử dụng con trỏ hàm. Tuy nhiên, con trỏ đến các hàm thành viên không tĩnh không giống như các con trỏ hàm thông thường: các hàm thành viên cần được gọi trên một đối tượng được truyền như một đối số ngầm định cho hàm. Chữ ký của chức năng thành viên của bạn ở trên, do đó

void (aClass::*)(int, int)

thay vì loại bạn cố gắng sử dụng

void (*)(int, int)

Một cách tiếp cận có thể bao gồm việc tạo hàm thành viên statictrong trường hợp đó nó không yêu cầu bất kỳ đối tượng nào được gọi và bạn có thể sử dụng nó với kiểu void (*)(int, int).

Nếu bạn cần truy cập bất kỳ thành viên không tĩnh nào trong lớp của mình bạn cần gắn với con trỏ hàm, ví dụ: vì hàm là một phần của giao diện C, tùy chọn tốt nhất của bạn là luôn chuyển một void*hàm cho hàm của bạn, lấy con trỏ hàm và gọi thành viên của bạn thông qua một hàm chuyển tiếp lấy một đối tượng từ void*và sau đó gọi hàm thành viên.

Trong một giao diện C ++ thích hợp, bạn có thể muốn xem hàm của bạn có đối số mẫu cho các đối tượng hàm để sử dụng các kiểu lớp tùy ý. Nếu việc sử dụng một giao diện mẫu là không mong muốn, bạn nên sử dụng một cái gì đó như std::function<void(int, int)>: bạn có thể tạo một đối tượng chức năng có thể gọi phù hợp cho chúng, ví dụ: sử dụng std::bind().

Các phương pháp tiếp cận kiểu an toàn bằng cách sử dụng đối số mẫu cho kiểu lớp hoặc kiểu phù hợp std::function<...>sẽ thích hợp hơn là sử dụng void*giao diện vì chúng loại bỏ khả năng xảy ra lỗi do truyền sang kiểu sai.

Để làm rõ cách sử dụng con trỏ hàm để gọi một hàm thành viên, đây là một ví dụ:

// the function using the function pointers:
void somefunction(void (*fptr)(void*, int, int), void* context) {
    fptr(context, 17, 42);
}

void non_member(void*, int i0, int i1) {
    std::cout << "I don't need any context! i0=" << i0 << " i1=" << i1 << "\n";
}

struct foo {
    void member(int i0, int i1) {
        std::cout << "member function: this=" << this << " i0=" << i0 << " i1=" << i1 << "\n";
    }
};

void forwarder(void* context, int i0, int i1) {
    static_cast<foo*>(context)->member(i0, i1);
}

int main() {
    somefunction(&non_member, 0);
    foo object;
    somefunction(&forwarder, &object);
}

Ok, tôi thích câu trả lời này! Bạn có thể vui lòng chỉ rõ ý của bạn với "gọi thành viên của bạn thông qua một hàm chuyển tiếp lấy một đối tượng từ khoảng trống * và sau đó gọi hàm thành viên", hoặc chia sẻ một liên kết hữu ích đến nó không? Cảm ơn
Jorge Leitao

Tôi nghĩ rằng tôi đã nhận nó. (Tôi đã chỉnh sửa bài đăng của bạn) Cảm ơn vì lời giải thích và ví dụ, thực sự hữu ích. Chỉ để xác nhận: đối với mọi chức năng thành viên mà tôi muốn trỏ tới, tôi phải tạo một người chuyển tiếp. Đúng?
Jorge Leitao

Vâng, vâng, đại loại. Tùy thuộc vào mức độ hiệu quả bạn đang sử dụng các mẫu, bạn có thể bắt đầu tạo các mẫu chuyển tiếp có thể hoạt động với các lớp và hàm thành viên khác nhau. Làm thế nào để làm được điều này sẽ là một chức năng riêng biệt, tôi nghĩ ;-)
Dietmar Kuhl

1
Tôi không thích câu trả lời này vì nó sử dụng void*, có nghĩa là bạn có thể nhận được những lỗi rất khó chịu, vì nó không được kiểm tra đánh máy nữa.
Superlokkus

@Superlokkus: bạn có thể vui lòng khai sáng cho chúng tôi một giải pháp thay thế được không?
Dietmar Kühl

88

Câu trả lời của @Pete Becker là tốt nhưng bạn cũng có thể làm điều đó mà không cần chuyển đối tượng classdưới dạng tham số rõ ràng đến function1trong C ++ 11:

#include <functional>
using namespace std::placeholders;

void function1(std::function<void(int, int)> fun)
{
    fun(1, 1);
}

int main (int argc, const char * argv[])
{
   ...

   aClass a;
   auto fp = std::bind(&aClass::test, a, _1, _2);
   function1(fp);

   return 0;
}

1
void function1(std::function<void(int, int)>)đúng không?
Deqing

2
Bạn cần đặt cho đối số hàm một tên biến và sau đó tên biến là những gì bạn thực sự truyền vào. Vì vậy: void function1(std::function<void(int, int)> functionToCall)và sau đó functionToCall(1,1);. Tôi đã cố gắng chỉnh sửa câu trả lời nhưng ai đó đã từ chối vì lý do nào đó không có ý nghĩa gì. Chúng tôi sẽ xem liệu nó có được ủng hộ vào một thời điểm nào đó hay không.
Kỹ sư Dorky

1
@DorkyEngineer Điều đó khá kỳ lạ, tôi nghĩ bạn phải đúng nhưng tôi không biết làm thế nào mà lỗi đó lại có thể không được chú ý trong một thời gian dài. Dù sao, tôi đã chỉnh sửa câu trả lời bây giờ.
Matt Phillips

2
Tôi tìm thấy bài đăng này nói rằng có một hình phạt hiệu suất nghiêm trọng từ hàm std ::.
kevin

2
@kevin, bạn có thể muốn sửa đổi nhận xét của mình, vì câu trả lời của bài đăng đó cho thấy một lỗ hổng trong điểm chuẩn.
Jorge Leitao

54

Một con trỏ tới hàm thành viên khác với một con trỏ tới hàm. Để sử dụng một hàm thành viên thông qua một con trỏ, bạn cần một con trỏ tới nó (hiển nhiên) và một đối tượng để áp dụng nó. Vì vậy, phiên bản thích hợp của function1sẽ là

void function1(void (aClass::*function)(int, int), aClass& a) {
    (a.*function)(1, 1);
}

và gọi nó là:

aClass a; // note: no parentheses; with parentheses it's a function declaration
function1(&aClass::test, a);

1
Cảm ơn rât nhiều. Tôi vừa phát hiện ra rằng số lượng dấu ngoặc ở đây: function1 (& (aClass :: test), a) hoạt động với MSVC2013 nhưng không hoạt động với gcc. gcc cần & trực tiếp ở phía trước của tên lớp (mà tôi thấy khó hiểu, bởi vì toán tử & mất địa chỉ của hàm, không phải của lớp)
kritzel_sw

Tôi nghĩ functiontrong void (aClass::*function)(int, int)là một loại, bởi vì nó là một loại trong typedef void (aClass::*function)(int, int).
Olumide

@Olumide - typedef int X;định nghĩa một kiểu; int X;tạo một đối tượng.
Pete Becker

11

Kể từ năm 2011, nếu bạn có thể thay đổi function1, hãy làm như vậy, như sau:

#include <functional>
#include <cstdio>

using namespace std;

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d", a, b, a + b);
    }
};

template <typename Callable>
void function1(Callable f)
{
    f(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d", a , b , a - b);
}

int main()
{
    aClass obj;

    // Free function
    function1(&test);

    // Bound member function
    using namespace std::placeholders;
    function1(std::bind(&aClass::aTest, obj, _1, _2));

    // Lambda
    function1([&](int a, int b) {
        obj.aTest(a, b);
    });
}

( bản demo trực tiếp )

Cũng lưu ý rằng tôi đã sửa định nghĩa đối tượng bị hỏng của bạn ( aClass a();khai báo một hàm).


2

Tôi đã hỏi một câu hỏi tương tự ( C ++ openframeworks chuyển void từ các lớp khác ) nhưng câu trả lời tôi tìm thấy rõ ràng hơn, vì vậy đây là lời giải thích cho các bản ghi trong tương lai:

nó dễ dàng hơn để sử dụng hàm std :: như trong:

 void draw(int grid, std::function<void()> element)

và sau đó gọi là:

 grid.draw(12, std::bind(&BarrettaClass::draw, a, std::placeholders::_1));

hoặc thậm chí dễ dàng hơn:

  grid.draw(12, [&]{a.draw()});

nơi bạn tạo một lambda gọi đối tượng chụp nó bằng tham chiếu


1
Cho rằng đây là CPP được gắn thẻ, tôi nghĩ đây là giải pháp sạch nhất. Tuy nhiên, chúng tôi có thể sử dụng một tham chiếu liên tục đến std::functionin drawthay vì sao chép nó trong mỗi cuộc gọi.
Sohaib

2
Không nên sử dụng trình giữ chỗ trong ví dụ này vì hàm không nhận bất kỳ tham số nào.
kroiz

1

Tôi đã đặt hàm thành viên là tĩnh và tất cả đều hoạt động:

#include <iostream>

class aClass
{
public:
    static void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

void function1(int a,int b,void function(int, int))
{
    function(a, b);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(10,12,test);
    function1(10,12,a.aTest); // <-- How should I point to a's aClass::test function?

    getchar();return 0;
}

Không chỉ giải quyết cho câu hỏi chính "Làm thế nào tôi có thể sử dụng a của aClass :: test làm đối số cho function1?" nhưng cũng nên tránh sửa đổi các biến private của lớp bằng cách sử dụng mã bên ngoài lớp
mathngineer

1

Không chắc tại sao giải pháp cực kỳ đơn giản này đã được thông qua:

#include <stdio.h>

class aClass
{
public:
    void aTest(int a, int b)
    {
        printf("%d + %d = %d\n", a, b, a + b);
    }
};

template<class C>
void function1(void (C::*function)(int, int), C& c)
{
    (c.*function)(1, 1);
}
void function1(void (*function)(int, int)) {
  function(1, 1);
}

void test(int a,int b)
{
    printf("%d - %d = %d\n", a , b , a - b);
}

int main (int argc, const char* argv[])
{
    aClass a;

    function1(&test);
    function1<aClass>(&aClass::aTest, a);
    return 0;
}

Đầu ra:

1 - 1 = 0
1 + 1 = 2

0

Nếu bạn thực sự không cần sử dụng cá thể a (nghĩa là bạn có thể làm cho nó tĩnh như câu trả lời của @mathengineer ), bạn có thể chỉ cần chuyển vào lambda không chụp. (phân rã thành con trỏ hàm)


#include <iostream>

class aClass
{
public:
   void aTest(int a, int b)
   {
      printf("%d + %d = %d", a, b, a + b);
   }
};

void function1(void (*function)(int, int))
{
    function(1, 1);
}

int main()
{
   //note: you don't need the `+`
   function1(+[](int a,int b){return aClass{}.aTest(a,b);}); 
}

Wandbox


lưu ý: nếu aClasstốn kém chi phí xây dựng hoặc có tác dụng phụ, đây có thể không phải là cách tốt.


-1

Bạn có thể ngừng đập đầu ngay bây giờ. Đây là trình bao bọc cho hàm thành viên để hỗ trợ các hàm hiện có lấy các hàm C thuần túy làm đối số. thread_localchỉ thị là chìa khóa ở đây.

http://cpp.sh/9jhk3

// Example program
#include <iostream>
#include <string>

using namespace std;

typedef int FooCooker_ (int);

// Existing function
extern "C" void cook_10_foo (FooCooker_ FooCooker) {
    cout << "Cooking 10 Foo ..." << endl;
    cout << "FooCooker:" << endl;
    FooCooker (10);
}

struct Bar_ {
    Bar_ (int Foo = 0) : Foo (Foo) {};
    int cook (int Foo) {
        cout << "This Bar got " << this->Foo << endl;
        if (this->Foo >= Foo) {
            this->Foo -= Foo;
            cout << Foo << " cooked" << endl;
            return Foo;
        } else {
            cout << "Can't cook " <<  Foo << endl;
            return 0;
        }
    }
    int Foo = 0;
};

// Each Bar_ object and a member function need to define
// their own wrapper with a global thread_local object ptr
// to be called as a plain C function.
thread_local static Bar_* BarPtr = NULL;
static int cook_in_Bar (int Foo) {
    return BarPtr->cook (Foo);
}

thread_local static Bar_* Bar2Ptr = NULL;
static int cook_in_Bar2 (int Foo) {
    return Bar2Ptr->cook (Foo);
}

int main () {
  BarPtr = new Bar_ (20);
  cook_10_foo (cook_in_Bar);

  Bar2Ptr = new Bar_ (40);
  cook_10_foo (cook_in_Bar2);

  delete BarPtr;
  delete Bar2Ptr;
  return 0;
}

Vui lòng bình luận về bất kỳ vấn đề nào với cách tiếp cận này.

Các câu trả lời khác không gọi được cácC hàm đơn giản hiện có : http://cpp.sh/8exun


3
Vì vậy, thay vì sử dụng std :: bind hoặc lambda để bao bọc phiên bản, bạn dựa vào một biến toàn cục. Tôi không thể thấy bất kỳ lợi thế nào của cách tiếp cận này so với các câu trả lời khác.
siêu

@super, Các câu trả lời khác không thể gọi các hàm hiện có sử dụng các hàm thuần túy Clàm đối số.
Necktwi

2
Câu hỏi là làm thế nào để gọi một hàm thành viên. Chuyển vào một chức năng miễn phí đã hoạt động cho OP trong câu hỏi. Bạn cũng không vượt qua bất cứ điều gì. Chỉ có các hàm được mã hóa cứng ở đây và một con trỏ Foo_ toàn cầu. Quy mô này như thế nào nếu bạn muốn gọi một hàm thành viên khác? Bạn sẽ phải viết lại các hàm cơ bản hoặc sử dụng các hàm khác nhau cho từng mục tiêu.
siêu
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.