Khi thực hiện chức năng gọi lại trong C ++, tôi vẫn nên sử dụng con trỏ hàm kiểu C:
void (*callbackFunc)(int);
Hoặc tôi nên sử dụng hàm std :::
std::function< void(int) > callbackFunc;
Khi thực hiện chức năng gọi lại trong C ++, tôi vẫn nên sử dụng con trỏ hàm kiểu C:
void (*callbackFunc)(int);
Hoặc tôi nên sử dụng hàm std :::
std::function< void(int) > callbackFunc;
Câu trả lời:
Tóm lại, sử dụngstd::function
trừ khi bạn có lý do để không.
Con trỏ chức năng có nhược điểm là không thể chụp một số bối cảnh. Ví dụ, bạn sẽ không thể chuyển một hàm lambda dưới dạng gọi lại để nắm bắt một số biến bối cảnh (nhưng nó sẽ hoạt động nếu nó không nắm bắt được bất kỳ). Do đó, việc gọi một biến thành viên của một đối tượng (tức là không tĩnh) là không thể, vì đối tượng ( this
-pulum) cần phải được bắt. (1)
std::function
(vì C ++ 11) chủ yếu để lưu trữ một chức năng (truyền nó xung quanh không yêu cầu nó được lưu trữ). Do đó, nếu bạn muốn lưu trữ cuộc gọi lại chẳng hạn trong một biến thành viên, đó có lẽ là lựa chọn tốt nhất của bạn. Nhưng ngoài ra, nếu bạn không lưu trữ nó, đó là một "lựa chọn đầu tiên" tốt mặc dù nó có nhược điểm là giới thiệu một số chi phí (rất nhỏ) khi được gọi (vì vậy trong tình huống rất quan trọng về hiệu năng, nó có thể là một vấn đề nhưng trong hầu hết nó không nên). Nó rất "phổ quát": nếu bạn quan tâm nhiều đến mã nhất quán và dễ đọc cũng như không muốn nghĩ về mọi lựa chọn bạn đưa ra (tức là muốn giữ cho nó đơn giản), hãy sử dụng std::function
cho mọi chức năng bạn chuyển qua.
Hãy suy nghĩ về một tùy chọn thứ ba: Nếu bạn sắp thực hiện một chức năng nhỏ, sau đó báo cáo điều gì đó thông qua chức năng gọi lại được cung cấp, hãy xem xét một tham số mẫu , sau đó có thể là bất kỳ đối tượng có thể gọi nào , ví dụ như con trỏ hàm, hàm functor, lambda, a std::function
, ... Hạn chế ở đây là chức năng (bên ngoài) của bạn trở thành một khuôn mẫu và do đó cần phải được thực hiện trong tiêu đề. Mặt khác, bạn có lợi thế là cuộc gọi đến cuộc gọi lại có thể được nội tuyến, vì mã máy khách của hàm (bên ngoài) của bạn "nhìn thấy" cuộc gọi đến cuộc gọi lại sẽ có thông tin loại chính xác.
Ví dụ cho phiên bản có tham số mẫu (viết &
thay vì &&
cho tiền C ++ 11):
template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
...
callback(...);
...
}
Như bạn có thể thấy trong bảng sau, tất cả chúng đều có những ưu điểm và nhược điểm:
+-------------------+--------------+---------------+----------------+
| | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture | no(1) | yes | yes |
| context variables | | | |
+-------------------+--------------+---------------+----------------+
| no call overhead | yes | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be inlined | no | no | yes |
| (see comments) | | | |
+-------------------+--------------+---------------+----------------+
| can be stored | yes | yes | no(2) |
| in class member | | | |
+-------------------+--------------+---------------+----------------+
| can be implemented| yes | yes | no |
| outside of header | | | |
+-------------------+--------------+---------------+----------------+
| supported without | yes | no(3) | yes |
| C++11 standard | | | |
+-------------------+--------------+---------------+----------------+
| nicely readable | no | yes | (yes) |
| (my opinion) | (ugly type) | | |
+-------------------+--------------+---------------+----------------+
(1) Giải pháp tồn tại để khắc phục giới hạn này, ví dụ: truyền dữ liệu bổ sung dưới dạng tham số tiếp theo cho hàm (bên ngoài) của bạn: myFunction(..., callback, data)
sẽ gọi callback(data)
. Đó là "gọi lại với các đối số" theo kiểu C, có thể có trong C ++ (và bằng cách được sử dụng nhiều trong API WIN32) nhưng nên tránh vì chúng ta có các tùy chọn tốt hơn trong C ++.
(2) Trừ khi chúng ta đang nói về một mẫu lớp, tức là lớp mà bạn lưu trữ chức năng là một mẫu. Nhưng điều đó có nghĩa là về phía máy khách, loại hàm quyết định loại đối tượng lưu trữ cuộc gọi lại, gần như không bao giờ là một tùy chọn cho các trường hợp sử dụng thực tế.
(3) Đối với tiền C ++ 11, hãy sử dụng boost::function
std::function
kiểu xóa nếu bạn cần lưu trữ bên ngoài ngay lập tức gọi là bối cảnh).
void (*callbackFunc)(int);
có thể là một chức năng gọi lại kiểu C, nhưng nó là một chức năng không thể sử dụng khủng khiếp với thiết kế kém.
Một cuộc gọi lại kiểu C được thiết kế tốt trông giống như void (*callbackFunc)(void*, int);
- nó có một void*
cho phép mã thực hiện cuộc gọi lại để duy trì trạng thái ngoài chức năng. Không làm điều này buộc người gọi phải lưu trữ nhà nước trên toàn cầu, đó là bất lịch sự.
std::function< int(int) >
kết thúc là đắt hơn một chút so với điều khoản int(*)(void*, int)
trong hầu hết các triển khai. Tuy nhiên, một số trình biên dịch nội tuyến khó hơn. Có các std::function
triển khai nhân bản mà các chi phí gọi con trỏ hàm đối thủ (xem 'đại biểu nhanh nhất có thể', v.v.) có thể đi vào thư viện.
Bây giờ, khách hàng của hệ thống gọi lại thường cần thiết lập tài nguyên và xử lý chúng khi gọi lại được tạo và xóa và để ý đến thời gian gọi lại. void(*callback)(void*, int)
không cung cấp điều này.
Đôi khi điều này có sẵn thông qua cấu trúc mã (cuộc gọi lại có thời gian giới hạn) hoặc thông qua các cơ chế khác (cuộc gọi lại không đăng ký và tương tự).
std::function
cung cấp một phương tiện để quản lý trọn đời hạn chế (bản sao cuối cùng của đối tượng sẽ biến mất khi nó bị lãng quên).
Nói chung, tôi sẽ sử dụng std::function
trừ khi có biểu hiện liên quan đến hiệu suất. Nếu họ đã làm, trước tiên tôi sẽ tìm kiếm các thay đổi về cấu trúc (thay vì gọi lại theo pixel, làm thế nào về việc tạo bộ xử lý quét dựa trên lambda mà bạn vượt qua tôi? Điều đó đủ để giảm chi phí gọi hàm xuống mức tầm thường. ). Sau đó, nếu nó vẫn còn, tôi sẽ viết một delegate
đại biểu nhanh nhất có thể và xem vấn đề hiệu suất có biến mất không.
Tôi hầu như chỉ sử dụng các con trỏ hàm cho các API kế thừa hoặc để tạo giao diện C để giao tiếp giữa các trình biên dịch mã được tạo khác nhau. Tôi cũng đã sử dụng chúng làm chi tiết triển khai nội bộ khi tôi triển khai các bảng nhảy, gõ xóa, v.v .: khi tôi vừa sản xuất vừa tiêu thụ nó, và không để lộ ra bên ngoài cho bất kỳ mã khách hàng nào sử dụng và con trỏ hàm làm tất cả những gì tôi cần .
Lưu ý rằng bạn có thể viết giấy gói mà biến std::function<int(int)>
thành một int(void*,int)
callback phong cách, giả sử có cơ sở hạ tầng quản lý đời callback thích hợp. Vì vậy, như một thử nghiệm khói cho bất kỳ hệ thống quản lý trọn đời gọi lại kiểu C nào, tôi chắc chắn rằng việc bọc một std::function
tác phẩm sẽ hợp lý.
void*
đến từ đâu? Tại sao bạn muốn duy trì trạng thái ngoài chức năng? Một hàm nên chứa tất cả mã cần thiết, tất cả chức năng, bạn chỉ cần truyền cho nó các đối số mong muốn và sửa đổi và trả về một cái gì đó. Nếu bạn cần một số trạng thái bên ngoài thì tại sao một hàmPtr hoặc gọi lại sẽ mang hành lý đó? Tôi nghĩ rằng gọi lại là phức tạp không cần thiết.
this
. Có phải vì người ta phải tính đến trường hợp hàm thành viên được gọi, nên chúng ta cần this
con trỏ trỏ đến địa chỉ của đối tượng? Nếu tôi sai, bạn có thể cho tôi một liên kết đến nơi tôi có thể tìm thêm thông tin về điều này không, vì tôi không thể tìm thấy nhiều về nó. Cảm ơn trước.
void*
để cho phép truyền trạng thái thời gian chạy. Một con trỏ hàm với một void*
và một void*
đối số có thể mô phỏng một cuộc gọi hàm thành viên đến một đối tượng. Xin lỗi, tôi không biết về một tài nguyên đi qua "thiết kế cơ chế gọi lại C 101".
this
. Ý tôi là thế Ok, cảm ơn dù sao đi nữa.
Sử dụng std::function
để lưu trữ các đối tượng có thể gọi tùy ý. Nó cho phép người dùng cung cấp bất kỳ bối cảnh nào là cần thiết cho cuộc gọi lại; một con trỏ hàm đơn giản không.
Nếu bạn cần sử dụng các con trỏ hàm đơn giản vì một số lý do (có lẽ vì bạn muốn API tương thích C), thì bạn nên thêm một void * user_context
đối số để ít nhất có thể (mặc dù không thuận tiện) để nó truy cập trạng thái không được truyền trực tiếp vào chức năng.
Lý do duy nhất để tránh std::function
là sự hỗ trợ của các trình biên dịch kế thừa thiếu hỗ trợ cho mẫu này, đã được giới thiệu trong C ++ 11.
Nếu việc hỗ trợ ngôn ngữ pre-C ++ 11 không phải là một yêu cầu, việc sử dụng std::function
mang lại cho người gọi của bạn nhiều sự lựa chọn hơn trong việc thực hiện cuộc gọi lại, làm cho nó trở thành một lựa chọn tốt hơn so với các con trỏ hàm "đơn giản". Nó cung cấp cho người dùng API của bạn nhiều sự lựa chọn hơn, đồng thời trừu tượng hóa các chi tiết cụ thể về việc triển khai của họ cho mã của bạn thực hiện cuộc gọi lại.