Онлайн библиотека PLAM.RU

Загрузка...



  • Совет 38. Проектируйте классы функторов для передачи по значению
  • Совет 39. Реализуйте предикаты в виде «чистых» функций
  • Совет 40. Классы функторов должны быть адаптируемыми
  • Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref
  • Совет 42. Следите за тем, чтобы конструкция less<T> означала operator<
  • Функции, функторы и классы функций

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









    Главная | Контакты | Нашёл ошибку | Прислать материал | Добавить в избранное

    Все материалы представлены для ознакомления и принадлежат их авторам.