Hàm gọi lại trong C ++


303

Trong C ++, khi nào và làm thế nào để bạn sử dụng chức năng gọi lại?

EDIT:
Tôi muốn xem một ví dụ đơn giản để viết hàm gọi lại.


[Điều này] ( thispulum.com/ Kẻ ) giải thích cơ bản về các hàm gọi lại rất tốt và dễ hiểu khái niệm này.
Anurag Singh

Câu trả lời:


449

Lưu ý: Hầu hết các câu trả lời bao gồm các con trỏ hàm có khả năng đạt được logic "gọi lại" trong C ++, nhưng cho đến ngày nay không phải là câu hỏi thuận lợi nhất tôi nghĩ.

Cuộc gọi lại là gì (?) Và tại sao nên sử dụng chúng (!)

Một cuộc gọi lại là một cuộc gọi (xem thêm xuống) được chấp nhận bởi một lớp hoặc chức năng, được sử dụng để tùy chỉnh logic hiện tại tùy thuộc vào cuộc gọi lại đó.

Một lý do để sử dụng gọi lại là để viết chung mã không phụ thuộc vào logic trong hàm được gọi và có thể được sử dụng lại với các cuộc gọi lại khác nhau.

Nhiều chức năng của thư viện thuật toán tiêu chuẩn <algorithm>sử dụng các cuộc gọi lại. Ví dụ: for_eachthuật toán áp dụng một cuộc gọi lại đơn phương cho mọi mục trong một phạm vi các vòng lặp:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
  for (; first != last; ++first) {
    f(*first);
  }
  return f;
}

có thể được sử dụng để tăng đầu tiên và sau đó in một vectơ bằng cách chuyển các tên gọi thích hợp chẳng hạn:

std::vector<double> v{ 1.0, 2.2, 4.0, 5.5, 7.2 };
double r = 4.0;
std::for_each(v.begin(), v.end(), [&](double & v) { v += r; });
std::for_each(v.begin(), v.end(), [](double v) { std::cout << v << " "; });

mà in

5 6.2 8 9.5 11.2

Một ứng dụng khác của cuộc gọi lại là thông báo cho người gọi về một số sự kiện nhất định cho phép một lượng linh hoạt thời gian tĩnh / biên dịch nhất định.

Cá nhân, tôi sử dụng thư viện tối ưu hóa cục bộ sử dụng hai cuộc gọi lại khác nhau:

  • Cuộc gọi lại đầu tiên được gọi nếu giá trị hàm và độ dốc dựa trên vectơ của các giá trị đầu vào là bắt buộc (gọi lại logic: xác định giá trị hàm / dẫn xuất độ dốc).
  • Cuộc gọi lại thứ hai được gọi một lần cho mỗi bước thuật toán và nhận thông tin nhất định về sự hội tụ của thuật toán (cuộc gọi lại thông báo).

Do đó, người thiết kế thư viện không chịu trách nhiệm quyết định những gì xảy ra với thông tin được cung cấp cho lập trình viên thông qua cuộc gọi lại thông báo và anh ta không cần lo lắng về cách xác định giá trị hàm thực sự bởi vì chúng được cung cấp bởi cuộc gọi lại logic. Làm cho những điều đó đúng là một nhiệm vụ do người dùng thư viện và giữ cho thư viện mỏng và chung chung hơn.

Hơn nữa, cuộc gọi lại có thể cho phép hành vi thời gian chạy động.

Hãy tưởng tượng một loại lớp công cụ trò chơi nào đó có chức năng được kích hoạt, mỗi lần người dùng nhấn một nút trên bàn phím và một bộ chức năng kiểm soát hành vi trò chơi của bạn. Với các cuộc gọi lại, bạn có thể (tái) quyết định trong thời gian chạy hành động nào sẽ được thực hiện.

void player_jump();
void player_crouch();

class game_core
{
    std::array<void(*)(), total_num_keys> actions;
    // 
    void key_pressed(unsigned key_id)
    {
        if(actions[key_id]) actions[key_id]();
    }
    // update keybind from menu
    void update_keybind(unsigned key_id, void(*new_action)())
    {
        actions[key_id] = new_action;
    }
};

Ở đây, hàm key_pressedsử dụng các hàm gọi lại được lưu trữ actionsđể có được hành vi mong muốn khi nhấn một phím nhất định. Nếu người chơi chọn thay đổi nút để nhảy, động cơ có thể gọi

game_core_instance.update_keybind(newly_selected_key, &player_jump);

và do đó thay đổi hành vi của một cuộc gọi đến key_pressed(mà các cuộc gọi player_jump) một khi nút này được nhấn vào lần tiếp theo ingame.

Là gì callables trong C ++ (11)?

Xem các khái niệm C ++: Có thể gọi được trên cppreference để có mô tả chính thức hơn.

Chức năng gọi lại có thể được nhận ra theo nhiều cách trong C ++ (11) do một số thứ khác nhau hóa ra có thể gọi được * :

  • Các con trỏ hàm (bao gồm các con trỏ tới các hàm thành viên)
  • std::function các đối tượng
  • Biểu thức Lambda
  • Biểu thức ràng buộc
  • Các đối tượng hàm (các lớp với toán tử gọi hàm quá tải operator())

* Lưu ý: Con trỏ tới các thành viên dữ liệu cũng có thể gọi được nhưng không có chức năng nào được gọi cả.

Một số cách quan trọng để viết chi tiết gọi lại

  • X.1 "Viết" một cuộc gọi lại trong bài viết này có nghĩa là cú pháp để khai báo và đặt tên cho loại gọi lại.
  • X.2 "Gọi" một cuộc gọi lại đề cập đến cú pháp để gọi các đối tượng đó.
  • X.3 "Sử dụng" một cuộc gọi lại có nghĩa là cú pháp khi truyền đối số cho hàm bằng cách sử dụng một cuộc gọi lại.

Lưu ý: Kể từ C ++ 17, một cuộc gọi như f(...)có thể được viết vì std::invoke(f, ...)nó cũng xử lý con trỏ tới trường hợp thành viên.

1. Con trỏ hàm

Một con trỏ hàm là 'đơn giản nhất' (về tính tổng quát; về khả năng đọc được cho là tệ nhất) loại gọi lại có thể có.

Chúng ta có một chức năng đơn giản foo:

int foo (int x) { return 2+x; }

1.1 Viết một con trỏ hàm / ký hiệu loại

Một loại con trỏ hàm có ký hiệu

return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)

trong đó một kiểu con trỏ hàm được đặt tên sẽ trông như thế nào

return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int); 

// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo; 
// can alternatively be written as 
f_int_t foo_p = &foo;

Các usingtuyên bố cho chúng ta lựa chọn để làm cho mọi việc dễ đọc hơn một chút, kể từ khi typedefcho f_int_tcũng có thể được viết như sau:

using f_int_t = int(*)(int);

Trong đó (ít nhất là đối với tôi) thì rõ ràng hơn f_int_tlà bí danh kiểu mới và việc nhận dạng kiểu con trỏ hàm cũng dễ dàng hơn

Và một khai báo của một hàm sử dụng một hàm gọi lại của kiểu con trỏ hàm sẽ là:

// foobar having a callback argument named moo of type 
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);

1.2 Ký hiệu cuộc gọi lại

Ký hiệu cuộc gọi tuân theo cú pháp gọi hàm đơn giản:

int foobar (int x, int (*moo)(int))
{
    return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
    return x + moo(x); // function pointer moo called using argument x
}

1.3 Ký hiệu sử dụng gọi lại và các loại tương thích

Một hàm gọi lại lấy một con trỏ hàm có thể được gọi bằng cách sử dụng các con trỏ hàm.

Sử dụng một hàm có một hàm gọi lại con trỏ hàm khá đơn giản:

 int a = 5;
 int b = foobar(a, foo); // call foobar with pointer to foo as callback
 // can also be
 int b = foobar(a, &foo); // call foobar with pointer to foo as callback

1.4 Ví dụ

Một hàm ca được viết mà không dựa vào cách gọi lại hoạt động:

void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

nơi gọi lại có thể

int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }

được sử dụng như

int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};

2. Con trỏ đến chức năng thành viên

Một con trỏ tới hàm thành viên (của một số lớp C) là một loại con trỏ hàm đặc biệt (và thậm chí phức tạp hơn) đòi hỏi một đối tượng kiểu Cđể hoạt động.

struct C
{
    int y;
    int foo(int x) const { return x+y; }
};

2.1 Viết con trỏ tới ký hiệu hàm / kiểu thành viên

Một con trỏ tới kiểu hàm thành viên cho một số lớp Tcó ký hiệu

// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)

trong đó một con trỏ được đặt tên cho hàm thành viên sẽ - tương tự như con trỏ hàm - trông như thế này:

return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x); 

// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;

Ví dụ: Khai báo một hàm lấy một con trỏ tới hàm gọi lại thành viên làm một trong các đối số của nó:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2 Ký hiệu cuộc gọi lại

Con trỏ tới hàm thành viên của Ccó thể được gọi, đối với một đối tượng kiểu Cbằng cách sử dụng các hoạt động truy cập thành viên trên con trỏ bị hủy đăng ký. Lưu ý: Yêu cầu dấu ngoặc đơn!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

