return ++timesCalled == 3; // Предикаты должны быть "чистыми"
} // функциями, а "чистые" функции
// не имеют состояния
Как бы вы ни программировали предикаты, они всегда должны быть «чистыми» функциями.
Совет 40. Классы функторов должны быть адаптируемыми
Предположим, у нас имеется список указателей Widget*
и функция, которая по указателю определяет, является ли объект Widget
«интересным»:
list WidgetPtrs;
bool isInteresting(const Widget *pw);
Если потребуется найти в списке первый указатель на «интересный» объект Widget
, это делается легко:
list::iterator i = find_if(widgetPts.begin(), widgetPts.end(),
isIntersting);
if (i != widgetPts.end()) {
… // Обработка первого "интересного"
} // указателя на Widget
С другой стороны, если потребуется найти первый указатель на «неинтересный» объект Widget
, следующее очевидное решение не компилируется:
list::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(),
not1(isInteresting)); // Ошибка! He компилируется
Перед not1
к функции isInteresting
необходимо применить ptr_fun
:
list::iterator i =
find_if(widgetPtrs.begin(), widgetPtrs.end(),
not1( ptr_fun(isInteresting))); // Нормально
if (i != widgetPtrs.end()) { // Обработка первого
… // "неинтересного" указателя
} //на Widget
При виде этого решения невольно возникают вопросы. Почему мы должны применять ptr_fun
к isInteresting перед not1
? Что ptr_fun
для нас делает и почему начинает работать приведенная выше конструкция?
Ответ оказывается весьма неожиданным. Вся работа ptr_fun
сводится к предоставлению нескольких определений типов. Эти определения типов необходимы для not1
, поэтому применение not1
к ptr_fun
работает, а непосредственное применение not1
к isInteresting
не работает. Примитивный указатель на функцию isInteresting
не поддерживает определения типов, необходимые для not1
.
Впрочем, not1
— не единственный компонент STL, предъявляющий подобные требования. Все четыре стандартных адаптера ( not1
, not2
, bind1st
и bind2nd
), а также все нестандартные STL-совместимые адаптеры из внешних источников (например, входящие в SGI и Boost — см. совет 50), требуют существования некоторых определений типов. Объекты функций, предоставляющие необходимые определения типов, называются адаптируемыми; при отсутствии этих определений объект называется неадаптируемым . Адаптируемые объекты функций могут использоваться в контекстах, в которых невозможно использование неадаптируемых объектов, поэтому вы должны по возможности делать свои объекты функций адаптируемыми. Адаптируемость не требует никаких затрат, но значительно упрощает использование классов функторов клиентами.
Наверное, вместо туманного выражения «некоторые определения типов» вы бы предпочли иметь точный список? Речь идет об определениях argument_type
, first_argument_type
, second_argument_type
и result_type
, но ситуация осложняется тем, что разные классы функторов должны предоставлять разные подмножества этих имен. Честно говоря, если вы не занимаетесь разработкой собственных адаптеров, вам вообще ничего не нужно знать об этих определениях. Как правило, определения наследуются от базового класса, а говоря точнее — от базовой структуры. Для классов функторов, у которых operator()
вызывается с одним аргументом, в качестве предка выбирается структура std::unary_function
. Классы функторов, у которых operator()
вызывается с двумя аргументами, наследуют от структуры std::binary_function
.
Впрочем, не совсем так. unary_function
и binary_function
являются шаблонами, поэтому прямое наследование от них невозможно. Вместо этого при наследовании используются структуры, созданные на основе этих шаблонов, а для этого необходимо указать аргументы типов. Для unary_function
задается тип параметра, получаемого функцией operator()
вашего класса функтора, а также тип возвращаемого значения. Для binary_function
количество типов увеличивается до трех: типы первого и второго параметров operator()
и тип возвращаемого значения.
Пара примеров:
template
class MeetsThreshold: public std::unary_function{
Читать дальше