Функции, функторы и классы функций

Нравится нам это или нет, но функции и представляющие их объекты (функторы) занимают важное место в STL. Они используются ассоциативными контейнерами для упорядочения элементов, управляют работой алгоритмов типа find_if, конструкции for_each и transform без них теряют смысл, а адаптеры типа not1 и bind2nd активно создают их.

Да, функторы и классы функторов встречаются в STL на каждом шагу. Встретятся они и в ваших программах. Умение создавать правильно работающие функторы абсолютно необходимо для эффективного использования STL, поэтому большая часть этой главы посвящена одной теме — как добиться того, чтобы функторы работали именно так, как им положено работать в STL. Впрочем, один совет посвящен другой теме и наверняка пригодится тем, кто задумывался о необходимости включения в программу вызовов ptr_fun, mem_fun и mem_fun_ref. При желании начните с совета 41, но пожалуйста, не останавливайтесь на этом. Когда вы поймете, для чего нужны эти функции, материал остальных советов поможет вам наладить правильное взаимодействие ваших функторов с ними и с STL в целом.

Совет 38. Проектируйте классы функторов для передачи по значению

Ни С, ни С++ не позволяют передавать функции в качестве параметров других функций. Вместо этого разрешается передавать указатели на функции. Например, объявление стандартной библиотечной функции qsort выглядит следующим образом:

void qsort(void *base, size_t nmemb, size_t size,

int (*cmpfcn)(const void*,const void*));

В совете 46 объясняется, почему вместо функции qsort обычно рекомендуется использовать алгоритм sort, но дело не в этом. Нас сейчас интересует объявление параметра cmpfcn функции qsort. При внимательном анализе становится ясно, что аргумент cmpcfn, который является указателем на функцию, копируется (то есть передается по значению) из точки вызова в функцию qsort. Данный пример поясняет правило, соблюдаемое стандартными библиотеками С и С++, — указатели на функции должны передаваться по значению.

Объекты функций STL создавались по образцу указателей на функции, поэтому в STL также действует правило, согласно которому объекты функций передаются по значению (то есть копируются). Вероятно, это правило лучше всего демонстрирует приведенное в Стандарте объявление алгоритма for_each, который получает и передает по значению объекты функций:

template<class InputIterator.

class Function>

Functon // Возврат по значению

for_each(InputIterator first,

InputIterator last,

Functon f);// Передача по значению

Честно говоря, передача по значению не гарантирована полностью, поскольку вызывающая сторона может явно задать типы параметров в точке вызова. Например, в следующем фрагменте foreach получает и возвращает функторы по ссылке:

class DoSomething:

public unary_function<int,void>{// Базовый класс описан

void operator() (int x){...}// в совете 40

};

typedef deque<int>::iterator DequeIntIter: // Вспомогательное определение

deque<int> di;

...

DoSomething d; // Создать объект функции

for_each<DequeIntIter, //Вызвать for_each с типами

DoSomethng&>(di .begin(),//параметров DequelntIter

di.end(),//и DoSomething&: в результате

d);//происходит передача

//и возврат по ссылке.

Пользователи STL почти никогда не используют эту возможность, а в некоторых реализациях алгоритмов STL при передаче объектов функций по ссылке программы даже не компилируются. В продолжение этого совета будем считать, что объекты функций всегда передаются по значению, поскольку на практике это почти всегда так.

Поскольку объекты функций передаются и возвращаются по значению, вы должны позаботиться о том, чтобы объект функции правильно работал при передаче подобным способом (то есть копированием). Для этого необходимо соблюдение двух условий. Во-первых, объекты функций должны быть небольшими, в противном случае копирование обойдется слишком дорого. Во-вторых, объекты функций должны быть мономорфными (то есть не полиморфными), поэтому в них не могут использоваться виртуальные функции. Второе требование связано с тем, что при передаче по значению объектов производных классов в параметрах базового класса происходит отсечение: в процессе копирования удаляются специализированные составляющие (другой пример проблемы отсечения в STL приведен в совете 3).

Бесспорно, эффективность является важным фактором, и предотвратить отсечение тоже необходимо, однако не все функторы малы и мономорфны. Одно из преимуществ объектов функций перед обычными функциями заключается в отсутствии ограничений на объем информации состояния. Некоторые объекты функций от природы «упитанны», и очень важно, чтобы они могли передаваться алгоритмам STL так же просто, как и их «тощие» собратья.

Столь же нереалистичен и запрет на полиморфные функторы. Иерархическое наследование и динамическое связывание относятся к числу важнейших особенностей С++, и при проектировании классов функторов они могут принести такую же пользу, как и в других областях. Что такое классы функторов без наследования? С++ без «++». Итак, необходимы средства, которые бы позволяли легко передавать большие и/или полиморфные объекты функций с соблюдением установленного в STL правила о передаче функторов по значению.

Такие средства действительно существуют. Достаточно взять данные и/или полиморфные составляющие, которые требуется сохранить в классе функтора, перенести их в другой класс и сохранить в классе функтора указатель на этот новый класс. Рассмотрим пример создания класса полиморфного функтора с большим количеством данных:

template<typename Т> // BPFC = 'Big Polymorphic

class BPFC: //Functor class'

public // Базовый класс описан

unary_function<T,void> {// в совете 40

private:

Widget w;// Класс содержит большой объем

int х;// данных, поэтому передача

// по значению

// была бы неэффективной

public:

virtual void operator() (const T& val) const; // Виртуальная функция.

// создает проблему

};// отсечения

Мы выделяем все данные и виртуальные функции в класс реализации и создаем компактный, мономорфный класс, содержащий указатель на класс реализации:

template<typename Т> //Новый класс реализации

class BPFCImpl{ //для измененного BPFC.

private:

Widget w; //Все данные, ранее находившиеся

int х: //в BPFC, теперь размещаются

//в этом классе,

vrtual ~BPFCImpl(); //В полиморфных классах нужен

//виртуальный деструктор,

virtual void operator() (const T& val) const;

friend class BPFC<T>;// Разрешить BPFC доступ к данным

};

template<typename T>

class BPFC:// Компактная, мономорфная версия

public unary_function<T,void> {

private:

BPFCImpl<T>* pImpl;// Все данные BPFC

public:

void operator()(const T& val) const; // Функция не является

{// виртуальной; вызов передается

plImpl->operator()(val);// BPFCImpl

}

};

Реализация BFPC:: operator() дает пример того, как должны строиться реализации всех виртуальных функций BPFC: они должны вызывать свои виртуальные «прототипы» из BPFCImpl. Полученный в результате класс функтора (BPFC) компактен и мономорфен, но при этом он предоставляет доступ к большому объему данных состояния и работает полиморфно.

Добавить отзыв
ВСЕ ОТЗЫВЫ О КНИГЕ В ИЗБРАННОЕ

0

Вы можете отметить интересные вам фрагменты текста, которые будут доступны по уникальной ссылке в адресной строке браузера.

Отметить Добавить цитату