Tôi nên sử dụng hàm std :: hay con trỏ hàm trong C ++?


141

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;

9
Nếu chức năng gọi lại được biết tại thời điểm biên dịch, hãy xem xét một mẫu thay thế.
Baum mit Augen

4
Khi thực hiện chức năng gọi lại, bạn nên làm bất cứ điều gì người gọi yêu cầu. Nếu câu hỏi của bạn thực sự là về việc thiết kế giao diện gọi lại, thì không có đủ thông tin ở đây để trả lời. Bạn muốn người nhận cuộc gọi lại của bạn làm gì? Những thông tin nào bạn cần để truyền cho người nhận? Những thông tin nào người nhận nên gửi lại cho bạn do kết quả của cuộc gọi?
Pete Becker

Câu trả lời:


171

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::functioncho 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


9
con trỏ hàm có phí gọi so với tham số mẫu. các tham số mẫu làm cho nội tuyến trở nên dễ dàng, ngay cả khi bạn được truyền xuống các mức tăng, bởi vì mã được thực thi được mô tả bởi loại tham số không phải là giá trị. Và các đối tượng hàm mẫu được lưu trữ trong các kiểu trả về mẫu là một mẫu phổ biến và hữu ích (với một hàm tạo sao chép tốt, bạn có thể tạo hàm mẫu hiệu quả bất khả xâm phạm có thể được chuyển đổi thành std::functionkiể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).
Yakk - Adam Nevraumont

1
@tohecz Bây giờ tôi có đề cập đến nếu nó yêu cầu C ++ 11 hay không.
leeme

1
@Yakk Oh tất nhiên, quên điều đó! Đã thêm nó, cảm ơn.
leeme

1
@MooingDuck Tất nhiên nó phụ thuộc vào việc thực hiện. Nhưng nếu tôi nhớ chính xác, do cách thức tẩy xóa hoạt động, có thêm một sự gián tiếp xảy ra? Nhưng bây giờ tôi nghĩ về nó một lần nữa, tôi đoán đây không phải là trường hợp nếu bạn gán các con trỏ hàm hoặc lambdas không bắt giữ cho nó ... (như một tối ưu hóa điển hình)
leeme

1
@leeme: Phải, đối với các con trỏ hàm hoặc lambdas không bị giam giữ, nó phải có cùng chi phí như một c-func-ptr. Mà vẫn là một gian hàng đường ống + không tầm thường.
Vịt Mooing

24

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::functiontriể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::functiontrừ 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::functiontác phẩm sẽ hợp lý.


1
Nơi này đã 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.
Nikos

@ nik-lz Tôi không chắc chắn về cách tôi sẽ dạy bạn cách sử dụng và lịch sử các cuộc gọi lại trong C trong một bình luận. Hoặc triết lý về thủ tục trái ngược với lập trình chức năng. Vì vậy, bạn sẽ không hoàn thành.
Yakk - Adam Nevraumont

Tôi quên mấ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 thiscon 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.
Nikos

Các chức năng thành viên @ Nik-Lz không có chức năng. Hàm không có trạng thái (runtime). Các cuộc gọi lại mất một 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".
Yakk - Adam Nevraumont

Vâng, đó là những gì tôi đã nói về. Trạng thái thời gian chạy về cơ bản là địa chỉ của đối tượng được gọi (vì nó thay đổi giữa các lần chạy). Nó vẫn còn về this. Ý tôi là thế Ok, cảm ơn dù sao đi nữa.
Nikos

17

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.


Loại p ở đây là gì? nó sẽ là một kiểu chức năng std ::? khoảng trống f () {}; tự động p = f; p ();
sree

14

Lý do duy nhất để tránh std::functionlà 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::functionmang 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.


1

std::function có thể mang VMT đến mã trong một số trường hợp, điều này có ảnh hưởng đến hiệu suất.


3
Bạn có thể giải thích mũ VMT này là?
Gupta
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.