Lưu ý: Nếu một con trỏ Ccó sẵn, cú pháp là tương đương (trong đó con trỏ Cphải được hủy đăng ký):

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3 Ký hiệu sử dụng gọi lại và các loại tương thích

Một hàm gọi lại lấy một con trỏ hàm thành viên của lớp Tcó thể được gọi bằng cách sử dụng một con trỏ hàm thành viên của lớp T.

Sử dụng một hàm đưa con trỏ đến hàm gọi lại thành viên là - tương tự như con trỏ hàm - cũng khá đơn giản:

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::functionđối tượng (tiêu đề <functional>)

Các std::functionlớp học là một chức năng wrapper đa hình để lưu trữ, sao chép hoặc invoke callables.

3.1 Viết std::functionký hiệu đối tượng / loại

Loại std::functionđối tượng lưu trữ một cuộc gọi có thể gọi là:

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2 Ký hiệu cuộc gọi lại

Lớp std::functionđã operator()định nghĩa có thể được sử dụng để gọi mục tiêu của nó.

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3 Ký hiệu sử dụng gọi lại và các loại tương thích

Cuộc std::functiongọi lại chung chung hơn con trỏ hàm hoặc con trỏ đến hàm thành viên vì các loại khác nhau có thể được truyền và chuyển đổi hoàn toàn thành một std::functionđối tượng.

3.3.1 Con trỏ hàm và con trỏ tới hàm thành viên

Một con trỏ hàm

int a = 2;
int b = stdf_foobar(a, &foo);
// b == 6 ( 2 + (2+2) )

hoặc một con trỏ tới hàm thành viên

int a = 2;
C my_c{7}; // aggregate initialization
int b = stdf_C_foobar(a, c, &C::foo);
// b == 11 == ( 2 + (7+2) )

có thể được sử dụng.

3.3.2 Biểu thức Lambda

Một bao đóng không tên từ biểu thức lambda có thể được lưu trữ trong một std::functionđối tượng:

int a = 2;
int c = 3;
int b = stdf_foobar(a, [c](int x) -> int { return 7+c*x; });
// b == 15 ==  a + (7*c*a) == 2 + (7+3*2)

3.3.3 std::bindbiểu thức

Kết quả của một std::bindbiểu thức có thể được thông qua. Ví dụ: bằng cách liên kết các tham số với một lệnh gọi con trỏ hàm:

int foo_2 (int x, int y) { return 9*x + y; }
using std::placeholders::_1;

int a = 2;
int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
// b == 23 == 2 + ( 9*2 + 3 )
int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
// c == 49 == 2 + ( 9*5 + 2 )

Trường hợp các đối tượng cũng có thể được liên kết làm đối tượng cho việc gọi con trỏ tới các hàm thành viên:

int a = 2;
C const my_c{7}; // aggregate initialization
int b = stdf_foobar(a, std::bind(&C::foo, my_c, _1));
// b == 1 == 2 + ( 2 + 7 )

3.3.4 Đối tượng chức năng

Các đối tượng của các lớp có operator()quá tải thích hợp cũng có thể được lưu trữ bên trong một std::functionđối tượng.

struct Meow
{
  int y = 0;
  Meow(int y_) : y(y_) {}
  int operator()(int x) { return y * x; }
};
int a = 11;
int b = stdf_foobar(a, Meow{8});
// b == 99 == 11 + ( 8 * 11 )

3,4 Ví dụ

Thay đổi ví dụ con trỏ hàm để sử dụng std::function

void stdf_tranform_every_int(int * v, unsigned n, std::function<int(int)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

cung cấp nhiều tiện ích hơn cho chức năng đó bởi vì (xem 3.3) chúng ta có nhiều khả năng sử dụng nó hơn:

// using function pointer still possible
int a[5] = {1, 2, 3, 4, 5};
stdf_tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};

// use it without having to write another function by using a lambda
stdf_tranform_every_int(&a[0], 5, [](int x) -> int { return x/2; });
// now a == {1, 2, 3, 4, 5}; again

// use std::bind :
int nine_x_and_y (int x, int y) { return 9*x + y; }
using std::placeholders::_1;
// calls nine_x_and_y for every int in a with y being 4 every time
stdf_tranform_every_int(&a[0], 5, std::bind(nine_x_and_y, _1, 4));
// now a == {13, 22, 31, 40, 49};

4. Kiểu gọi lại tạm thời

Sử dụng các mẫu, mã gọi lại gọi lại có thể còn chung chung hơn so với sử dụng std::functioncác đối tượng.

Lưu ý rằng các mẫu là một tính năng thời gian biên dịch và là một công cụ thiết kế cho tính đa hình thời gian biên dịch. Nếu hành vi động thời gian chạy phải đạt được thông qua các cuộc gọi lại, các mẫu sẽ giúp nhưng chúng sẽ không tạo ra động lực thời gian chạy.

4.1 Viết (ký hiệu loại) và gọi lại cuộc gọi theo khuôn mẫu

Tổng quát hóa tức là std_ftransform_every_intmã từ phía trên thậm chí có thể đạt được bằng cách sử dụng các mẫu:

template<class R, class T>
void stdf_transform_every_int_templ(int * v,
  unsigned const n, std::function<R(T)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

với cú pháp thậm chí tổng quát hơn (cũng như dễ nhất) cho loại gọi lại là một đối số khuôn mẫu đơn giản, dễ bị suy diễn:

template<class F>
void transform_every_int_templ(int * v, 
  unsigned const n, F f)
{
  std::cout << "transform_every_int_templ<" 
    << type_name<F>() << ">\n";
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = f(v[i]);
  }
}

Lưu ý: Đầu ra đi kèm in tên loại được suy ra cho loại templated F. Việc thực hiện type_nameđược đưa ra ở cuối bài này.

Việc triển khai chung nhất cho phép chuyển đổi đơn nhất của một phạm vi là một phần của thư viện chuẩn, cụ thể là std::transform, cũng được đặt theo khuôn mẫu đối với các kiểu lặp.

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
  UnaryOperation unary_op)
{
  while (first1 != last1) {
    *d_first++ = unary_op(*first1++);
  }
  return d_first;
}

4.2 Ví dụ sử dụng các cuộc gọi lại templated và các loại tương thích

Các loại tương thích cho std::functionphương thức gọi lại templated stdf_transform_every_int_templgiống hệt với các loại đã đề cập ở trên (xem 3.4).

Tuy nhiên, sử dụng phiên bản templated, chữ ký của cuộc gọi lại được sử dụng có thể thay đổi một chút:

// Let
int foo (int x) { return 2+x; }
int muh (int const &x) { return 3+x; }
int & woof (int &x) { x *= 4; return x; }

int a[5] = {1, 2, 3, 4, 5};
stdf_transform_every_int_templ<int,int>(&a[0], 5, &foo);
// a == {3, 4, 5, 6, 7}
stdf_transform_every_int_templ<int, int const &>(&a[0], 5, &muh);
// a == {6, 7, 8, 9, 10}
stdf_transform_every_int_templ<int, int &>(&a[0], 5, &woof);

Lưu ý: std_ftransform_every_int(phiên bản không có templated; xem ở trên) không hoạt động foonhưng không sử dụng muh.

// Let
void print_int(int * p, unsigned const n)
{
  bool f{ true };
  for (unsigned i = 0; i < n; ++i)
  {
    std::cout << (f ? "" : " ") << p[i]; 
    f = false;
  }
  std::cout << "\n";
}

Tham số templated đơn giản của transform_every_int_templcó thể là mọi loại có thể gọi được.

int a[5] = { 1, 2, 3, 4, 5 };
print_int(a, 5);
transform_every_int_templ(&a[0], 5, foo);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, muh);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, woof);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, [](int x) -> int { return x + x + x; });
print_int(a, 5);
transform_every_int_templ(&a[0], 5, Meow{ 4 });
print_int(a, 5);
using std::placeholders::_1;
transform_every_int_templ(&a[0], 5, std::bind(foo_2, _1, 3));
print_int(a, 5);
transform_every_int_templ(&a[0], 5, std::function<int(int)>{&foo});
print_int(a, 5);

Các mã trên in:

