|
||||
|
Функции, функторы и классы функций Нравится нам это или нет, но функции и представляющие их объекты (функторы) занимают важное место в STL. Они используются ассоциативными контейнерами для упорядочения элементов, управляют работой алгоритмов типа find_if, конструкции for_eachи transformбез них теряют смысл, а адаптеры типа not1и bind2ndактивно создают их. Да, функторы и классы функторов встречаются в STL на каждом шагу. Встретятся они и в ваших программах. Умение создавать правильно работающие функторы абсолютно необходимо для эффективного использования STL, поэтому большая часть этой главы посвящена одной теме — как добиться того, чтобы функторы работали именно так, как им положено работать в STL. Впрочем, один совет посвящен другой теме и наверняка пригодится тем, кто задумывался о необходимости включения в программу вызовов ptr_fun, mem_funи mem_fun_ref. При желании начните с совета 41, но пожалуйста, не останавливайтесь на этом. Когда вы поймете, для чего нужны эти функции, материал остальных советов поможет вам наладить правильное взаимодействие ваших функторов с ними и с STL в целом. Совет 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 всегда происходит копирование (помните, что говорилось выше о передаче по значению?). Из этого вытекают два требования: компактность и мономорфизм. Совет 39. Реализуйте предикаты в виде «чистых» функций Для начала разберемся с основными терминами. • Предикатом называется функция, возвращающая тип bool(или другое значение, которое может быть автоматически преобразовано к bool). Предикаты широко используются в STL. В частности, функции сравнения в стандартных ассоциативных контейнерах представляют собой предикаты. Предикатные функции часто передаются в виде параметров таким алгоритмам, как find_if, и различным алгоритмам сортировки (обзор алгоритмов сортировки приведен в совете 31). • «Чистой» функцией называется функция, возвращаемое значение которой зависит только от параметров. Если f— «чистая» функция, а xи y— объекты, то возвращаемое значение f(x, y)может измениться только в случае изменения х или у. В C++ все данные, используемые «чистыми» функциями, либо передаются в виде параметров, либо остаются постоянными на протяжении всего жизненного цикла функции (естественно, такие постоянные данные объявляются с ключевым словом const). Если бы данные, используемые «чистой» функцией, могли изменяться между вызовами, то вызов этой функции в разные моменты времени с одинаковыми параметрами мог бы давать разные результаты, что противоречит определению «чистой» функции. Из сказанного должно быть понятно, что нужно сделать, чтобы предикаты были «чистыми» функциями. Мне остается лишь убедить читателя в том, что эта рекомендация обоснована. Для этого придется ввести еще один термин. • Предикатным классом называется класс функтора, у которого функция operator()является предикатом, то есть возвращает trueили false. Как и следует ожидать, во всех случаях, когда STL ожидает получить предикат, может передаваться либо настоящий предикат, либо объект предикатного класса. Обещаю, что новых терминов больше не будет. Теперь давайте разберемся, почему следует выполнять рекомендацию данного совета. В совете 38 объяснялось, что объекты функций передаются по значению, поэтому при проектировании необходимо позаботиться о возможном копировании. Для объектов функций, являющихся предикатами, существует и другой аргумент в пользу специальной поддержки копирования. Алгоритмы могут создавать копии функторов и хранить их определенное время перед применением, причем некоторые реализации алгоритмов этим активно пользуются. Важнейшим следствием этого факта является то, что предикатные функции должны быть «чистыми». Предположим, вы нарушили это ограничение. Ниже приведен плохо спроектированный класс предиката, который независимо от переданных аргументов возвращает trueтолько один раз — при третьем вызове. Во всех остальных случаях возвращается false. class BadPredicate: // Базовый класс описан public unary_function<Widget, bool>{ // в совете 40 public: BadPredicate(): timesCalles(0) {} // Переменная timesCalled // инициализируется нулем bool operator()(const Widget&) { return ++timesCalled = 3; } private: size_t timesCalled; }; Предположим, класс BadPedicateиспользуется для исключения третьего объекта Widgetиз контейнера vector<Widget>: vector<Widget> vw; // Создать вектор и заполнить его … // объектами Widget vww.erase(remove_if(vw.begin(), // Удалить третий объект Widget. vw.end(), // связь между erase и remove_if BadPredcate()), // описана в совете 32 vw.end()); Программа выглядит вполне разумно, однако во многих реализациях STL из вектора vwудаляется не только третий, но и шестой элемент! Чтобы понять, почему это происходит, необходимо рассмотреть один из распространенных вариантов реализации remove_if. Помните, что эта реализация не является обязательной. template<typename FwdIterator, typename Predicate> FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p) { begin = find_if(begin, end, p); if (begin==end) return begin; else { FwdIterator next=begin; return remove_copy_if(++next, end, begin, p); } } Подробности нас сейчас не интересуют. Обратите внимание: предикат pсначала передается find_if, а затем remove_copy_if. Конечно, в обоих случаях pпередается по значению — то есть копируется (теоретически возможны исключения, но на практике дело обстоит именно так; за подробностями обращайтесь к совету 38). Первый вызов remove_if(расположенный в клиентском коде, удаляющем третий элемент из vw) создает анонимный объект BadPredcateс внутренней переменной timesCalled, равной 0. Этот объект, известный в remove_ifпод именем p, затем копируется в find_if, поэтому find_ifтоже получает объект BadPredicateс переменной timesCalled, равной 0. Алгоритм find_if«вызывает» этот объект, пока тот не вернет true; таким образом, объект вызывается три раза. Затем find_ifвозвращает управление remove_if. Remove_ifпродолжает выполняться и в итоге вызывает remove_copy_if, передавая в качестве предиката очередную копию p. Но переменная timesCalledобъекта pпо-прежнему равна 0! Ведь алгоритм find_ifвызывал не p, а лишь копию p. В результате при третьем вызове из remove_copy_ifпредикат тоже вернет true. Теперь понятно, почему remove_ifудаляет два объекта Widgetвместо одного. Чтобы обойти эту лингвистическую ловушку, проще всего объявить функцию operator()с ключевым словом constв предикатном классе. В этом случае компилятор не позволит изменить переменные класса: class BadPredicate: public unary_function<Widget, bool> { public: bool operator()(const Widget&) const { return ++timesCalled == 3; // Ошибка! Изменение локальных данных } // в константной функции невозможно }; Из-за простоты этого решения я чуть было не озаглавил этот совет «Объявляйте operator()константным в предикатных классах», но этой формулировки недостаточно. Даже константные функции могут обращаться к mutablе-переменным, неконстантным локальным статическим объектам, неконстантным статическим объектам класса, неконстантным объектам в области видимости пространства имен и неконстантным глобальным объектам. Хорошо спроектированный предикатный класс должен обеспечить независимость функций operator()и от этих объектов. Объявление константных функций operator()в предикатных классах необходимо для правильного поведения, но не достаточно. Правильно написанная функция operator()является константной, но это еще не все. Она должна быть «чистой» функцией. Ранее в этом совете уже упоминалось о том, что всюду, где STL ожидает получить предикатную функцию, может передаваться либо реальная функция, либо объект предикатного класса. Этот принцип действует в обоих направлениях. В любом месте, где STL рассчитывает получить объект предикатного класса, подойдет и предикатная функция (возможно, модифицированная при помощи ptr_fun— см. совет 41). Теперь вы знаете, что функции operator()в предикатных классах должны быть «чистыми» функциями, поэтому ограничение распространяется и на предикатные функции. Следующая функция также плоха в качестве предиката, как и объекты, созданные на основе класса BadPredcate: bool anotherBadPredicate(const Widget&, const Widget&) { static int timesCalled = 0; // Нет! Нет! Нет! Нет! Нет! Нет! return ++timesCalled == 3; // Предикаты должны быть "чистыми" } // функциями, а "чистые" функции // не имеют состояния Как бы вы ни программировали предикаты, они всегда должны быть «чистыми» функциями. Совет 40. Классы функторов должны быть адаптируемыми Предположим, у нас имеется список указателей Widget*и функция, которая по указателю определяет, является ли объект Widget«интересным»: list<Widget*> WidgetPtrs; bool isInteresting(const Widget *pw); Если потребуется найти в списке первый указатель на «интересный» объект Widget, это делается легко: list<Widget*>::iterator i = find_if(widgetPts.begin(), widgetPts.end(), isIntersting); if (i != widgetPts.end()) { … // Обработка первого "интересного" } // указателя на Widget С другой стороны, если потребуется найти первый указатель на «неинтересный» объект Widget, следующее очевидное решение не компилируется: list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(isInteresting)); // Ошибка! He компилируется Перед not1к функции isInterestingнеобходимо применить ptr_fun: list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(), not1(ptr_fun(isInteresting))); // Нормально if (i != widgetPtrs.end()) { // Обработка первого … // "неинтересного" указателя } //на Widget При виде этого решения невольно возникают вопросы. Почему мы должны применять ptr_funк isInteresting перед not1? Что ptr_funдля нас делает и почему начинает работать приведенная выше конструкция? Ответ оказывается весьма неожиданным. Вся работа ptr_funсводится к предоставлению нескольких определений типов. Эти определения типов необходимы для not1, поэтому применение not1к ptr_funработает, а непосредственное применение not1к isInterestingне работает. Примитивный указатель на функцию isInterestingне поддерживает определения типов, необходимые для not1. Впрочем, not1— не единственный компонент STL, предъявляющий подобные требования. Все четыре стандартных адаптера ( not1, not2, bind1stи bind2nd), а также все нестандартные STL-совместимые адаптеры из внешних источников (например, входящие в SGI и Boost — см. совет 50), требуют существования некоторых определений типов. Объекты функций, предоставляющие необходимые определения типов, называются адаптируемыми; при отсутствии этих определений объект называется неадаптируемым. Адаптируемые объекты функций могут использоваться в контекстах, в которых невозможно использование неадаптируемых объектов, поэтому вы должны по возможности делать свои объекты функций адаптируемыми. Адаптируемость не требует никаких затрат, но значительно упрощает использование классов функторов клиентами. Наверное, вместо туманного выражения «некоторые определения типов» вы бы предпочли иметь точный список? Речь идет об определениях argument_type, first_argument_type, second_argument_typeи result_type, но ситуация осложняется тем, что разные классы функторов должны предоставлять разные подмножества этих имен. Честно говоря, если вы не занимаетесь разработкой собственных адаптеров, вам вообще ничего не нужно знать об этих определениях. Как правило, определения наследуются от базового класса, а говоря точнее — от базовой структуры. Для классов функторов, у которых operator()вызывается с одним аргументом, в качестве предка выбирается структура std::unary_function. Классы функторов, у которых operator()вызывается с двумя аргументами, наследуют от структуры std::binary_function. Впрочем, не совсем так. unary_functionи binary_functionявляются шаблонами, поэтому прямое наследование от них невозможно. Вместо этого при наследовании используются структуры, созданные на основе этих шаблонов, а для этого необходимо указать аргументы типов. Для unary_functionзадается тип параметра, получаемого функцией operator()вашего класса функтора, а также тип возвращаемого значения. Для binary_functionколичество типов увеличивается до трех: типы первого и второго параметров operator()и тип возвращаемого значения. Пара примеров: template<typename T> class MeetsThreshold: public std::unary_function<Widget, bool> { private: const T threshold; public: Meets Threshold(const T& threshold); bool operator()(const WidgetS) const; … }; struct WidgetNameCompare: std::binary_function<Widget, Widget, bool> { bool operator()(const Widget& lhs, const Widget& rhs) const; }; В обоих случаях типы, передаваемые unary_functionили binary_function, совпадают с типами, получаемыми и возвращаемыми функцией operator()класса функтора, хотя на первый взгляд несколько странно, что тип возвращаемого значения operator()передается в последнем аргументе unary_functionили binary_function. Возможно, вы заметили, что MeetsThesholdявляется классом, а WidgetNameCompareявляется структурой. MeetsThesholdобладает внутренним состоянием (переменная threshold), и для инкапсуляции этих данных логично воспользоваться именно классом. WidgetNameCompareсостояния не имеет, поэтому и закрытые данные не нужны. Авторы классов функторов, в которых вся информация является открытой, часто объявляют структуры вместо классов — вероятно, только для того, чтобы им не приходилось вводить « public» перед базовым классом и функцией operator(). Выбор между классом и структурой при объявлении таких функторов определяется исключительно стилем программирования. Если вы еще не выработали собственного стиля и стараетесь имитировать профессионалов, учтите, что классы функторов без состояния в самой библиотеке STL (например, less<T>, plus<T>и т. д.) обычно записываются в виде структур. Вернемся к определению WidgetNameCompare: struct WidgetNameCompare: std::binary_function<Widget, Widget, bool> { bool operator()(const Widget& lhs, const Widget& rhs) const; }; Хотя аргументы operator()относятся к типу const Widget&, шаблону binary_functionпередается тип Widget. Обычно при передаче unary_functionили binary_functionтипов, не являющихся указателями, ключевые слова constи знаки ссылки удаляются… только не спрашивайте, почему, — ответ на этот вопрос не интересен и не принципиален. Если вы сгораете от любопытства, напишите программу, в которой они не удаляются, и проанализируйте полученную диагностику компилятора. А если вы и после этого не утратите интерес к этой теме, посетите сайт boost.org (см. совет 50) и поищите на нем информацию об адаптерах объектов функций. Если operator()получает параметры-указатели, ситуация меняется. Ниже приведена структура, аналогичная WidgetNameCompare, но работающая с указателями Widget*: struct PtrWidgetNameCompare: std::binary_function<const Widget*, const Widget*, bool> { bool operator()(const Widget* lhs, const Widget* rhs) const; }; В этом случае типы, передаваемые binary_function, совпадают с типами, передаваемыми operator(). Общее правило для классов функторов, получающих или возвращающих указатели, заключается в том, что unary_functionили binary_functionпередаются в точности те типы, которые получает или возвращает operator(). Помните, что базовые классы unary_functionи binary_functionвыполняют только одну важную функцию — они предоставляют определения типов, необходимые для работы адаптеров, поэтому наследование от этих классов порождает адаптируемые объекты функций. Это позволяет использовать в программах следующие конструкции: list<Widget> widgets; … list<Widget>::reverse_iterator i1 = // Найти последний объект find_if(widgets.rbegin(), widgets.rend(), // Widget, не соответствующий not1(MeetsThreshold<int>(10))); // пороговому критерию 10 //(что бы это ни означало) Widget w(аргументы конструктора); // Найти первый объект Widget. list<Widget>::iterator i2 = // предшествующий w в порядке find_if(widgets.begin(), widgets.end(), // сортировки, определенном bind2nd(WidgetNameCompare(), w)); // WidgetNameCompare Если бы классы функторов не определялись производными от unary_functionили binary_function, ни один из этих примеров не компилировался бы, поскольку not1и bind2ndработают только с адаптируемыми объектами функций. Объекты функций STL построены по образцу функций C++, а функции C++ характеризуются единственным набором типов параметров и одним типом возвращаемого значения. В результате STL неявно подразумевает, что каждый класс функтора содержит единственную функцию operator(), типы параметров и возвращаемого значения которой должны передаваться unary_functionили binary_function(с учетом правил передачи ссылок и указателей, о которых говорилось ранее). Из этого следует одно важное обстоятельство: не поддавайтесь соблазну и не пытайтесь объединять функциональность WidgetnNameCompareи PtrWidgetCompareв одной структуре с двумя функциями operator(). В этом случае функтор будет адаптируемым по отношению лишь к одной из двух форм вызова (той, что использовалась при передаче параметров binary_function), а пользы от такого решения будет немного — наполовину адаптируемый функтор ничуть не лучше неадаптируемого. Иногда в классе функтора бывает разумно определить несколько форм вызова, тем самым отказавшись от адаптируемости (примеры таких ситуаций приведены в советах 7, 20, 23 и 25), но это скорее исключение, а не правило. Адаптируемость важна, и о ней следует помнить при разработке классов функторов. Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref Загадочные функции ptr_fun/mem_fun/mem_fun_refчасто вызывают недоумение. В одних случаях их присутствие обязательно, в других они не нужны… но что же они все-таки делают? На первый взгляд кажется, что они бессмысленно загромождают имена функций. Их неудобно вводить и читать, они затрудняют понимание программы. Что это — очередные пережитки прошлого STL (другие примеры приводились в советах 10 и 18) или синтаксическая шутка, придуманная членами Комитета по стандартизации с извращенным чувством юмора? Действительно, имена выглядят довольно странно, но функции ptr_fun, mem_funи mem_fun_refвыполняют важные задачи. Если уж речь зашла о синтаксических странностях, надо сказать, что одна из важнейших задач этих функций связана с преодолением синтаксической непоследовательности C++. В C++ существуют три варианта синтаксиса вызова функции fдля объекта x: f(x); // Синтаксис 1: f не является функцией класса //(вызов внешней функции) x.f(); // Синтаксис 2: f является функцией класса, а х // является объектом или ссылкой на объект p->f(); // Синтаксис 3: f является функцией класса, // а р содержит указатель на х Рассмотрим гипотетическую функцию, предназначенную для «проверки» объектов Widget: void test(Widget& w); // Проверить объект w. Если объект не проходит // проверку, он помечается как "плохой" Допустим, у нас имеется контейнер объектов Widget: vector<Widget> vw; // vw содержит объекты Widget Для проверки всех объектов Widgetв контейнере vwможно воспользоваться алгоритмом for_each: for_each(vw.begin(), vw.end(), test); // Вариант 1 (нормально компилируется) Но представьте, что testявляется функцией класса Widget, а не внешней функцией (то есть класс Widgetсам обеспечивает проверку своих объектов): class Widget { public: … void test(); // Выполнить самопроверку. Если проверка … // завершается неудачей, объект помечается }; // как "плохой" В идеальном мире мы могли бы воспользоваться for_eachдля вызова функции Widget::testвсех объектов вектора vw: for_each(vw.begin(), vw.end(), &Widget::test); // Вариант 2 (не компилируется!) Более того, если бы наш мир был действительно идеальным, алгоритм for_eachмог бы использоваться и для вызова Widget::testв контейнере указателей Widget*: list<Widget*> lpw; // Список lpw содержит указатели // на объекты Widget for_each(lpw.begin(), lpw.end(), // Вариант 3 (не компилируется!) &widget::test); Но подумайте, что должно было бы происходить в этом идеальном мире. Внутри функции for_eachв варианте 1 вызывается внешняя функция, поэтому должен использоваться синтаксис 1. Внутри вызова for_eachв варианте 2 следовало бы использовать синтаксис 2, поскольку вызывается функция класса. А внутри функции for_eachв варианте 3 пришлось бы использовать синтаксис 3, поскольку речь идет о функции класса и указателе на объект. Таким образом, нам понадобились бы триразных версии for_each— разве такой мир можно назвать идеальным? В реальном мире существует только одна версия for_each. Нетрудно представить себе возможную ее реализацию: template<typename InputIterator, typename Function> Function for_each(InputIterator begin, InputIterator end, Function f) { while (begin != end) f(*begin++); } Жирный шрифт используется для выделения того, что при вызове for_eachиспользуется синтаксис 1. В STL существует всеобщее правило, согласно которому функции и объекты функций всегда вызываются в первой синтаксической форме (как внешние функции). Становится понятно, почему вариант 1 компилируется, а варианты 2 и 3 не компилируются — алгоритмы STL (в том числе и for_each) жестко закодированы на использование синтаксиса внешних функций, с которым совместим только вариант 1. Теперь понятно, для чего нужны функции mem_funи mem_fun_ref. Они обеспечивают возможность вызова функций классов (обычно вызываемых в синтаксисе 2 и 3) при помощи синтаксиса 1. Принцип работы mem_funи mem_fun_refпрост, хотя для пущей ясности желательно рассмотреть объявление одной из этих функций. В действительности они представляют собой шаблоны функций, причем существует несколько вариантов mem_funи mem_fun_refдля разного количества параметров и наличия-отсутствия константности адаптируемых ими функций классов. Одного объявления вполне достаточно, чтобы разобраться в происходящем: template<typename R, typename C> // Объявление mem_fun для неконстантных mem_fun_t<R, C> // функций без параметров. С - класс. mem_fun(R(C::*pmf)()); // R - тип возвращаемого значения функции. // на которую ссылается указатель Функция mem_funсоздает указатель pmfна функцию класса и возвращает объект типа mem_fun_t. Тип представляет собой класс функтора, содержащий указатель на функцию и функцию operator(), которая по указателю вызывает функцию для объекта, переданного operator(). Например, в следующем фрагменте: list<Widget*> lpw; // См. ранее … for_each(lpw.begin(), lpw.end(), mem_fun(&Widget::test)); // Теперь нормально компилируется При вызове for_eachпередается объект типа mem_fun_t, содержащий указатель на Widget::test. Для каждого указателя Widget*в lpwалгоритм for_each«вызывает» объект mem_fun_tс использованием синтаксиса 1, а этот объект непосредственно вызывает Widget::testдля указателя Widget*с использованием синтаксиса 3. В целом mem_funприводит синтаксис 3, необходимый для Widget::testпри использовании с указателем Widget*, к синтаксису 1, используемому алгоритмом for_each. По вполне понятным причинам такие классы, как mem_fun_t, называются адаптерами объектов функций. Наверное, вы уже догадались, что по аналогии со всем, о чем говорилось ранее, функции mem_fun_refадаптируют синтаксис 2 к синтаксису 1 и генерируют адаптеры типа mem_fun_ref_t. Объекты, создаваемые функциями mem_funи mem_fun_ref, не ограничиваются простой унификацией синтаксиса для компонентов STL. Они (а также объекты, создаваемые функцией ptr_fun) также предоставляют важные определения типов. Об этих определениях уже было рассказано в совете 40, поэтому я не стану повторяться. Тем не менее, стоит разобраться, почему конструкция for_each(vw.begin(), vw.end(), test); // См. ранее, вариант 1. // Нормально компилируется компилируется, а следующие конструкции не компилируются: for_each(vw.begin(), vw.end(), &Widget::test); // См. ранее, вариант 2. // Не компилируется. for_each(lpw.begin(), lpw.end(), &Widget::test); // См. ранее, вариант 3. // Не компилируется При первом вызове (вариант 1) передается настоящая функция, поэтому адаптация синтаксиса вызова для for_eachне нужна; алгоритм сам вызовет ее с правильным синтаксисом. Более того, for_eachне использует определения типов, добавляемые функцией ptr_fun, поэтому при передаче testфункция ptr_funне нужна. С другой стороны, добавленные определения не повредят, поэтому следующий фрагмент функционально эквивалентен приведенному выше: for_each(vw.begin(), vw.end(), ptr_fun(test)); // Компилируется и работает, // как вариант 1. Если вы забываете, когда функция ptr_funобязательна, а в каких случаях без нее можно обойтись, лучше используйте ее при всех передачах функций компонентам STL. STL игнорирует лишние вызовы, и они не отражаются на быстродействии программы. Возможно, во время чтения вашей программы кто-нибудь удивленно поднимет брови при виде лишнего вызова ptr_fun. Насколько это беспокоит вас? Наверное, ответ зависит от природной мнительности. Существует и другой подход — использовать ptr_funв случае крайней необходимости. Если функция отсутствует там, где необходимы определения типов, компилятор выдает сообщение об ошибке. Тогда вы возвращаетесь к программе и включаете в нее пропущенный вызов. С mem_funи mem_fun_refситуация принципиально иная. Эти функции всегда должны применяться при передаче функции компонентам STL, поскольку помимо определения типов (необходимых или нет) они адаптируют синтаксис вызова, который обычно используется для функций класса, к синтаксису, принятому в STL. Если не использовать эти функции при передаче указателей на функции класса, программа не будет компилироваться. Остается лишь разобраться со странными именами адаптеров. Перед нами самый настоящий пережиток прошлого STL. Когда впервые возникла необходимость в адаптерах, разработчики STL ориентировались на контейнеры указателей (с учетом недостатков таких контейнеров, описанных в советах 7, 20 и 33, это может показаться странным, но не стоит забывать, что контейнеры указателей поддерживают полиморфизм, а контейнеры объектов — нет). Когда понадобился адаптер для функций классов (MEMber FUNctions), его назвали mem_fun. Только позднее разработчики поняли, что для контейнеров объектов понадобится другой адаптер, и для этой цели изобрели имя mem_fun_ref. Конечно, выглядит не слишком элегантно, но… бывает, ничего не поделаешь. Пусть тот, кому никогда не приходилось жалеть о поспешном выборе имен своих компонентов, первым бросит камень. Совет 42. Следите за тем, чтобы конструкция less<T> означала operator< Допустим, объект класса Widgetобладает атрибутами weightи maxSpeed: class Widget { public: … size_t weight() const; size_t maxSpeed() const; … } Будем считать, что естественная сортировка объектов Widgetосуществляется по атрибуту weight, что отражено в операторе <класса Widget: bool operator<(const Widget& lhs, const Widget& rhs) { return lhs.weight()<rhs.weight(); } Предположим, потребовалось создать контейнер multiset<Widget>, в котором объекты Widgetотсортированы по атрибуту maxSpeed. Известно, что для контейнера multiset<Widget>используется функция сравнения less<Widget>, которая по умолчанию вызывает функцию operator<класса Widget. Может показаться, что единственный способ сортировки multiset<Widget>по атрибуту maxSpeedоснован на разрыве связи между less<Widget>и operator<и специализации less<Widget>на сравнении атрибута maxSpeed: template<> // Специализация std::less struct std::less<Widget>: // для Widget: такой подход public // считается крайне нежелательным! std::binаry_function<Widget, Widget, // Базовый класс описан bool> { // в совете 40 bool operator() (const Widget& lhs, const Widget& rhs) const { return lhs.maxSpeed() < rhs.maxSpeed(); } }; Поступать подобным образом не рекомендуется, но, возможно, совсем не по тем причинам, о которых вы подумали. Вас не удивляет, что этот фрагмент вообще компилируется? Многие программисты обращают внимание на то, что в приведенном фрагменте специализируется не обычный шаблон, а шаблон из пространства имен std. «Разве пространство stdне должно быть местом священным, зарезервированным для разработчиков библиотек и недоступным для простых программистов? — спрашивают они. — Разве компилятор не должен отвергнуть любое вмешательство в творения бессмертных гуру C++?» Вообще говоря, попытки модификации компонентов stdдействительно запрещены, поскольку их последствия могут оказаться непредсказуемыми, но в некоторых ситуациях минимальные изменения все же разрешены. А именно, программистам разрешается специализировать шаблоны stdдля пользовательских типов. Почти всегда существуют альтернативные решения, но в отдельных случаях такой подход вполне разумен. Например, разработчики классов умных указателей часто хотят, чтобы их классы при сортировке вели себя как встроенные указатели, поэтому специализация std::lessдля типов умных указателей встречается не так уж редко. Далее приведен фрагмент класса shared_ptrиз библиотеки Boost, упоминающегося в советах 7 и 50: namespace std { template<typename T> // Специализация std::less struct less<boost::shared_ptr<T> >: // для boost::shared_ptr<T> public // (boost - пространство имен) binary_function<boost::shared_ptr<T>, boost::shared_ptr<T>, // Базовый класс описан bool> { // в совете 40 bool operator()(const boost::shared_ptr<T>& a, const boost::shared_ptr<T>& b) const { return less<T*>()(a.get(), b.get()); // shared_ptr::get возвращает } // встроенный указатель }; // из объекта shared_ptr } В данном примере специализация выглядит вполне разумно, поскольку специализация lessвсего лишь гарантирует, что порядок сортировки умных указателей будет совпадать с порядком сортировки их встроенных аналогов. К сожалению, наша специализация less для класса Widgetпреподносит неприятный сюрприз. Программисты C++ часто опираются на предположения. Например, они предполагают, что копирующие конструкторы действительно копируют (как показано в совете 8, невыполнение этого правила приводит к удивительным последствиям). Они предполагают, что в результате взятия адреса объекта вы получаете указатель на этот объект (в совете 18 рассказано, что может произойти в противном случае). Они предполагают, что адаптеры bind1stи not2могут применяться к объектам функций (см. совет 40). Они предполагают, что оператор +выполняет сложение (кроме объектов string, но знак «+» традиционно используется для выполнения конкатенации строк), что оператор -вычитает, а оператор ==проверяет равенство. И еще они предполагают, что функция lessэквивалентна operator<. В действительности operator<представляет собой нечто большее, чем реализацию lessпо умолчанию — он соответствует ожидаемому поведению less. Если lessвместо вызова operator<делает что-либо другое, это нарушает ожидания программистов и вступает в противоречие с «принципом минимального удивления». Конечно, поступать так не стоит — особенно если без этого можно обойтись. В STL нет ни одного случая использования less, когда программисту бы не предоставлялась возможность задать другой критерий сравнения. Вернемся к исходному примеру с контейнером multiset<Widget>, упорядоченному по атрибуту maxSpeed. Задача решается просто: для выполнения нужного сравнения достаточно создать класс функтора практически с любым именем, кроме less. Пример: struct MaxSpeedCompare: public binary_function<Widget, Widget, bool> { bool operator()(const Widget& lhs, const Widget& rhs) const { return lhs.maxSpeed() < rhs.maxSpeed(); } }; При создании контейнера multisetдостаточно указать тип сравнения MaxSpeedCompare, тем самым переопределяя тип сравнения по умолчанию ( less<Widget>): multiset<Widget, MaxSpeedCompare> widgets; Смысл этой команды абсолютно очевиден: мы создаем контейнер multisetс элементами Widget, упорядоченными в соответствии с классом функтора MaxSpeedCompare. Сравните со следующим объявлением: multiset<Widget> widgets; В нем создается контейнер multisetобъектов Widget, упорядоченных по стандартному критерию. Строго говоря, упорядочение производится по критерию less<Widget>, но большинство программистов будет полагать, что сортировка производится функцией operator<. Не нужно обманывать их ожидания и подменять определение less. Если вы хотите использовать less(явно или косвенно), проследите за тем, чтобы этот критерий был эквивалентен operator<. Если объекты должны сортироваться по другому критерию, создайте специальный класс функтора и назовите его как-нибудь иначе. |
|
||
Главная | Контакты | Нашёл ошибку | Прислать материал | Добавить в избранное |
||||
|