12.3. Объекты-функции
12.3. Объекты-функции
Наша функция min() дает хороший пример как возможностей, так и ограничений механизма шаблонов:
template typename Type
const Type&
min( const Type *p, int size )
{
Type minval = p[ 0 ];
for ( int ix = 1; ix size; ++ix )
if ( p[ ix ] minval )
minval = p[ ix ];
return minval;
}
Достоинство этого механизма – возможность определить единственный шаблон min(), который конкретизируется для бесконечного множества типов. Ограничение же заключается в том, что даже при такой конкретизации min() будет работать не со всеми.
Это ограничение вызвано использованием оператора "меньше": в некоторых случаях базовый тип его не поддерживает. Так, класс изображения Image может и не предоставлять реализации такого оператора, но мы об этом не знаем и пытаемся найти минимальный кадр анимации в данном массиве изображений. Однако попытка конкретизировать min() для такого массива приведет к ошибке компиляции:
error: invalid types applied to the
Возможна и другая ситуация: оператор "меньше" существует, но имеет неподходящую
семантику. Например, если мы хотим найти наименьшую строку, но при этом принимать во
внимание только буквы, не учитывая регистр, то такой реализованный в классе оператор
не даст нужного результата.
Традиционное решение состоит в том, чтобы параметризовать оператор сравнения. В данном
случае это можно сделать, объявив указатель на функцию, принимающую два аргумента и
возвращающую значение типа bool:
template typename Type,
bool (*Comp)(const Type&, const Type&)&
const Type&
min( const Type *p, int size, Comp comp )
{
Type minval = p[ 0 ];
for ( int ix = 1; ix size; ++ix )
if ( Comp( p[ ix ] minval ))
minval = p[ ix ];
return minval;
}
Такое решение вместе с нашей первой реализацией на основе встроенного оператора "меньше" обеспечивает универсальную поддержку для любого типа, включая и класс Image, если только мы придумаем подходящую семантику для сравнения двух изображений. Основной недостаток указателя на функцию связан с низкой эффективностью, так как косвенный вызов не дает воспользоваться преимуществами встроенных функций.
Альтернативная стратегия параметризации заключается в применении объекта-функции вместо указателя (примеры мы видели в предыдущем разделе). Объект-функция – это класс, перегружающий оператор вызова (operator()). Такой оператор инкапсулирует семантику обычного вызова функции. Объект-функция, как правило, передается обобщенному алгоритму в качестве аргумента, хотя можно определять и независимые объекты-функции. Например, если бы был определен объект-функция AddImages, который принимает два изображения, объединяет их некоторым образом и возвращает новое изображение, то мы могли бы объявить его следующим образом:
AddImages AI;
Чтобы объект-функция удовлетворял нашим требованиям, мы применяем оператор вызова, предоставляя необходимые операнды в виде объектов класса Image:
Image im1("foreground.tiff"), im2("background.tiff");
// ...
// вызывает Image AddImages::operator()(const Image1&, const Image2&);
Image new_image = AI (im1, im2 );
У объекта-функции есть два преимущества по сравнению с указателем на функцию. Во-первых, если перегруженный оператор вызова – это встроенная функция, то компилятор может выполнить ее подстановку, обеспечивая значительный выигрыш в производительности. Во-вторых, объект-функция способен содержать произвольное количество дополнительных данных, например кэш или информацию, полезную для выполнения текущей операции.
Ниже приведена измененная реализация шаблона min() (отметим, что это объявление допускает также и передачу указателя на функцию, но без проверки прототипа):
template typename Type,
typename Comp
const Type&
min( const Type *p, int size, Comp comp )
{
Type minval = p[ 0 ];
for ( int ix = 1; ix size; ++ix )
if ( Comp( p[ ix ] minval ))
minval = p[ ix ];
return minval;
}
Как правило, обобщенные алгоритмы поддерживают обе формы применения операции: как использование встроенного (или перегруженного) оператора, так и применение указателя на функцию либо объекта-функции.
Есть три источника появления объектов-функций:
из набора предопределенных арифметических, сравнительных и логических объектов-функций стандартной библиотеки; из набора предопределенных адаптеров функций, позволяющих специализировать или расширять предопределенные (или любые другие) объекты-функции; определенные нами собственные объекты-функции для передачи обобщенным алгоритмам. К ним можно применять и адаптеры функций. В этом разделе мы рассмотрим все три источника объектов-функций.
12.3.1. Предопределенные объекты-функции
Предопределенные объекты-функции подразделяются на арифметические, логические и сравнительные. Каждый объект – это шаблон класса, параметризованный типами операндов. Для использования любого из них необходимо включить заголовочный файл:
#include functional
Например, объект-функция, поддерживающий сложение, – это шаблон класса с именем plus. Для определения экземпляра, способного складывать два целых числа, нужно
#include functional
написать:
plus int intAdd;
Для выполнения операции сложения мы применяем перегруженный оператор вызова к intAdd точно так же, как и к классу AddImage в предыдущем разделе:
int ival1 = 10, ival2 = 20;
// эквивалентно int sum = ival1 + ival2;
int sum = intAdd( ival1, ival2 );
Реализация шаблона класса plus вызывает оператор сложения, ассоциированный с типом своего параметра – int. Этот и другие предопределенные объекты-функции применяются прежде всего в качестве аргументов обобщенных алгоритмов и обычно замещают подразумеваемую по умолчанию операцию. Например, по умолчанию алгоритм sort() располагает элементы контейнера в порядке возрастания с помощью оператора "меньше" базового типа. Для сортировки по убыванию мы передаем
vector string svec;
// ...
предопределенный шаблон класса greater, который вызывает оператор "больше":
sort( svec.begin(), svec.end(), greaterstring() );
Предопределенные объекты-функции перечислены в следующих разделах и разбиты на категории: арифметические, логические и сравнительные. Применение каждого из них иллюстрируется как в качестве именованного, так и в качестве безымянного объекта, передаваемого функции. Мы пользуемся следующими определениями объектов, включая и определение простого класса (перегрузка операторов подробно рассматривается в главе
class Int {
public:
Int( int ival = 0 ) : _val( ival ) {}
int operator-() { return -_val; }
int operator%(int ival) { return -_val % ival; }
bool operator(int ival) { return -_val ival; }
bool operator!() { return -_val == 0; }
private:
int _val;
};
vector string svec;
string sval1, sval2, sres;
complex cval1, cval2, cres;
int ival1, ival2, ires;
Int Ival1, Ival2, Ires;
15):
double dval1, dval2, dres;
Кроме того, мы определяем два шаблона функций, которым передаем различные безымянные объекты-функции:
template class FuncObject, class Type
Type UnaryFunc( FuncObject fob, const Type &val )
{ return fob( val ); }
template class FuncObject, class Type
Type BinaryFunc( FuncObject fob,
const Type &val1, const Type &val2 )
{ return fob( val1, val2 ); }