Для того чтобы выполнить эти условия, нам нужен объект-функция, т.е. объект, который ведет себя как функция. Нам нужен объект, поскольку именно объекты могут хранить данные, например значение для сравнения. Рассмотрим пример.
Следует отметить, что это определение представляет собой именно то, что мы требовали от предиката. Теперь осталось понять, как это работает. Написав выражение
Larger_than(31)
, мы (очевидно) создаем объект класса
Larger_than
, хранящий число
31
в члене
v
. Рассмотрим пример.
find_if(v.begin,v.end,Larger_than(31))
Здесь мы передаем объект
Larger_than(31)
алгоритму
find_if
как параметр с именем
pred
. Для каждого элемента v алгоритм
find_if
осуществляет вызов
pred(*first)
Это активизирует оператор вызова функции, т.е. функцию-член operator, для объекта-функции с аргументом
*first
. В результате происходит сравнение значения элемента, т.е.
*first
, с числом
31
.
Мы видим, что вызов функции можно рассматривать как результат работы оператора
, аналогично любому другому оператору. Оператор называют также оператором вызова функции (function call operator) или прикладным оператором (application operator). Итак, оператор в выражении
pred(*first)
эквивалентен оператору
Larger_than::operator
, точно так же, как оператор
[]
в выражении
v[i]
эквивалентен оператору
vector::operator[]
.
21.4.1. Абстрактная точка зрения на функции-объекты
Таким образом, мы имеем механизм, позволяющий функции хранить данные, которые ей нужны. Очевидно, что функции-объекты образуют универсальный, мощный и удобный механизм. Рассмотрим понятие объекта-функции подробнее.
class F { // абстрактный пример объекта-функции
S s; // состояние
public:
F(const S& ss):s(ss) { /* устанавливает начальное значение */ }
T operator (const S& ss) const
{
// делает что-то с аргументом ss
// возвращает значение типа T (часто T — это void,
// bool или S)
}
const S& state const { return s; } // демонстрирует
. По мере необходимости объект-функция
может иметь много данных-членов. Иногда вместо фразы “что-то хранит данные” говорят “нечто пребывает в состоянии”. Когда мы создаем объект класса
F
, мы можем инициализировать это состояние. При необходимости мы можем прочитать это состояние. В классе
F
для считывания состояния предусмотрена операция
state
, а для записи состояния — операция
reset
. Однако при разработке объекта-функции мы свободны в выборе способа доступа к его состоянию.
Разумеется, мы можем прямо или косвенно вызывать объект-функцию, используя обычную систему обозначений. При вызове объект-функция
F
получает один аргумент, но мы можем определять объекты-функции, получающие столько параметров, сколько потребуется.
Использование объектов-функций является основным способом параметризации в библиотеке STL. Мы используем объекты-функции для того, чтобы указать алгоритму поиска, что именно мы ищем (см. раздел 21.3), для определения критериев сортировки (раздел 21.4.2), для указания арифметических операций в численных алгоритмах (раздел 21.5), для того, чтобы указать, какие объекты мы считаем равными (раздел 21.8), а также для многого другого. Использование объектов-функций — основной источник гибкости и универсальности алгоритмов.
Объекты-функции, как правило, очень эффективны. В частности, передача по значению небольшого объекта-функции в качестве аргумента шаблонной функции обеспечивает оптимальную производительность. Причина проста, но удивительна для людей, хорошо знающих механизм передачи функций в качестве аргументов: обычно передача функции в виде объекта приводит к созданию значительно более маленького и быстродействующего кода, чем при передаче функции как таковой! Это утверждение оказывается истинным, только если объект мал (например, если он содержит одно-два слова данных или вообще не хранит данные) или передается по ссылке, а также если оператор вызова функции невелик (например, простое сравнение с помощью оператора
<
) и определен как подставляемая функция (например, если его определение содержится в теле класса). Большинство примеров в этой главе — и в книге в целом — соответствует этому правилу. Основная причина высокой производительности небольших и простых объектов-функций состоит в том, что они предоставляют компилятору объем информации о типе, достаточный для того, чтобы сгенерировать оптимальный код. Даже устаревшие компиляторы с несложными оптимизаторами могут генерировать простую машинную инструкцию “больше” для сравнения в классе
Larger_than
, вместо вызова функции. Вызов функции обычно выполняется в 10–50 раз дольше, чем простая операция сравнения. Кроме того, код для вызова функции больше, чем код простого сравнения.
21.4.2. Предикаты на членах класса
Как мы уже видели, стандартные алгоритмы хорошо работают с последовательностями элементов базовых типов, таких как
int
и
double
. Однако в некоторых предметных областях более широко используются контейнеры объектов пользовательских классов. Рассмотрим пример, играющий главную роль во многих областях, — сортировка записей по нескольким критериям.
struct Record {
string name; // стандартная строка
char addr[24]; // старый стиль для согласованности
// с базами данных
// ...
};
vector<Record> vr;
Иногда мы хотим сортировать вектор
vr
по имени, а иногда — по адресам. Если мы не стремимся одновременно к элегантности и эффективности, наши методы ограничены практической целесообразностью. Мы можем написать следующий код: