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
). Но если вы пишете новый код для использования в качестве аргумента алгоритма, то лучше сделать его функциональным объектом.
89. Корректно пишите функциональные объекты
Разрабатывайте функциональные объекты так, чтобы их копирование выполнялось как можно эффективнее. Там, где это возможно, делайте их максимально адаптируемыми путем наследования от unary_function
или binary_function
.
Функциональные объекты моделируют указатели на функции. Подобно указателям на функции, они обычно передаются в функции по значению. Все стандартные алгоритмы передают объекты по значению, и то же должны делать и ваши алгоритмы, например:
template<class InputIter, class Func>
Function for_each(InputIter first, InputIter last, Function f);
Следовательно, функциональные объекты должны легко копироваться и быть мономорфными (для защиты от срезки), так что избегайте виртуальных функций (см. рекомендацию 54). Конечно, у вас могут быть большие и/или полиморфные функциональные объекты — их тоже вполне можно использовать; просто скройте их размер с помощью идиомы Pimpl (указателя на реализацию; см. рекомендацию 43). Эта идиома позволяет, как и требуется, получить внешний мономорфный класс малого размера, обеспечивающий доступ к богатой функциональности. Внешний класс должен удовлетворять следующим условиям.
• unary_function
или binary_function
.
• shared_ptr
) на (возможно, большого размера) реализацию необходимой функциональности.
•
Этим ограничиваются требования к внешнему классу (не считая возможного наличия собственных (не генерируемых компилятором) конструкторов, оператора присваивания и/или деструктора.
Функциональные объекты должны быть адаптируемы. Стандартные связыватели и адаптеры полагаются на наличие определенных инструкций typedef
, обеспечить которые легче всего при наследовании ваших функциональных объектов от unary_function
или binary_function
. Инстанцируйте unary_function
или binary_function
с теми типами, которые получает и возвращает ваш оператор operator()
(при этом у каждого типа, не являющегося указателем, следует убрать все спецификаторы const
верхнего уровня, а также все &
).
Постарайтесь избежать наличия нескольких операторов operator()
, поскольку это затрудняет адаптируемость. Дело в том, что обычно оказывается невозможно обеспечить корректные инструкции typedef
, необходимые для адаптирования, поскольку один и тот же синоним типа, определяемый через инструкцию typedef
, имеет разные значения для разных операторов operator()
.
Не все функциональные объекты являются предикатами — предикаты представляют собой подмножество функциональных объектов (см. рекомендацию 87).
Безопасность типов
Если вы лжете компилятору, он будет мстить.
Всегда будут вещи, которые мы будем хотеть сказать в наших программах и которые трудно сформулировать на любом языке программирования.
Последней (не по важности) темой книги является корректность типов — очень важное свойство программ, которое вы должны изо всех сил стараться поддерживать. Теоретически корректная с точки зрения типов функция не может обратиться к нетипизированной памяти или вернуть неверные значения. На практике, если ваш код поддерживает корректность типов, он тем самым избегает большого количества