Чи варто використовувати std :: функцію або покажчик функції на C ++?


142

Чи реалізувати функцію зворотного дзвінка в C ++, чи повинен я все-таки використовувати вказівник функції C:

void (*callbackFunc)(int);

Або я повинен використовувати std :: функцію:

std::function< void(int) > callbackFunc;

9
Якщо функція зворотного дзвінка відома під час компіляції, розгляньте замість неї шаблон.
Baum mit Augen

4
При реалізації функції зворотного виклику , ви повинні робити те , що абонент вимагає. Якщо ваше питання справді стосується розробки інтерфейсу зворотного дзвінка, тут немає ніде такої інформації, щоб відповісти на нього. Що ви хочете робити одержувачу зворотного дзвінка? Яку інформацію потрібно передати одержувачу? Яку інформацію одержувач повинен передати вам у результаті дзвінка?
Піт Бекер

Відповіді:


171

Словом, використовуйте,std::function якщо у вас немає причин цього не робити.

Покажчики функцій мають недолік у тому, що вони не в змозі захопити певний контекст. Наприклад, ви не зможете передавати лямбда-функцію як зворотний виклик, який фіксує деякі змінні контексту (але він буде працювати, якщо не зафіксує жодної). Викликати змінну члена об'єкта (тобто нестатичну), таким чином, також неможливо, оскільки об'єкт ( this-показник) потрібно захопити. (1)

std::function(оскільки C ++ 11) - це насамперед для зберігання функції (для передачі її навколо не потрібно зберігати). Отже, якщо ви хочете зберегти зворотний виклик, наприклад, у змінній члена, це, мабуть, ваш найкращий вибір. Але також якщо ви не зберігаєте його, це хороший "перший вибір", хоча у нього є недолік введення деяких (дуже маленьких) накладних витрат при виклику (так що в дуже критичній продуктивності ситуація може бути проблемою, але в більшості не повинно). Це дуже "універсально": якщо ви багато піклуєтесь про послідовний і читабельний код, а також не хочете думати про кожен свій вибір (тобто хочете зробити його простим), використовуйте std::functionдля кожної функції, яку ви проходите навколо.

Подумайте про третій варіант: якщо ви збираєтесь реалізувати невелику функцію, яка потім щось повідомляє через надану функцію зворотного виклику, розгляньте параметр шаблону , який може бути будь-яким об'єктом , що може викликати , тобто вказівником функції, функтором, лямбда, a std::function, ... Недолік тут полягає в тому, що ваша (зовнішня) функція стає шаблоном і тому її потрібно реалізувати в заголовку. З іншого боку, ви отримуєте перевагу в тому, що виклик до зворотного дзвінка може бути вкладений, оскільки клієнтський код вашої (зовнішньої) функції "бачить" виклик до зворотного дзвінка буде точною інформацією про тип.

Приклад для версії з параметром шаблону (запишіть &замість &&попереднього C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Як видно з наступної таблиці, всі вони мають свої переваги та недоліки:

+-------------------+--------------+---------------+----------------+
|                   | 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) Для вирішення цього обмеження існують обхідні шляхи, наприклад, передача додаткових даних у якості додаткових параметрів вашій (зовнішній) функції: myFunction(..., callback, data)викликає callback(data). Це "зворотний виклик з аргументами" у стилі C, який можливий у C ++ (і, до речі, широко використовується в API WIN32), але його слід уникати, оскільки в C ++ є кращі варіанти.

(2) Якщо ми не говоримо про шаблон класу, тобто клас, у якому ви зберігаєте функцію, є шаблон. Але це означало б, що на стороні клієнта тип функції визначає тип об'єкта, який зберігає зворотний виклик, що майже ніколи не є варіантом для реальних випадків використання.

(3) Для попереднього C ++ 11 використовуйте boost::function


9
функціональні покажчики мають накладні виклики порівняно з параметрами шаблону. Параметри шаблону полегшують вкладку легко, навіть якщо вам передано дванадцять рівнів, оскільки виконуваний код описується типом параметра, а не значенням. А об’єкти функції шаблону, що зберігаються у типах повернення шаблонів, є загальним і корисним шаблоном (за допомогою хорошого конструктора копій ви можете створити ефективну функцію виклику шаблону, яку можна перетворити на стирану std::functionтип, якщо вам потрібно негайно зберігати її за межами називається контекстом).
Якк - Адам Невраумон

1
@tohecz Я зараз згадую, чи вимагає C ++ 11 чи ні.
leemes

1
@Yakk О звичайно, забув про це! Додав, дякую.
leemes

1
@MooingDuck Звичайно, це залежить від реалізації. Але якщо я правильно пам'ятаю, через те, як працює стирання типу, має місце ще одна непрямість? Але тепер, коли я знову замислююся над цим, я думаю, що це не так, якщо ви призначите йому покажчики функцій або не захоплюєте лямбда ... (як типова оптимізація)
leemes

1
@leemes: Правильно, для функціональних покажчиків або безкарбованих лямбд, вони повинні мати таку ж накладну частину, як і c-func-ptr. Що досі є стійлом трубопроводу + не тривіально накресленим.
Mooing Duck

25

void (*callbackFunc)(int); це може бути функцією зворотного дзвінка в стилі C, але це погано непридатний варіант поганого дизайну.

