Чтение онлайн

ЖАНРЫ

Программирование. Принципы и практика использования C++ Исправленное издание
Шрифт:

}

Очевидно, что функция

Larger_than
должна удовлетворять двум условиям.

• Ее можно вызывать как предикат, например

pred(*first)
.

• Она может хранить значение, например

31
или
x
, передаваемое при вызове.

Для того чтобы выполнить эти условия, нам нужен объект-функция, т.е. объект, который ведет себя как функция. Нам нужен объект, поскольку именно объекты могут хранить данные, например значение для сравнения. Рассмотрим пример.

class Larger_than {

int v;

public:

Larger_than(int vv) : v(vv) { } //
хранит аргумент

bool operator(int x) const { return x>v; } // сравнение

};

Следует отметить, что это определение представляет собой именно то, что мы требовали от предиката. Теперь осталось понять, как это работает. Написав выражение

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; } // демонстрирует

// состояние

void reset(const S& ss) { s = ss; } // восстанавливает

// состояние

};

Объект класса

F
хранит данные в своем члене
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
по имени, а иногда — по адресам. Если мы не стремимся одновременно к элегантности и эффективности, наши методы ограничены практической целесообразностью. Мы можем написать следующий код:

Поделиться с друзьями: