Câu hỏi Hàm gọi lại trong c ++


Trong c ++, khi nào và làm thế nào để bạn sử dụng một hàm gọi lại?

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


221
2018-02-19 17:16


gốc




Các câu trả lời:


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

Gọi lại là gì (?) Và tại sao sử dụng chúng (!)

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

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

Nhiều chức năng của thư viện thuật toán chuẩn <algorithm> sử dụng gọi lại. Ví dụ: for_each thuật toán áp dụng gọi lại một cách đơn nhất cho mọi mục trong một loạt các trình 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 số lượng đầu tiên và sau đó in một véc tơ bằng cách chuyển các cuộc gọi thích hợp, ví dụ:

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

bản in nào

5 6.2 8 9.5 11.2

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

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

  • Gọi lại đầu tiên được gọi nếu một giá trị hàm và gradient dựa trên một 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 gradient).
  • 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 được một số thông tin nhất định về sự hội tụ của thuật toán (gọi lại thông báo).

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

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

Hãy tưởng tượng một số loại động cơ trò chơi 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 callbacks bạn có thể (lại) quyết định thời gian chạy mà hành động 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 chức năng key_pressed sử dụng các cuộc gọi lại được lưu trữ trong actions để có được hành vi mong muốn khi nhấn một phím nào đó. 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 cuộc gọi đến key_pressed (mà các cuộc gọi player_jump) khi nút này được nhấn vào lần ingame tiếp theo.

Là gì cuộc gọi trong C ++ (11)?

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

Chức năng gọi lại có thể được thực hiện theo nhiều cách trong C ++ (11) vì nhiều thứ khác nhau hóa ra là có thể gọi *:

  • Con trỏ hàm (bao gồm con trỏ đến 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 có 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ó hàm nào được gọi.

Một số cách quan trọng để viết callbacks chi tiết

  • 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 kiểu 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 các đối số cho một 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 như std::invoke(f, ...) cũng xử lý con trỏ đến trường hợp thành viên.

1. Chức năng con trỏ

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

Hãy có một hàm đơn giản foo:

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

1.1 Viết ký hiệu kiểu con trỏ / kiểu

A kiểu con trỏ chức năng 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)

nơi một con trỏ hàm được đặt tên loại sẽ trông giống như

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 using tuyên bố cho chúng ta tùy chọn để làm cho mọi thứ dễ đọc hơn một chút, vì typedef cho f_int_t cũng có thể được viết dưới dạng:

using f_int_t = int(*)(int);

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

Và một tuyên bố của một hàm bằng cách sử dụng 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 gọi lại

Ký pháp gọi sau 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ý pháp 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 con trỏ hàm.

Sử dụng một hàm nhận một hàm gọi lại hàm con trỏ 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 phụ thuộc 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 có thể gọi lại

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 hàm thành viên

Một con trỏ đến hàm thành viên (của một số lớp C) là một kiểu con trỏ chức năng đặ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 kiểu / hàm thành viên

A con trỏ đến kiểu hàm thành viên cho một số lớp T có 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)

nơi một con trỏ được đặt tên cho hàm thành viên sẽ tương tự với con trỏ hàm giống như sau:

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 hàm nhận trỏ tới hàm gọi lại hàm thành viên là 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 gọi lại

Con trỏ đến hàm thành viên của C có thể được gọi, đối với một đối tượng kiểu C bằng cách sử dụng các hoạt động truy cập thành viên trên con trỏ bị bỏ qua. 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 con trỏ đến Ccó sẵn cú pháp là tương đương (nơi con trỏ đến C cũng phả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ý pháp sử dụng gọi lại và các loại tương thích

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

Sử dụng hàm nhận một con trỏ tới hàm gọi lại hàm thành viên là -in tương tự với 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::function class là một hàm bao hàm đa hình để lưu trữ, sao chép hoặc gọi các callables.

3.1 Viết một std::function ký hiệu đối tượng / loại

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

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 gọi lại

Lớp std::function có operator() được đị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 Cú pháp sử dụng gọi lại và các loại tương thích

Các std::function gọi lại là tổng quát hơn so với con trỏ hàm hoặc con trỏ đến hàm thành viên vì các kiểu khác nhau có thể được chuyển và ngầm được chuyển đổi thành std::function vật.

3.3.1 Con trỏ hàm và con trỏ đến 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ỏ đến 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 đóng cửa chưa được đặt tên từ một biểu thức lambda có thể được lưu trữ trong một std::function vật:

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::bind biểu thức

Kết quả của một std::bind biểu thức có thể được thông qua. Ví dụ bằng các tham số liên kết với một cuộc 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 )

Các đối tượng cũng có thể bị ràng buộc như đối tượng cho việc invokation của 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 hàm

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

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ụ về 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 hàm đó 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 theo kiểu Templated

Sử dụng các mẫu, mã gọi lại gọi lại có thể còn tổng quát hơn việc sử dụng std::function các đối tượng.

Lưu ý rằng các mẫu là một tính năng 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 là để đạt được thông qua callbacks, 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 gọi lại theo khuôn mẫu

Tổng quát tức là std_ftransform_every_int mã từ trên cao hơn nữa có thể đạt được bằng cách sử dụng 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 một cú pháp tổng quát hơn (cũng như dễ nhất) cho một kiểu gọi lại là một đối số đơn giản, được suy luận suy luậ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 được bao gồm in tên loại suy ra cho loại templated F. Việc thực hiện type_name được đưa ra ở cuối bài đăng này.

Việc thực hiện chung nhất cho việc 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 là templated đối với các loại lặp đi lặp lại.

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ụ bằng cách sử dụng các callback có khuôn mẫu và các kiểu tương thích

Các loại tương thích cho khuôn mẫu std::function phương thức gọi lại stdf_transform_every_int_templ giống với các loại được đề cập ở trên (xem 3.4).

Tuy nhiên, bằng cách 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);

Chú thích: std_ftransform_every_int (phiên bản không có khuôn mẫu; xem ở trên) không hoạt động với foo như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";
}

Các tham số đồng bằng templated của transform_every_int_templ có 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 triển khai đượ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;
}

281
2018-02-24 07:02



Địa ngục không, điều này giống như một bản sao tàn bạo / dán một chương đầy đủ của tài liệu cho các lập trình viên có kinh nghiệm. Nó không trả lời câu hỏi yêu cầu một ví dụ đơn giản và giải thích khái niệm gọi lại. Tôi là một người mới bắt đầu bản thân mình, và câu trả lời này là hoàn toàn không hiệu quả. - Bogey Jammer
@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ề "callbacks" với một ví dụ nhỏ. 2. Một danh sách đầy đủ các callables khác nhau và cách viết mã bằng cách sử dụng callbacks. Bạn được chào đón để không đi sâu vào chi tiết hoặc đọc toàn bộ câu trả lời nhưng chỉ vì bạn không muốn xem chi tiết, nó không phải là trường hợp câu trả lời không hiệu quả hoặc "được sao chép tàn bạo". Chủ đề là "c ++ callbacks". Ngay cả khi phần 1 là ok cho OP, những người khác có thể tìm thấy phần 2 hữu ích. Vui lòng chỉ ra bất kỳ sự thiếu thông tin hoặc phê bình mang tính xây dựng nào cho phần đầu tiên thay vì -1. - Pixelchemist
Phần 1 không phải là người mới bắt đầu thân thiện 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ý để tìm hiểu 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 tham gia câu hỏi ngay cả khi bạn giả vờ nó có ích không, 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 này được tìm kiếm ngay từ đầu. Tôi chắc chắn giữ cho downvote. Một phiếu bầu đại diện cho một ý kiến ​​cá nhân, vì vậy hãy chấp nhận và tôn trọng nó. - Bogey Jammer
@BogeyJammer Tôi không phải là người mới lập trình nhưng tôi mới làm quen với "c ++ hiện đại". Câu trả lời này mang lại cho tôi bối cảnh chính xác mà tôi cần phải giải thích về vai trò gọi lại, cụ thể là, c ++. OP có thể không yêu cầu nhiều ví dụ, nhưng nó là phong tục 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 của kẻ ngu, để 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à luyện tập một chút bằng cách đọc một vài trong số họ. - 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? foo nên là một con trỏ cho điều này để làm việc AFAIK. - konoufo


Ngoài ra còn có cách C làm callbacks: 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 như 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);
}

146
2018-02-19 19:06



Có lỗi trong ví dụ về phương thức lớp. Lệnh gọi phải là: (instance. * Callback) (1.0f) - CarlJohnson
Cảm ơn bạn đã chỉ ra rằng. Tôi sẽ thêm cả hai để minh họa cách gọi thông qua một đối tượng và thông qua một con trỏ đối tượng. - Ramon Zarazua B.
Điều này có bất lợi từ std :: tr1: function trong đó callback được gõ cho mỗi lớp; điều này làm cho nó không thực tế khi sử dụng callback kiểu C 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 typedefing kiểu gọi lại? Thậm chí có thể không? - Tomáš Zato
Có bạn có thể. typedef chỉ là cú pháp để làm cho nó dễ đọc hơn. Không có 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.


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 "hiện đại" viết các cuộc gọi lại C ++.


62
2018-02-19 17:23



Không quan tâm, cuốn sách nào SM đưa ra ví dụ này? Chúc mừng :) - sjwarner
@sjwarner "Hiệu quả C ++" - Karl von Moor
Tôi biết điều này là cũ, nhưng vì tôi gần như bắt đầu làm điều này và nó đã kết thúc không làm việc trên 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 tôi đang sử dụng sẽ không 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 callback kiểu C kiểu cũ tốt, hoạt động tốt. - OzBarry
@ KarlvonMoor Phiên bản nào? - Kimbluey


A Chức năng 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 vào một thời điểm nào đó theo thói quen mà nó được truyền vào.

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

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 một hàm API, với thường trình của riêng bạn, và thường trình 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 trở nên rất linh hoạt.


37
2018-02-19 17:20



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


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, các trạng thái OP

Tôi muốn thấy một ví dụ đơn giản để viết chức năng gọi lại.

Vì vậy, ở đây bạn đi, từ C ++ 11 bạn có std::function do đó không cần các 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 chức năng print_hashes với việc triển khai các hàm băm khác nhau, với mục đích này, tôi đã cung cấp một hàm đơn giản. Nó nhận 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&)> trong đó mô tả chức năng như là một đối số đầu vào của hàm sẽ gọi nó.


8
2017-07-01 20:36





Không có khái niệm rõ ràng về hàm 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ù những phản hồi tiêu cực câu trả lời này đã nhận được, nó không phải là sai. Tôi sẽ cố gắng giải thích tốt hơn về việc tôi đến từ đâu.

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

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

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 pháp giao diện run-of-the-mill. Tôi không nghĩ rằng bất cứ ai sẽ tranh luận rằng tất cả các cuộc gọi phương pháp ảo là chức năng gọi lại. Điều gì làm cho GetFormat là một phương thức gọi lại không phải là cơ chế của cách nó được truyền hoặc được gọi, nhưng 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 có 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ó biến cố gõ 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ó Xử lý mệnh đề, tuyên bố rõ ràng một phương thức để trở thành một hàm gọi lại trong khi trừu tượng hóa khái niệm của các đại biểu hoặc cá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 chức năng gọi lại gần như rõ ràng. Các cơ chế 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 một cái gì đó tinh vi hơn bao gồm các 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.

Tóm lại, C ++ có những gì bạn cần để thực hiện callbacks, thường khá dễ dàng và tầm thường bằng cách sử dụng con trỏ hàm. Những gì nó không có là từ khóa và các 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 + =, vv Nếu bạn đang đến từ một ngôn ngữ với các loại yếu tố, hỗ trợ gọi lại tự nhiên trong C ++ sẽ cảm thấy bị tổn thương.


7
2018-02-19 17:21



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 đã có thể lập tức bị trả lại! - ubugnu


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 quan sát thay thế: http://en.wikipedia.org/wiki/Observer_pattern


6
2017-12-14 16:45



Các chức năng gọi lại không nhất thiết phải đồng nghĩa với việc thực thi một hàm thông qua một con trỏ hàm được chuyển như một đối số. Theo một số định nghĩa, thuật ngữ gọi lại chức năng mang ngữ nghĩa bổ sung của thông báo một số mã khác của một cái 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 chức năng gọi lại không phải là một phần của tiêu chuẩn C, nhưng có thể dễ dàng thực hiện 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
"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 hiểu lầm điển hình, nhưng một sự hiểu lầm dù sao :-) - Limited Atonement
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 hơn nếu tôi thay đổi nó ngay bây giờ. Tôi có nghĩa là để 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à gây hiểu lầm. - AudioDroid
Trong những cách nào là chức năng gọi lại "một phần của tiêu chuẩn C"? Tôi không nghĩ thực tế là nó hỗ trợ các hàm và con trỏ đến các hàm có nghĩa là nó đặc biệt lồng tiếng gọi lại như một khái niệm ngôn ngữ. Bên cạnh đó, 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à làm thế nào" để sử dụng callbacks trong C ++ (một câu hỏi quá nhỏ, quá rộng, nhưng dù sao), và câu trả lời của bạn là một lời chỉ dẫn liên kết để làm một cái gì đó khác thay thế. - underscore_d


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

Trong C ++, bạn cần có các hàm gọi lại gọi 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 để định nghĩa 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 callbacks trong C ++. Giả sử 4 tệp. Một cặp tệp .CPP / .H cho mỗi lớp. Lớp C1 là lớp có phương thức mà 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 nhận 1 tham số mà tôi đã thêm vào vì độc giả. Ví dụ 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 gọi lại sau đó có thể xử lý nó. Kỹ thuật này cắt giảm chi phí của 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);
}

4
2017-07-22 23:23





Boost's singals2 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.

Ví dụ: Các tín hiệu xem tài liệu có thể được sử dụng để triển khai linh hoạt   Kiến trúc tài liệu-xem. Tài liệu sẽ chứa tín hiệu   mỗi chế độ xem có thể kết nối với nhau. 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 trữ một tín hiệu duy nhất mà tất cả các khung nhìn 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 định nghĩa các khung nhìn. Lớp TextView sau   cung cấp một cái 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
2017-08-15 19:50