1 2 3 4 5
transform_every_int_templ <int(*)(int)>
3 4 5 6 7
transform_every_int_templ <int(*)(int&)>
6 8 10 12 14
transform_every_int_templ <int& (*)(int&)>
9 11 13 15 17
transform_every_int_templ <main::{lambda(int)#1} >
27 33 39 45 51
transform_every_int_templ <Meow>
108 132 156 180 204
transform_every_int_templ <std::_Bind<int(*(std::_Placeholder<1>, int))(int, int)>>
975 1191 1407 1623 1839
transform_every_int_templ <std::function<int(int)>>
977 1193 1409 1625 1841

type_name thực hiện được sử dụng ở trên

#include <type_traits>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>

template <class T>
std::string type_name()
{
  typedef typename std::remove_reference<T>::type TR;
  std::unique_ptr<char, void(*)(void*)> own
    (abi::__cxa_demangle(typeid(TR).name(), nullptr,
    nullptr, nullptr), std::free);
  std::string r = own != nullptr?own.get():typeid(TR).name();
  if (std::is_const<TR>::value)
    r += " const";
  if (std::is_volatile<TR>::value)
    r += " volatile";
  if (std::is_lvalue_reference<T>::value)
    r += " &";
  else if (std::is_rvalue_reference<T>::value)
    r += " &&";
  return r;
}

35
@BogeyJammer: Trong trường hợp bạn không nhận thấy: Câu trả lời có hai phần. 1. Một lời giải thích chung về "cuộc gọi lại" với một ví dụ nhỏ. 2. Một danh sách toàn diện các tên gọi khác nhau và các cách viết mã bằng cách gọi lại. Bạn được hoan nghênh không đi sâu vào chi tiết hoặc đọc toàn bộ câu trả lời mà chỉ vì bạn không muốn có một cái nhìn chi tiết, đó không phải là trường hợp câu trả lời không hiệu quả hoặc "sao chép một cách tàn nhẫn". Chủ đề là "cuộc gọi lại c ++". Ngay cả khi phần 1 là ổn đối với OP, những người khác có thể thấy phần 2 hữu ích. Vui lòng chỉ ra bất kỳ sự thiếu thông tin hoặc chỉ trích mang tính xây dựng nào cho phần đầu tiên thay vì -1.
Nhà hóa học

1
Phần 1 không thân thiện với người mới bắt đầu và đủ rõ ràng. Tôi không thể xây dựng hơn bằng cách nói rằng nó đã không quản lý để học cho tôi một cái gì đó. Và phần 2, đã không được yêu cầu, tràn ngập trang và không có vấn đề gì mặc dù bạn giả vờ nó có ích mặc dù thực tế nó thường được tìm thấy trong tài liệu chuyên dụng nơi thông tin chi tiết như vậy được tìm kiếm ở nơi đầu tiên. Tôi chắc chắn giữ downvote. Một phiếu bầu đại diện cho ý kiến ​​cá nhân vì vậy hãy chấp nhận và tôn trọng nó.
Bogey Jammer

24
@BogeyJammer Tôi không mới lập trình nhưng tôi chưa quen với "c ++ hiện đại". Câu trả lời này cho tôi bối cảnh chính xác mà tôi cần để lý giải về các cuộc gọi lại vai trò, cụ thể là, c ++. OP có thể không yêu cầu nhiều ví dụ, nhưng theo thông lệ trên SO, trong một nhiệm vụ không bao giờ kết thúc để giáo dục một thế giới ngu ngốc, để liệt kê tất cả các giải pháp có thể cho một câu hỏi. Nếu nó đọc quá nhiều như một cuốn sách, lời khuyên duy nhất tôi có thể cung cấp là thực hành một chút bằng cách đọc một vài trong số chúng .
dcow

int b = foobar(a, foo); // call foobar with pointer to foo as callback, đây là một lỗi đánh máy phải không? foonên là một con trỏ để làm việc này AFAIK.
konoufo

@konoufo: [conv.func]của tiêu chuẩn C ++ 11 nói: " Một giá trị của loại chức năng T có thể được chuyển đổi thành một giá trị của con trỏ kiểu kiểu thành T. Kết quả là một con trỏ tới hàm. "Đây là một chuyển đổi tiêu chuẩn và như vậy xảy ra ngầm. Người ta có thể (tất nhiên) sử dụng con trỏ hàm ở đây.
Nhà hóa học

160

Ngoài ra còn có cách thực hiện cuộc gọi lại C: con trỏ hàm

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

Bây giờ nếu bạn muốn truyền vào các phương thức lớp dưới dạng gọi lại, các khai báo cho các con trỏ hàm đó có các khai báo phức tạp hơn, ví dụ:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}

1
Có một lỗi trong ví dụ phương thức lớp. Lời mời phải là: (ví dụ. * Gọi lại) (1.0f)
CarlJohnson

Cảm ơn bạn đã chỉ ra rằng. Tôi sẽ thêm cả hai để minh họa việc gọi qua một đối tượng và thông qua một con trỏ đối tượng.
Ramon Zarazua B.

3
Điều này có nhược điểm từ hàm std :: tr1: trong đó hàm gọi lại được gõ theo từng lớp; điều này làm cho việc sử dụng các cuộc gọi lại kiểu C trở nên không thực tế khi đối tượng thực hiện cuộc gọi không biết lớp của đối tượng được gọi.
bleater

Làm thế nào tôi có thể làm điều đó mà không cần typedefloại gọi lại? Nó thậm chí có thể?
Tomáš Zato - Phục hồi Monica

1
Có bạn có thể. typedefchỉ là cú pháp đường để làm cho nó dễ đọc hơn. Nếu không typedef, định nghĩa của DoWorkObject cho các con trỏ hàm sẽ là : void DoWorkObject(int (*callback)(float)). Đối với con trỏ thành viên sẽ là:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.

68

Scott Meyers đưa ra một ví dụ hay:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

Tôi nghĩ rằng ví dụ nói lên tất cả.

