Подробная разница между вызовом функтора и вызовом функции? - PullRequest
13 голосов
/ 15 октября 2010

Основная причина этой работы заключается в том, что for_each () фактически не принимает свой третий аргумент как функцию.Он просто предполагает, что его третий аргумент - это то, что можно вызвать с соответствующим аргументом.Правильно определенный объект служит, а зачастую и лучше, чем функция.Например, легче встроить оператор приложения класса, чем встроить функцию, переданную как указатель на функцию.Следовательно, функциональные объекты часто выполняются быстрее, чем обычные функции.Объект класса с оператором приложения (§11.9) называется функционально-подобным объектом, функтором или просто функциональным объектом.

[Stroustrup, C ++ 3-е издание, 18.4-lastпараграф]

  1. Я всегда думал, что вызов оператора (() - это как вызов функции во время выполнения).Чем он отличается от обычного вызова функции?

  2. Почему проще встроить оператор приложения, чем обычную функцию?

  3. Какони быстрее вызова функции?

Ответы [ 3 ]

10 голосов
/ 15 октября 2010

Как правило, функторы передаются в шаблонные функции - если вы это делаете, то не имеет значения, если вы передаете "реальную" функцию (т.е. указатель функции) или функтор (т.е.класс с перегруженным operator()).По сути, оба имеют оператор вызова функции и, следовательно, являются допустимыми параметрами шаблона, для которых компилятор может создать экземпляр шаблона for_each. Это означает, что for_each создается либо с конкретным типом переданного функтора, либо с конкретным типом переданного указателя функции. И это в той специализациичто функторы могут превосходить указатели на функции.

В конце концов, если вы передаете указатель на функцию , тогда тип аргумента типа компиляции - это просто указатель на функцию.Если сам for_each не является встроенным, то этот конкретный экземпляр for_each компилируется для вызова непрозрачного указателя на функцию - в конце концов, как компилятор может встроить указатель на функцию?Он просто знает свой тип , а не , какую функцию этого типа фактически передают - по крайней мере, если он не может использовать нелокальную информацию при оптимизации, что сложнее сделать.

Однако, если вы передаете функтор , то тип времени компиляции этого функтора используется для создания экземпляра шаблона for_each.При этом вы, вероятно, передаете простой, не виртуальный класс только с одной реализацией соответствующего operator().Таким образом, когда компилятор встречает вызов operator(), он точно знает, какая реализация подразумевается - уникальная реализация для этого функтора - и теперь он может встроить это.

Если ваш функтор использует виртуальные методы, потенциальное преимуществоисчезает.И, конечно же, функтор - это класс, с помощью которого вы можете выполнять любые другие неэффективные действия.Но в базовом случае именно поэтому компилятору проще оптимизировать и встроить вызов функтора, чем вызов указателя функции.

Сводка

Указатели на функции нельзя встроить, поскольку при компиляцииfor_each компилятор имеет только тип функции, а не идентификатор функции.В отличие от этого, функторы могут быть встроенными, поскольку, хотя компилятор имеет только тип функтора, его обычно достаточно для однозначной идентификации метода operator() функтора.

4 голосов
/ 15 октября 2010

Рассмотрим два следующих варианта шаблона:

std::for_each<class std::vector<int>::const_iterator, class Functor>(...)

и

std::for_each<class std::vector<int>::const_iterator, void(*)(int)>(...)

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

Во 2-м сценарии компилятор будет создавать экземпляр шаблона один раз для всех функций с одинаковой сигнатурой , поэтому он не может легко встроить вызов.

Теперь умные компиляторы могут выяснить, какую функцию вызывать во время компиляции, особенно в таких сценариях, как это:

std::for_each(v.begin(), v.end(), &foo);

и по-прежнему встроить функцию, генерируя пользовательские экземпляры вместо единственного универсального, упомянутого ранее.

0 голосов
/ 15 октября 2010

Я всегда думал, что вызов operator () похож на вызов функции во время выполнения.Чем он отличается от обычного вызова функции?

Я думаю, не очень много.Чтобы убедиться в этом, посмотрите выходные данные сборки вашего компилятора для каждого из них.Предполагая тот же уровень оптимизации, он, вероятно, будет почти идентичным.(С дополнительными сведениями о том, что указатель this должен быть передан.)

Почему проще встроить оператор приложения, чем обычную функцию?

Чтобы процитировать цитату, которую вы цитировали:

Например, проще встроить оператор приложения класса, чем встроить функцию, переданную в качестве указателя на функцию.

Я не компиляторчеловек, но я читаю это так: если функция вызывается через указатель функции, то для компилятора трудно решить, изменится ли когда-либо адрес, сохраненный в этом указателе функции, во время выполнения, поэтому небезопасно заменять call инструкция с телом функции;Если подумать, тело самой функции не обязательно будет известно во время компиляции.

Как они работают быстрее, чем вызов функции?

Во многихобстоятельства, которые я ожидаю, вы не заметите никакой разницы.Но, учитывая аргумент вашей цитаты о том, что компилятор может делать больше вставок, это может привести к лучшему коду locality и меньшему количеству branch .Если код вызывается часто, это приведет к заметному ускорению.

...