Книга: Эффективное использование STL

Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref

Совет 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. Конечно, выглядит не слишком элегантно, но… бывает, ничего не поделаешь. Пусть тот, кому никогда не приходилось жалеть о поспешном выборе имен своих компонентов, первым бросит камень.

Оглавление книги


Генерация: 1.158. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз