Книга: Эффективное использование STL
Совет 38. Проектируйте классы функторов для передачи по значению
Совет 38. Проектируйте классы функторов для передачи по значению
Ни C, ни C++ не позволяют передавать функции в качестве параметров других функций. Вместо этого разрешается передавать указатели на функции. Например, объявление стандартной библиотечной функции qsort
выглядит следующим образом:
void qsort(void *base, size_t nmemb, size_t size,
int (*cmpfcn)(const void*, const void*));
В совете 46 объясняется, почему вместо функции qsort
обычно рекомендуется использовать алгоритм sort
, но дело не в этом. Нас сейчас интересует объявление параметра cmpfcn
функции qsort
. При внимательном анализе становится ясно, что аргумент cmpcfn
, который является указателем на функцию, копируется (то есть передается по значению) из точки вызова в функцию qsort
. Данный пример поясняет правило, соблюдаемое стандартными библиотеками C и C++, — указатели на функции должны передаваться по значению.
Объекты функций STL создавались по образцу указателей на функции, поэтому в STL также действует правило, согласно которому объекты функций передаются по значению (то есть копируются). Вероятно, это правило лучше всего демонстрирует приведенное в Стандарте объявление алгоритма for_each
, который получает и передает по значению объекты функций:
template<class InputIterator, class Function>
Function // Возврат по значению
for_each(InputIterator first, InputIterator last,
Function f);// Передача по значению
Честно говоря, передача по значению не гарантирована полностью, поскольку вызывающая сторона может явно задать типы параметров в точке вызова. Например, в следующем фрагменте for_each
получает и возвращает функторы по ссылке:
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(), // параметров DequeIntIter
di.end(), // и DoSomething&; в результате
d); // происходит передача
// и возврат по ссылке.
Пользователи STL почти никогда не используют эту возможность, а в некоторых реализациях алгоритмов STL при передаче объектов функций по ссылке программы даже не компилируются. В продолжение этого совета будем считать, что объекты функций всегда передаются по значению, поскольку на практике это почти всегда так.
Поскольку объекты функций передаются и возвращаются по значению, вы должны позаботиться о том, чтобы объект функции правильно работал при передаче подобным способом (то есть копированием). Для этого необходимо соблюдение двух условий. Во-первых, объекты функций должны быть небольшими, в противном случае копирование обойдется слишком дорого. Во-вторых, объекты функций должны быть мономорфными (то есть не полиморфными), поэтому в них не могут использоваться виртуальные функции. Второе требование связано с тем, что при передаче по значению объектов производных классов в параметрах базового класса происходит отсечение: в процессе копирования удаляются специализированные составляющие (другой пример проблемы отсечения в STL приведен в совете 3).
Бесспорно, эффективность является важным фактором, и предотвратить отсечение тоже необходимо, однако не все функторы малы и мономорфны. Одно из преимуществ объектов функций перед обычными функциями заключается в отсутствии ограничений на объем информации состояния. Некоторые объекты функций от природы «упитанны», и очень важно, чтобы они могли передаваться алгоритмам STL так же просто, как и их «тощие» собратья.
Столь же нереалистичен и запрет на полиморфные функторы. Иерархическое наследование и динамическое связывание относятся к числу важнейших особенностей C++, и при проектировании классов функторов они могут принести такую же пользу, как и в других областях. Что такое классы функторов без наследования? C++ без «++». Итак, необходимы средства, которые бы позволяли легко передавать большие и/или полиморфные объекты функций с соблюдением установленного в STL правила о передаче функторов по значению.
Такие средства действительно существуют. Достаточно взять данные и/или полиморфные составляющие, которые требуется сохранить в классе функтора, перенести их в другой класс и сохранить в классе функтора указатель на этот новый класс. Рассмотрим пример создания класса полиморфного функтора с большим количеством данных:
template<typename T> // 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 T> //Новый класс реализации
class BPFCImpl { //для измененного BPFC.
private:
Widget w; // Все данные, ранее находившиеся
int х; // в BPFC, теперь размещаются
… // в этом классе,
virtual ~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
) компактен и мономорфен, но при этом он предоставляет доступ к большому объему данных состояния и работает полиморфно.
Материал изложен довольно кратко, поскольку описанные базовые приемы хорошо известны в кругах C++. В книге «Effective C++» этой теме посвящен совет 34. В книге «Приемы объектно-ориентированного проектирования» [6] соответствующая методика называется «паттерн Bridge». Саттер в своей книге «Exceptional C++» [8] использует термин «идиома Pimpl
».
С позиций STL прежде всего необходимо помнить о том, что классы функторов, использующие данную методику, должны поддерживать соответствующий механизм копирования. Если бы вы были автором приведенного выше класса BPFC
, то вам пришлось бы позаботиться о том, чтобы копирующий конструктор выполнял осмысленные действия с объектом BPFCImpl
, на который он ссылается. Возможно, простейшее решение заключается в организации подсчета ссылок при помощи указателя shared_ptr
из библиотеки Boost
или его аналога (см. совет 50).
В сущности, копирующий конструктор BPFC
— единственное, о чем вам придется побеспокоиться в контексте данного примера, поскольку при передаче и получении функторов от функций STL всегда происходит копирование (помните, что говорилось выше о передаче по значению?). Из этого вытекают два требования: компактность и мономорфизм.
- Совет 38. Проектируйте классы функторов для передачи по значению
- Совет 39. Реализуйте предикаты в виде «чистых» функций
- Совет 40. Классы функторов должны быть адаптируемыми
- Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref
- Совет 42. Следите за тем, чтобы конструкция less означала operator
- Функции, функторы и классы функций
- Глава 3. Классы
- Глава 3. Классы P и NP
- 1.2. Понятие информации. Общая характеристика процессов сбора, передачи, обработки и накопления информации
- 9.1. Классы и прототипы
- Классы сертификатов
- Статические классы
- Можно ли смотреть телепередачи на компьютере?
- Глава 14 Советы хакера
- 9.8. Классы в ECMAScript 5
- 4.15. Советы по конфигурированию Firewall
- 7.7.3. Секреты и советы