std::function<> là cách viết "gọi lại" hiện đại của C ++.


1
Không quan tâm, cuốn sách nào SM đưa ra ví dụ này? Chúc mừng :)
sam-w

5
Tôi biết cái này đã cũ, nhưng vì tôi gần như đã bắt đầu làm cái này và nó đã không hoạt động với thiết lập của tôi (mingw), nếu bạn đang sử dụng phiên bản GCC <4.x, phương pháp này không được hỗ trợ. Một số phụ thuộc mà tôi đang sử dụng sẽ không được biên dịch mà không có nhiều công việc trong phiên bản gcc> = 4.0.1, vì vậy tôi bị mắc kẹt với việc sử dụng các hàm gọi lại kiểu C cũ, hoạt động tốt.
OzBarry

38

Hàm gọi lại là một phương thức được truyền vào một thường trình và được gọi tại một số điểm theo thói quen mà nó được truyền vào.

Điều này rất hữu ích để làm phần mềm tái sử dụng. Ví dụ: nhiều API hệ điều hành (như API Windows) sử dụng các cuộc gọi lại rất nhiều.

Ví dụ: nếu bạn muốn làm việc với các tệp trong một thư mục - bạn có thể gọi hàm API, với thói quen của riêng bạn và thói quen của bạn được chạy một lần cho mỗi tệp trong thư mục được chỉ định. Điều này cho phép API rất linh hoạt.


63
Câu trả lời này thực sự không phải là lập trình viên trung bình nói bất cứ điều gì anh ta không biết. Tôi đang học C ++ khi đang làm quen với nhiều ngôn ngữ khác. Những gì gọi lại nói chung là không quan tâm đến tôi.
Tomáš Zato - Phục hồi Monica

17

Câu trả lời được chấp nhận là rất hữu ích và khá toàn diện. Tuy nhiên, OP tuyên bố

Tôi muốn xem một ví dụ đơn giản để viết hàm gọi lại.

Vì vậy, ở đây bạn đi, từ C ++ 11 bạn có std::functionnên không cần con trỏ hàm và các công cụ tương tự:

#include <functional>
#include <string>
#include <iostream>

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

Ví dụ này là bằng cách nào đó thực tế, bởi vì bạn muốn gọi hàm print_hashesvới các triển khai khác nhau của hàm băm, vì mục đích này tôi đã cung cấp một hàm đơn giản. Nó nhận được một chuỗi, trả về một int (giá trị băm của chuỗi được cung cấp) và tất cả những gì bạn cần nhớ từ phần cú pháp là std::function<int (const std::string&)>mô tả chức năng đó như là một đối số đầu vào của hàm sẽ gọi nó.


Trong số tất cả những câu trả lời ở trên, câu trả lời này khiến tôi hiểu cuộc gọi lại là gì và cách sử dụng chúng. cảm ơn.
Mehar Charan Sahai

@MeharCharanSahai Vui mừng khi nghe nó :) Bạn được chào đón.
Miljen Mikic

9

Không có khái niệm rõ ràng về chức năng gọi lại trong C ++. Các cơ chế gọi lại thường được thực hiện thông qua các con trỏ hàm, các đối tượng functor hoặc các đối tượng gọi lại. Các lập trình viên phải thiết kế rõ ràng và thực hiện chức năng gọi lại.

Chỉnh sửa dựa trên phản hồi:

Mặc dù phản hồi tiêu cực câu trả lời này đã nhận được, nó không sai. Tôi sẽ cố gắng làm tốt hơn việc giải thích nơi tôi đến.

C và C ++ có mọi thứ bạn cần để thực hiện các hàm gọi lại. Cách phổ biến và tầm thường nhất để thực hiện chức năng gọi lại là truyền con trỏ hàm làm đối số hàm.

Tuy nhiên, hàm gọi lại và con trỏ hàm không đồng nghĩa. Con trỏ hàm là một cơ chế ngôn ngữ, trong khi hàm gọi lại là một khái niệm ngữ nghĩa. Con trỏ hàm không phải là cách duy nhất để thực hiện chức năng gọi lại - bạn cũng có thể sử dụng hàm functor và thậm chí các hàm ảo khác nhau trong vườn. Điều làm cho một hàm gọi một cuộc gọi lại không phải là cơ chế được sử dụng để xác định và gọi hàm, mà là ngữ cảnh và ngữ nghĩa của cuộc gọi. Nói một cái gì đó là một hàm gọi lại ngụ ý một sự tách biệt lớn hơn bình thường giữa chức năng gọi và chức năng cụ thể được gọi, một khớp nối khái niệm lỏng lẻo hơn giữa người gọi và callee, với người gọi có quyền kiểm soát rõ ràng đối với những gì được gọi.

