Должен ли я использовать std :: function или указатель на функцию в C ++?

142

При реализации функции обратного вызова в C ++ я все еще должен использовать указатель на функцию в стиле C:

void (*callbackFunc)(int);

Или я должен использовать std :: function:

std::function< void(int) > callbackFunc;
Ян Сварт
источник
9
Если функция обратного вызова известна во время компиляции, рассмотрите шаблон вместо.
Baum mit Augen
4
При реализации функции обратного вызова вы должны делать все, что требует вызывающая сторона. Если ваш вопрос действительно касается разработки интерфейса обратного вызова, здесь недостаточно информации, чтобы ответить на него. Что вы хотите, чтобы получатель вашего обратного вызова сделал? Какую информацию вам нужно передать получателю? Какую информацию должен передать вам получатель в результате звонка?
Пит Беккер,

Ответы:

171

Короче говоря, используйте,std::function если у вас нет причин не делать этого.

Недостатком указателей на функции является невозможность захвата некоторого контекста. Вы не сможете, например, передать лямбда-функцию в качестве обратного вызова, которая захватывает некоторые переменные контекста (но она будет работать, если она не захватывает какие-либо). Вызов переменной-члена объекта (то есть нестатического), таким образом, также невозможен, так как объект ( this-pointer) должен быть захвачен. (1)

std::function(поскольку C ++ 11) предназначен главным образом для хранения функции (для ее передачи не требуется ее сохранения). Следовательно, если вы хотите сохранить обратный вызов, например, в переменной-члене, это, вероятно, ваш лучший выбор. Но также, если вы не сохраняете его, это хороший «первый выбор», хотя он имеет недостаток, заключающийся в том, что при вызове возникают некоторые (очень маленькие) издержки (так что в очень критичной для производительности ситуации это может быть проблемой, но в большинстве это не должно). Он очень «универсален»: если вы заботитесь о последовательном и читабельном коде, а также не хотите думать о каждом своем выборе (т.е. хотите сделать его простым), используйте его std::functionдля каждой передаваемой функции.

Подумайте о третьем варианте: если вы собираетесь реализовать небольшую функцию, которая затем сообщает о чем-либо через предоставленную функцию обратного вызова, рассмотрите параметр шаблона , который затем может быть любым вызываемым объектом , то есть указателем на функцию, функтором, лямбда-выражением, a std::function, ... Недостатком здесь является то, что ваша (внешняя) функция становится шаблоном и, следовательно, должна быть реализована в заголовке. С другой стороны, вы получаете преимущество, заключающееся в том, что вызов обратного вызова может быть встроенным, так как клиентский код вашей (внешней) функции «видит» вызов обратного вызова, когда будет доступна точная информация о типе.

Пример для версии с параметром шаблона (напишите & вместо &&pre-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 ++ (и, кстати, интенсивно используется в WIN32 API), но его следует избегать, потому что у нас есть лучшие варианты в C ++.

(2) Если мы не говорим о шаблоне класса, то есть класс, в котором вы храните функцию, является шаблоном. Но это будет означать, что на стороне клиента тип функции определяет тип объекта, в котором хранится обратный вызов, что практически никогда не используется в реальных случаях использования.

(3) Для pre-C ++ 11 используйте boost::function

leemes
источник
9
Указатели на функции имеют накладные расходы по сравнению с параметрами шаблона. Параметры шаблона упрощают встраивание, даже если вы прошли вниз по уровням, потому что выполняемый код описывается типом параметра, а не значением. А объекты-функции шаблона, сохраняемые в типах возвращаемых шаблонов, являются распространенным и полезным шаблоном (с хорошим конструктором копирования вы можете создать эффективную вызываемую функцию шаблона, которую можно преобразовать в стертую, std::functionесли вам нужно сразу же сохранить ее вне называется контекстом).
Якк - Адам Невраумонт
1
@tohecz Теперь я упоминаю, требует ли он C ++ 11 или нет.
Leemes
1
@Yakk О, конечно, забыл об этом! Добавил, спасибо.
Leemes
1
@MooingDuck Конечно, это зависит от реализации. Но если я правильно помню, из-за того, как работает стирание типа, происходит еще одно косвенное изменение? Но теперь, когда я снова об этом думаю, я полагаю, что это не тот случай, если вы назначаете ему указатели функций или лямбды без захвата ... (как типичная оптимизация)
leemes
1
@leemes: Правильно, для указателей функций или лямбд без захвата, он должен иметь такие же издержки, как c-func-ptr. Который до сих пор является трубопроводной кабиной + нетривиально встроенной.
Мычание утки
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 или для создания интерфейсов C для связи между сгенерированным кодом различными компиляторами. Я также использовал их в качестве внутренних деталей реализации, когда я реализую таблицы переходов, стирание типов и т. Д., Когда я и создаю, и использую его, и не выставляю его внешне для использования любым клиентским кодом, а указатели функций делают все, что мне нужно ,

Обратите внимание, что вы можете написать оболочки, которые превращают a std::function<int(int)>в int(void*,int)стиль обратного вызова, при условии, что имеется надлежащая инфраструктура управления временем жизни обратного вызова. Так что, как тест на дым для любой системы управления временем жизни обратного вызова в стиле C, я хотел бы убедиться, что упаковка std::functionработает достаточно хорошо.

Якк - Адам Невраумонт
источник
1
Откуда это void*взялось? Почему вы хотите поддерживать состояние за пределами функции? Функция должна содержать весь необходимый код, всю функциональность, вы просто передаете ей нужные аргументы, изменяете и возвращаете что-то. Если вам нужно какое-то внешнее состояние, тогда зачем functionPtr или callback нести этот багаж? Я думаю, что обратный вызов неоправданно сложен.
Никос
@ nik-lz Я не уверен, как я научу вас использовать и историю обратных вызовов в Си в комментарии. Или философия процедурного, а не функционального программирования. Итак, вы оставите неисполненным.
Якк - Адам Невраумонт
Я забыл this. Это потому, что нужно учитывать случай вызова функции-члена, поэтому нам нужен thisуказатель, указывающий на адрес объекта? Если я ошибаюсь, не могли бы вы дать мне ссылку, где я могу найти больше информации об этом, потому что я не могу найти много об этом. Заранее спасибо.
Никос
@ Функции-члены Nik-Lz не являются функциями. Функции не имеют (времени выполнения) состояния. Обратные вызовы принимают void*для разрешения передачи состояния времени выполнения. Указатель на функцию с аргументом a void*и void*аргументом может эмулировать вызов функции-члена для объекта. Извините, я не знаю ресурса, который проходит через "проектирование механизмов обратного вызова C 101".
Якк - Адам Невраумонт
Да, об этом я и говорил. Состояние выполнения - это, в основном, адрес вызываемого объекта (потому что он меняется между запусками). Это все еще о this. Это то, что я имел в виду. Хорошо, спасибо в любом случае.
Никос
17

Используйте std::functionдля хранения произвольных вызываемых объектов. Это позволяет пользователю предоставлять любой контекст, необходимый для обратного вызова; простой указатель на функцию - нет.

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

Майк Сеймур
источник
Какой тип р здесь? это будет тип std :: function? void f () {}; auto p = f; п();
Сри
14

Единственная причина, чтобы избежать std::function - это поддержка устаревших компиляторов, в которых отсутствует поддержка этого шаблона, который был представлен в C ++ 11.

Если поддержка языка, предшествующего C ++ 11, не является обязательным требованием, использование std::functionпредоставляет вызывающим абонентам больший выбор при реализации обратного вызова, что делает его более подходящим вариантом по сравнению с «простыми» указателями на функции. Он предлагает пользователям вашего API более широкий выбор, в то же время абстрагируясь от особенностей их реализации для вашего кода, который выполняет обратный вызов.

dasblinkenlight
источник
1

std::function может принести VMT в код в некоторых случаях, что оказывает некоторое влияние на производительность.

vladon
источник
3
Можете ли вы объяснить, что это за ВМТ?
Гупта