Добре розроблений зворотний виклик у стилі C void (*callbackFunc)(void*, int);- він має void*дозволити коду, який робить зворотний виклик, підтримувати стан поза функцією. Якщо цього не зробити, змушує абонента зберігати стан в усьому світі, що є нечесним.

std::function< int(int) >в кінцевому рахунку є трохи дорожчим, ніж int(*)(void*, int)виклик у більшості реалізацій. Однак деякі компілятори складніше надати назви. Є std::functionреалізація клонів, які конкурують накладні функції виклику функцій конкурента (див. "Найшвидші можливі делегати" тощо), які можуть пробитися в бібліотеки.

Тепер клієнтам системи зворотного дзвінка часто потрібно налаштовувати ресурси та розпоряджатися ними під час створення та видалення зворотного дзвінка, а також знати про термін служби зворотного дзвінка. void(*callback)(void*, int)не забезпечує цього.

Іноді це доступно через структуру коду (зворотний виклик має обмежений термін експлуатації) або через інші механізми (відреєстрація зворотних дзвінків тощо).

std::function забезпечує засіб для управління обмеженим терміном експлуатації (остання копія об'єкта відходить, коли він забутий).

Взагалі я б використовував, std::functionякщо не маніфест щодо продуктивності. Якби це було, я спершу шукаю структурні зміни (замість зворотного зворотного дзвінка за піксель, як щодо генерування процесора сканування на основі лямбда, який ви передаєте мені?), Чого має бути достатньо, щоб зменшити накладні виклики функцій до тривіальних рівнів. ). Тоді, якщо це не триває, я delegateсписав би найшвидших делегатів і побачив, чи не піде проблема з продуктивністю.

Я здебільшого використовую лише покажчики функцій для застарілих API або для створення інтерфейсів С для спілкування між кодом, що генерується різними компіляторами. Я також використовував їх як детальну інформацію про внутрішню реалізацію, коли я впроваджую таблиці стрибків, стираю тип тощо: коли я створюю і споживаю його, і не піддаю його зовнішньому використанню будь-якого коду клієнта, а покажчики функцій роблять все, що мені потрібно .

Зауважте, що ви можете писати обгортки, які перетворюються std::function<int(int)>на int(void*,int)стиль зворотного виклику, якщо припустити, що існує відповідна інфраструктура управління зворотним викликом. Отже, як тест на задимлення для будь-якої системи управління зворотним викликом у стилі C, я би переконався, що завершення std::functionроботи працює досить добре.


1
Звідки це void*взялося? Чому ви хочете підтримувати стан поза функцією? Функція повинна містити весь необхідний їй код, всю функціональність, ви просто передаєте їй потрібні аргументи та щось змінюєте та повертаєте. Якщо вам потрібен зовнішній стан, то чому функція Ptr або зворотний виклик несе цей багаж? Я думаю, що зворотний виклик є надмірно складним.
Нікос

@ nik-lz Я не впевнений, як я б в коментарі навчив вас використовувати та історію зворотних викликів у C. Або філософія процедурного на відміну від функціонального програмування. Отже, ви залишитесь невиконаними.
Якк - Адам Невраумон

Я забув this. Це тому, що треба враховувати випадок виклику функції-члена, тому нам потрібен thisвказівник для вказівки на адресу об’єкта? Якщо я помиляюся, ви можете надіслати мені посилання, де я можу знайти більше інформації про це, тому що я не можу багато про це знайти. Заздалегідь спасибі.
Нікос

@ Функції члена Nik-Lz не є функціями. У функцій немає стану (часу виконання). Зворотні виклики приймаються, void*щоб дозволити передачу стану виконання. Покажчик функції з аргументом void*та void*аргументом може емулювати виклик функції члена об’єкту. Вибачте, я не знаю про ресурс, який проходить через "проектування механізмів зворотного виклику C 101".
Якк - Адам Невраумон

Так, про це я говорив. Стан виконання є в основному адресою об'єкта, який викликається (оскільки він змінюється між прогонами). Це все ще про this. Це я мав на увазі. Добре, спасибі все одно.
Нікос

17

Використовуйте std::functionдля зберігання довільних об'єктів, що дзвоняться. Це дозволяє користувачеві надати будь-який контекст, необхідний для зворотного дзвінка; звичайний покажчик функції не робить.

Якщо вам потрібно з якихось причин використовувати звичайні функціональні покажчики (можливо, тому, що вам потрібен C-сумісний API), тоді вам слід додати void * user_contextаргумент, щоб принаймні можливий (хоч і незручний) для нього доступ до стану, який не передається безпосередньо в функція.


Що тут тип p? це буде std :: тип функції? void f () {}; auto p = f; p ();
sree

14

Єдина причина цього уникати std::function- це підтримка застарілих компіляторів, у яких відсутня підтримка цього шаблону, що було введено в C ++ 11.

Якщо підтримка мови попереднього C ++ 11 не є вимогою, використання std::functionнадає вашим абонентам більший вибір у реалізації зворотного дзвінка, що робить його кращим варіантом порівняно з покажчиками функцій "звичайний". Він пропонує користувачам вашого API більше вибору, одночасно абстрагуючи особливості їх застосування для вашого коду, який виконує зворотний виклик.


1

std::function може привести VMT до коду в деяких випадках, що має певний вплив на продуктивність.


3
Чи можете ви пояснити, що це за VMT?
Гупта
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.