Ví dụ: tài liệu .NET cho IFormatProvider nói rằng "GetFormat là một phương thức gọi lại" , mặc dù nó chỉ là một phương thức giao diện chạy. Tôi không nghĩ ai sẽ tranh luận rằng tất cả các cuộc gọi phương thức ảo là các hàm gọi lại. Điều làm cho GetFormat trở thành một phương thức gọi lại không phải là cơ chế về cách nó được truyền hoặc gọi, mà là ngữ nghĩa của người gọi chọn phương thức GetFormat của đối tượng sẽ được gọi.

Một số ngôn ngữ bao gồm các tính năng với ngữ nghĩa gọi lại rõ ràng, thường liên quan đến các sự kiện và xử lý sự kiện. Ví dụ, C # có loại sự kiện với cú pháp và ngữ nghĩa được thiết kế rõ ràng xung quanh khái niệm gọi lại. Visual Basic có mệnh đề Handles , nó tuyên bố rõ ràng một phương thức là hàm gọi lại trong khi trừu tượng hóa khái niệm đại biểu hoặc con trỏ hàm. Trong những trường hợp này, khái niệm ngữ nghĩa của một cuộc gọi lại được tích hợp vào chính ngôn ngữ.

C và C ++, mặt khác, không nhúng khái niệm ngữ nghĩa của các hàm gọi lại gần như rõ ràng. Các cơ chế là có, ngữ nghĩa tích hợp thì không. Bạn có thể thực hiện các chức năng gọi lại tốt, nhưng để có được thứ gì đó tinh vi hơn bao gồm ngữ nghĩa gọi lại rõ ràng, bạn phải xây dựng nó dựa trên những gì C ++ cung cấp, chẳng hạn như những gì Qt đã làm với Tín hiệu và Slots của họ .

Tóm lại, C ++ có những gì bạn cần để thực hiện các cuộc gọi lại, thường khá dễ dàng và tầm thường khi sử dụng các hàm con trỏ hàm. Những gì nó không có là các từ khóa và tính năng có ngữ nghĩa cụ thể cho các cuộc gọi lại, chẳng hạn như nâng cao , phát ra , Xử lý , sự kiện + = , v.v. Nếu bạn đến từ một ngôn ngữ có các loại yếu tố đó, hỗ trợ gọi lại riêng trong C ++ sẽ cảm thấy trung tính.


1
may mắn thay, đây không phải là câu trả lời đầu tiên tôi đọc khi tôi truy cập trang này, nếu không tôi sẽ trả lời ngay lập tức!
ubugnu

6

Các hàm gọi lại là một phần của tiêu chuẩn C, do đó cũng là một phần của C ++. Nhưng nếu bạn đang làm việc với C ++, tôi khuyên bạn nên sử dụng mẫu người quan sát thay thế: http://en.wikipedia.org/wiki/Observer_potype


1
Các hàm gọi lại không nhất thiết phải đồng nghĩa với thực thi một chức năng thông qua một con trỏ hàm được truyền dưới dạng đối số. Theo một số định nghĩa, hàm gọi lại thuật ngữ mang các ngữ nghĩa bổ sung để thông báo một số mã khác của điều gì đó vừa xảy ra, hoặc đó là thời gian mà một cái gì đó sẽ xảy ra. Từ quan điểm đó, một hàm gọi lại không phải là một phần của tiêu chuẩn C, nhưng có thể được thực hiện dễ dàng bằng cách sử dụng các con trỏ hàm, là một phần của tiêu chuẩn.
Darryl

3
"một phần của tiêu chuẩn C, do đó cũng là một phần của C ++." Đây là một sự hiểu lầm điển hình, nhưng dù sao cũng là một sự hiểu lầm :-)
Sự chuộc tội có giới hạn

Tôi phải đồng ý. Tôi sẽ để nó như vậy, vì nó sẽ chỉ gây ra nhiều nhầm lẫn nếu tôi thay đổi nó bây giờ. Tôi muốn nói rằng con trỏ hàm (!) Là một phần của tiêu chuẩn. Nói bất cứ điều gì khác với điều đó - tôi đồng ý - là sai lệch.
AudioDroid

Các hàm gọi lại là "một phần của tiêu chuẩn C" theo cách nào? Tôi không nghĩ rằng thực tế là nó hỗ trợ các hàm và con trỏ tới các hàm có nghĩa là nó đặc biệt hóa các cuộc gọi lại như một khái niệm ngôn ngữ. Ngoài ra, như đã đề cập, điều đó sẽ không liên quan trực tiếp đến C ++ ngay cả khi nó chính xác. Và nó đặc biệt không liên quan khi OP hỏi "khi nào và như thế nào" để sử dụng các cuộc gọi lại trong C ++ (một câu hỏi khập khiễng, quá rộng, nhưng dù sao), và câu trả lời của bạn là một lời khuyên chỉ liên kết để làm điều gì đó khác biệt.
gạch dưới

4

Xem định nghĩa ở trên trong đó nói rằng hàm gọi lại được chuyển sang một số hàm khác và tại một số điểm, nó được gọi.

Trong C ++, mong muốn có các hàm gọi lại gọi một phương thức lớp. Khi bạn làm điều này, bạn có quyền truy cập vào dữ liệu thành viên. Nếu bạn sử dụng cách C để xác định một cuộc gọi lại, bạn sẽ phải trỏ nó đến một hàm thành viên tĩnh. Điều này không phải là rất mong muốn.

Đây là cách bạn có thể sử dụng các cuộc gọi lại trong C ++. Giả sử 4 tập tin. Một cặp tệp .CPP / .H cho mỗi lớp. Lớp C1 là lớp có phương thức chúng ta muốn gọi lại. C2 gọi lại phương thức của C1. Trong ví dụ này, hàm gọi lại lấy 1 tham số mà tôi đã thêm cho người đọc vì lợi ích. Ví dụ này không hiển thị bất kỳ đối tượng nào được khởi tạo và sử dụng. Một trường hợp sử dụng cho việc triển khai này là khi bạn có một lớp đọc và lưu trữ dữ liệu vào không gian tạm thời và một lớp khác xử lý dữ liệu. Với chức năng gọi lại, đối với mỗi hàng dữ liệu đọc thì gọi lại có thể xử lý nó. Kỹ thuật này cắt giảm chi phí không gian tạm thời cần thiết. Nó đặc biệt hữu ích cho các truy vấn SQL trả về một lượng lớn dữ liệu mà sau đó phải được xử lý sau.

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}

0

Boost' signal2 cho phép bạn đăng ký các chức năng thành viên chung (không có mẫu!) Và theo cách an toàn cho chủ đề.

Ví dụ: Tín hiệu Chế độ xem Tài liệu có thể được sử dụng để triển khai các kiến ​​trúc Chế độ xem Tài liệu linh hoạt. Tài liệu sẽ chứa một tín hiệu mà mỗi chế độ xem có thể kết nối. Lớp Tài liệu sau định nghĩa một tài liệu văn bản đơn giản hỗ trợ các khung nhìn mulitple. Lưu ý rằng nó lưu một tín hiệu duy nhất mà tất cả các chế độ xem sẽ được kết nối.

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

Tiếp theo, chúng ta có thể bắt đầu xác định quan điểm. Lớp TextView sau đây cung cấp một khung nhìn đơn giản của văn bản tài liệu.

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};

0

Câu trả lời được chấp nhận là toàn diện nhưng liên quan đến câu hỏi tôi chỉ muốn đưa ra một ví dụ đơn giản ở đây. Tôi đã có một mã mà tôi đã viết nó từ lâu. tôi muốn đi ngang qua một cây theo cách theo thứ tự (nút trái rồi nút gốc rồi nút phải) và bất cứ khi nào tôi đạt đến một nút, tôi muốn có thể gọi một hàm tùy ý để nó có thể làm mọi thứ.

void inorder_traversal(Node *p, void *out, void (*callback)(Node *in, void *out))
{
    if (p == NULL)
        return;
    inorder_traversal(p->left, out, callback);
    callback(p, out); // call callback function like this.
    inorder_traversal(p->right, out, callback);
}


// Function like bellow can be used in callback of inorder_traversal.
void foo(Node *t, void *out = NULL)
{
    // You can just leave the out variable and working with specific node of tree. like bellow.
    // cout << t->item;
    // Or
    // You can assign value to out variable like below
    // Mention that the type of out is void * so that you must firstly cast it to your proper out.
    *((int *)out) += 1;
}
// This function use inorder_travesal function to count the number of nodes existing in the tree.
void number_nodes(Node *t)
{
    int sum = 0;
    inorder_traversal(t, &sum, foo);
    cout << sum;
}

 int main()
{

    Node *root = NULL;
    // What These functions perform is inserting an integer into a Tree data-structure.
    root = insert_tree(root, 6);
    root = insert_tree(root, 3);
    root = insert_tree(root, 8);
    root = insert_tree(root, 7);
    root = insert_tree(root, 9);
    root = insert_tree(root, 10);
    number_nodes(root);
}

1
Làm thế nào để trả lời câu hỏi?
Rajan Sharma

bạn biết câu trả lời được chấp nhận là chính xác và toàn diện và tôi nghĩ không có gì để nói chung. nhưng tôi đăng một ví dụ về việc sử dụng các hàm gọi lại của tôi.
Ehsan Ahmadi
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.