88. В качестве аргументов алгоритмов и компараторов лучше использовать функциональные объекты, а не функции

88. В качестве аргументов алгоритмов и компараторов лучше использовать функциональные объекты, а не функции

Резюме

Предпочтительно передавать алгоритмам функциональные объекты, а не функции, а компараторы ассоциативных контейнеров просто должны быть функциональными объектами. Функциональные объекты адаптируемы и, вопреки ожиданиям, обычно дают более быстрый по сравнению с функциями код.

Обсуждение

Во-первых, функциональные объекты легко сделать адаптируемыми (и такими их и следует делать — см. рекомендацию 89). Даже если у вас есть готовая функция, иногда для ее использования требуется "обертка" из ptr_fun или mem_fun. Например, такая обертка требуется при построении более сложных выражений с использованием связывателей (см. также рекомендацию 84):

inline bool isHeavy(const Thing&) { /* ... */ }

find_if(v.begin(), v.end(), not1(IsHeavy)); // Ошибка

Обойти эту ошибку обычно можно путем применения ptr_fun (или, в случае функции-члена, mem_fun или mem_fun_ref), что, к сожалению, не работает в данном конкретном случае:

inline bool IsHeavy(const Thing&) { /* ... */ }

find_if(v.begin(), v.end(),

 not1(ptr_fun(IsHeavy))); // Героическая попытка...

Беда в том, что этот способ не будет работать, даже если вы явно укажете аргументы шаблона ptr_fun. Коротко говоря, проблема в том, что ptr_fun точно выводит типы аргументов и возвращаемый тип (в частности, тип параметра будет выведен как const Thing&) и создает внутренний механизм, который, в свою очередь, пытается добавить другой &, а ссылка на ссылку в настоящее время в C++ не разрешена. Имеются способы исправлений языка и/или библиотека для решения данной проблемы (например, позволяя ссылке на ссылку свернуться в обычную ссылку; см. также рекомендацию 89), но на сегодняшний день проблема остается нерешенной.

Прибегать ко всем этим ухищрениям совершенно излишне, если у вас имеется корректно написанный функциональный объект (см. рекомендацию 89), который можно использовать без какого-либо специального синтаксиса:

struct IsHeavy : unary_function<Thing, bool> {

 bool operator()(const Thing&) const { /* ... */ }

};

find_if(v.begin(), v.end(), not1(IsHeavy())) ; // OK

Еще более важно то, что для определения сравнения в ассоциативных контейнерах вам нужен именно функциональный объект, а не функция. Это связано с тем, что нельзя инстанцировать шаблон с функцией в качестве параметра:

bool CompareThings(const Thing&, const Thing&);

set<Thing, CompareThings> s; // Ошибка

Вместо этого следует написать:

struct CompareThings

 : public binary_function<Thing,Thing,bool> {

 bool operator()( const Thing&, const Thing& ) const;

};

set<Thing, CompareThings> s; //OK

Наконец, имеется еще одно преимущество функциональных объектов — эффективность. Рассмотрим следующий знакомый алгоритм:

template<typename Iter, typename Compare>

Iter find_if(Iter first, Iter last, Compare comp);

Если мы передадим алгоритму в качестве компаратора функцию

inline bool Function(const Thing&) { /* ... */ }

find_if(v.begin(), v.end(), Function);

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

Давайте передадим алгоритму find_if в качестве компаратора функциональный объект:

struct FunctionObject : unary_function<Thing, bool> {

 bool operator()(const Thing&) const { /* ... */ }

};

find_if(v.begin(), v.end(), FunctionObject());

Если мы передаем объект, который имеет (явно или неявно) встраиваемый оператор operator(), то такие вызовы компиляторы С++ способны делать встраиваемыми уже очень давно.

Примечание. Эта методика не является преждевременной оптимизацией (см. рекомендацию 8); ее следует рассматривать как препятствие преждевременной пессимизации (см. рекомендацию 9). Если у вас имеется готовая функция — передавайте указатель на нее (кроме тех ситуаций, когда вы должны обязательно обернуть ее в ptr_fun или mem_fun). Но если вы пишете новый код для использования в качестве аргумента алгоритма, то лучше сделать его функциональным объектом.

Ссылки

[Austern99] §4, §8, §15 • [Josuttis99] §5.9 • [Meyers01] §46 • [Musser01] §8 • [Sutter04] §25