Впрочем, не совсем так. unary_function и binary_function являются шаблонами, поэтому прямое наследование от них невозможно. Вместо этого при наследовании используются структуры, созданные на основе этих шаблонов, а для этого необходимо указать аргументы типов. Для unary_function задается тип параметра, получаемого функцией operator() вашего класса функтора, а также тип возвращаемого значения. Для binary_function количество типов увеличивается до трех: типы первого и второго параметров operator() и тип возвращаемого значения.
Пара примеров:
template<typename T>
class MeetsThreshold: public std::unary_function<Widget, bool> {
private:
const T threshold;
public:
Meets Threshold(const T& threshold);
bool operator()(const WidgetS) const;
…
};
struct WidgetNameCompare:
std::binary_function<Widget, Widget, bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const;
};
В обоих случаях типы, передаваемые unary_function или binary_function, совпадают с типами, получаемыми и возвращаемыми функцией operator() класса функтора, хотя на первый взгляд несколько странно, что тип возвращаемого значения operator() передается в последнем аргументе unary_function или binary_function.
Возможно, вы заметили, что MeetsTheshold является классом, а WidgetNameCompare является структурой. MeetsTheshold обладает внутренним состоянием (переменная threshold), и для инкапсуляции этих данных логично воспользоваться именно классом. WidgetNameCompare состояния не имеет, поэтому и закрытые данные не нужны. Авторы классов функторов, в которых вся информация является открытой, часто объявляют структуры вместо классов — вероятно, только для того, чтобы им не приходилось вводить «public» перед базовым классом и функцией operator(). Выбор между классом и структурой при объявлении таких функторов определяется исключительно стилем программирования. Если вы еще не выработали собственного стиля и стараетесь имитировать профессионалов, учтите, что классы функторов без состояния в самой библиотеке STL (например, less<T>, plus<T> и т. д.) обычно записываются в виде структур.
Вернемся к определению WidgetNameCompare:
struct WidgetNameCompare:
std::binary_function<Widget, Widget, bool> {
bool operator()(const Widget& lhs, const Widget& rhs) const;
};
Хотя аргументы operator() относятся к типу const Widget&, шаблону binary_function передается тип Widget. Обычно при передаче unary_function или binary_function типов, не являющихся указателями, ключевые слова const и знаки ссылки удаляются… только не спрашивайте, почему, — ответ на этот вопрос не интересен и не принципиален. Если вы сгораете от любопытства, напишите программу, в которой они не удаляются, и проанализируйте полученную диагностику компилятора. А если вы и после этого не утратите интерес к этой теме, посетите сайт boost.org (см. совет 50) и поищите на нем информацию об адаптерах объектов функций.
Если operator() получает параметры-указатели, ситуация меняется. Ниже приведена структура, аналогичная WidgetNameCompare, но работающая с указателями Widget*:
struct PtrWidgetNameCompare:
std::binary_function<const Widget*, const Widget*, bool> {
bool operator()(const Widget* lhs, const Widget* rhs) const;
};
В этом случае типы, передаваемые binary_function, совпадают с типами, передаваемыми operator(). Общее правило для классов функторов, получающих или возвращающих указатели, заключается в том, что unary_function или binary_function передаются в точности те типы, которые получает или возвращает operator().
Помните, что базовые классы unary_function и binary_function выполняют только одну важную функцию — они предоставляют определения типов, необходимые для работы адаптеров, поэтому наследование от этих классов порождает адаптируемые объекты функций. Это позволяет использовать в программах следующие конструкции:
list<Widget> widgets;
…
list<Widget>::reverse_iterator i1 = // Найти последний объект
find_if(widgets.rbegin(), widgets.rend(), // Widget, не соответствующий
not1(MeetsThreshold<int>(10))); // пороговому критерию 10
//(что бы это ни означало)
Widget w(аргументы конструктора); // Найти первый объект Widget.
list<Widget>::iterator i2 = // предшествующий w в порядке
find_if(widgets.begin(), widgets.end(), // сортировки, определенном
bind2nd(WidgetNameCompare(), w)); // WidgetNameCompare
Если бы классы функторов не определялись производными от unary_function или binary_function, ни один из этих примеров не компилировался бы, поскольку not1 и bind2nd работают только с адаптируемыми объектами функций.
Объекты функций STL построены по образцу функций C++, а функции C++ характеризуются единственным набором типов параметров и одним типом возвращаемого значения. В результате STL неявно подразумевает, что каждый класс функтора содержит единственную функцию operator(), типы параметров и возвращаемого значения которой должны передаваться unary_function или binary_function (с учетом правил передачи ссылок и указателей, о которых говорилось ранее). Из этого следует одно важное обстоятельство: не поддавайтесь соблазну и не пытайтесь объединять функциональность WidgetnNameCompare и PtrWidgetCompare в одной структуре с двумя функциями operator(). В этом случае функтор будет адаптируемым по отношению лишь к одной из двух форм вызова (той, что использовалась при передаче параметров binary_function), а пользы от такого решения будет немного — наполовину адаптируемый функтор ничуть не лучше неадаптируемого.
Иногда в классе функтора бывает разумно определить несколько форм вызова, тем самым отказавшись от адаптируемости (примеры таких ситуаций приведены в советах 7, 20, 23 и 25), но это скорее исключение, а не правило. Адаптируемость важна, и о ней следует помнить при разработке классов функторов.
Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref
Загадочные функции ptr_fun/mem_fun/mem_fun_ref часто вызывают недоумение. В одних случаях их присутствие обязательно, в других они не нужны… но что же они все-таки делают? На первый взгляд кажется, что они бессмысленно загромождают имена функций. Их неудобно вводить и читать, они затрудняют понимание программы. Что это — очередные пережитки прошлого STL (другие примеры приводились в советах 10 и 18) или синтаксическая шутка, придуманная членами Комитета по стандартизации с извращенным чувством юмора?
Действительно, имена выглядят довольно странно, но функции ptr_fun, mem_fun и mem_fun_ref выполняют важные задачи. Если уж речь зашла о синтаксических странностях, надо сказать, что одна из важнейших задач этих функций связана с преодолением синтаксической непоследовательности C++.
В C++ существуют три варианта синтаксиса вызова функции f для объекта x:
f(x); // Синтаксис 1: f не является функцией класса
//(вызов внешней функции)
x.f(); // Синтаксис 2: f является функцией класса, а х
// является объектом или ссылкой на объект
p->f(); // Синтаксис 3: f является функцией класса,
// а р содержит указатель на х
Рассмотрим гипотетическую функцию, предназначенную для «проверки» объектов Widget:
void test(Widget& w); // Проверить объект w. Если объект не проходит
// проверку, он помечается как "плохой"
Допустим, у нас имеется контейнер объектов Widget:
vector<Widget> vw; // vw содержит объекты Widget
Для проверки всех объектов Widget в контейнере vw можно воспользоваться алгоритмом for_each:
for_each(vw.begin(), vw.end(), test); // Вариант 1 (нормально компилируется)
Но представьте, что test является функцией класса Widget, а не внешней функцией (то есть класс Widget сам обеспечивает проверку своих объектов):
class Widget {
public:
…
void test(); // Выполнить самопроверку. Если проверка
… // завершается неудачей, объект помечается
}; // как "плохой"
В идеальном мире мы могли бы воспользоваться for_each для вызова функции Widget::test всех объектов вектора vw:
for_each(vw.begin(), vw.end(),
&Widget::test); // Вариант 2 (не компилируется!)
Более того, если бы наш мир был действительно идеальным, алгоритм for_each мог бы использоваться и для вызова Widget::test в контейнере указателей Widget*: