РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО
ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ,
РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.
Здравствуйте, уважаемые подписчики!
CТАТЬЯ
Адаптер обобщенного указателя на функцию-член класса Вариации на тему STL
Я думаю, большинство из тех, кто использует C++ согласятся, что
STL – это хорошо. Это удобная, легкая, хорошо переносимая библиотека,
которая прекрасно расширяется и не содержит решений, которые были сделаны
только в силу вкусов кого-либо из авторов. В этом она совпадает по духу с
основным принципом C++, провозглашенным Бьярном Страуструпом в своей книге
«Дизайн и эволюция C++» – никогда и никому не навязывать
ничего насильственно. Но не все так гладко – часто приходится добавлять в
библиотеку возможности, не предусмотренные стандартом. Иногда при этом также
приходится бороться с неполной совместимостью компиляторов со стандартом C++.
Проблема обобщенных указателей
Что такое обобщенные указатели и почему они полезны
Представим себе некий объект, который имеет перегруженную
операцию operator->(). Мы можем его представить себе как некий обобщенный
указатель, который не является указателем в полном смысле этого слова, но
«прикидывается» им. Мы можем использовать его для доступа к полям и
методам некоего объекта. Можно придумать много разных применений для обобщенных
указателей: реализация различных вариантов умных указателей, осуществляющих
некоторую форму сборки мусора или просто ведущих статистику обращений к
объектам, можно использовать обобщенные указатели для реализации паттерна
«Proxy», когда мы вместо объекта используем обобщенный указатель на
него, а сам объект прячется где-либо из соображений инкапсуляции, можем
использовать их для реализации стратегий ленивых и сверхэнергичных вычислений и
для многого другого. Видно, что обобщенные указатели – весьма полезная
штука.
Обобщенный указатель всего лишь «прикидывается»
указателем и не может быть использован везде, где используются обычные
указатели. Например, возьмем адаптер указателя на функцию-член класса из STL:
template<class R, class T>
mem_fun_t<R, T> mem_fun(R (T::*pm)());
template<class R, class T>
struct mem_fun_t : public unary_function<T *, R>
{
explicit mem_fun_t(R (T::*pm)());
R operator()(T *p);
};
Видно, что когда мы вызываем mem_fun(some_class::some_member),
то получаем функциональный объект, который принимает указатель (обычный!) на
объект класса some_class и вызывает функцию some_member по этому указателю. Но
что будет, если мы попытаемся вызвать operator() с аргументом –
обобщенным указателем на объект класса A, если у этого указателя нет неявного
преобразования в указатель на объект класса?
ПРИМЕЧАНИЕ
Такие объекты-заместители бывают нужны, если клиенту нельзя
давать доступ к самому объекту: например, если тот размещен в специальной
области памяти и его адрес может меняться после сборки мусора.
Обобщение mem_fun
Проблемы с интерфейсом mem_fun_t
Для начала обратим внимание на то, что mem_fun_t::operator()
принимает только указатель на объект класса, чьим членом является функция pm.
От этого было бы неплохо избавиться. Рассмотрим такой вариант:
template<class TT, class R, class T>
struct gen_mem_fun_t
{
explicit gen_mem_fun_t(R (T::*pm)());
R operator()(TT p);
};
Сразу видна пара недостатков – во-первых, теперь адаптер
может работать только с одним типом обобщенных указателей, а во-вторых, этот
тип придется задавать при создании адаптера. Эти соображения должны натолкнуть
нас на мысль воспользоваться шаблонными функциями-членами классов.
template<class R, class T>
struct gen_mem_fun_t
{
explicit gen_mem_fun_t(R (T::*pm)());
template<class TT>
R operator()(TT p);
};
Теперь все хорошо - при необходимости вызвать operator() для
специфичного обобщенного указателя сгенерируется своя функция operator().
Реализация gen_mem_fun_t
Рассмотрим реализацию mem_fun_t:
template<class R, class T>
struct mem_fun_t
{
explicit mem_fun_t(R (T::*pm_)()) : pm(pm_) {}
R operator()(T *p) const { return ((p->*pm)()); }
private:
R (T::*pm)();
};
Все кажется идеальным для работы с указателями, но ведь
обобщенный указатель – это не указатель, он не знает, что такое
operator->*! Нужно явно узнать, на какой объект он ссылается и потом уже
выполнять операцию ->*
template<class R, class T>
struct gen_mem_fun_t
{
explicit gen_mem_fun_t(R (T::*pm_)()) : pm(pm_) {}
template<class TT>
R operator()(TT p) { return (p.operator->()->*pm)(); }
private:
R (T::*pm)();
};
Правда, возникает другая одна проблема – если теперь мы
захотим использовать наш адаптер с обычным указателем, то потерпим поражение:
обычные указатели не понимают operator->(). Таким образом, нам необходимо
специализировать нашу функцию operator() для работы с обычными указателями:
template<>
R operator()(T* p) { return (p->*pm)(); }
Реализация gen_mem_fun
Теперь реализация gen_mem_fun становится тривиальной:
Специализация шаблонных функций - членов шаблонного класса
К сожалению, вышеприведенный код не будет компилироваться на
компиляторах, не поддерживающих специализацию шаблонов-функций – членов
шаблонов классов.
ПРИМЕЧАНИЕ
К таким относятся, например, gcc-2.95 и gcc-2.96
Попробуем обойтись без них. Специализация в той или иной форме
нам в любом случае понадобится, так что воспользуемся тем, что есть –
частичной специализацией классов. Введем вспомогательный класс и специализируем
его для особого случая обычных указателей.
template<class R, class T, class TT>
struct gen_mem_fun_operator
{
R operator()(TT p, R (T::*pm)()) { return (p.operator->()->*pm)(); }
};
template<class R, class T>
struct gen_mem_fun_operator<R, T, T*>
{
R operator()(T* p, R (T::*pm)()) { return (p->*pm)(); }
};
Тогда наш gen_mem_fun_t запишется так:
template<class R, class T>
struct gen_mem_fun_t
{
explicit gen_mem_fun_t(R (T::*pm_)()) : pm(pm_) {}
template<class TT>
R operator()(TT p) { return gen_mem_fun_operator<R, T, TT>()(p, pm); }
private:
R (T::*pm)();
};
Проблема “return void”
Посмотрим внимательнее на реализацию функции operator() в нашем
адаптере. Что будет, если мы захотим в качестве типа возвращаемого значения
функции использовать void? Наша функция запишется так: void operator() { return
void; }. С точки зрения стандарта все хорошо, но все в нашем мире определяется
стандартом: есть компиляторы, которые не воспринимают такую конструкцию как
допустимую.
ПРИМЕЧАНИЕ
Таков, к примеру, Microsoft Visual C++ 6.0/7.0
К счастью, на помощь нам опять приходит частичная специализация:
К сожалению, не все компиляторы поддерживают частичную
специализацию шаблонных классов.
ПРИМЕЧАНИЕ
К таким относится и Microsoft Visual C++ 6.0/7.0
Для решения этой проблемы можно использовать паттерн
«traits», специфичный для C++. К сожалению, он не сможет помочь в
случае, когда один из параметров шаблона специализируется типом, зависящим от
другого параметра шаблона, но в случае проблемы «return void» он
помочь сможет.
ПРИМЕЧАНИЕ
Вопрос, реально ли вообще симулировать частичную специализацию
шаблонов, где специализируемый параметр шаблона зависит от неспециализируемого,
на компиляторе, не поддерживающем частичную специализацию шаблонов и
поддерживающем специализацию вообще только для глобальных классов и функций,
остается открытым. Я такой возможности не вижу. Таким образом, создать без
помощи препроцессора код нашего адаптера, компилирующийся и под gcc и под
Visual C++, не представляется возможным.
Этот класс специализирован для специального случая функции,
возвращающей void. Таким образом, хоть нам и придется ввести дополнительный
класс для функций, возвращающих void, для клиента это будет выглядеть
единообразно:
gen_mem_fun_traits<rettype>::signature<memberclass>::base.
Сами по себе ветви вычислений различных вариантов тривиальны:
Один момент здесь требует пояснения: typedef используется для
того, чтобы компилятор понял, какому предку нужно передать в конструктор наш
указатель на функцию-член.
И, наконец, gen_mem_fun вообще остался без изменений:
Полный исходный текст адаптера можно скачать здесь:
gen_mem_fun.zip
Заключение
Надеюсь, читатель понял, что создание адаптера как такового не
было основной целью этой статьи, тем более что гораздо более общий вариант
такого адаптера под названием bind находится в библиотеке
boost. Основная задача, которая стояла передо мной, была такова: дать
читателю некоторые навыки и умения, позволяющие не пасовать перед
необходимостью внести какие-либо дополнения или изменения в STL, а также
познакомить с некоторыми приемами, специфичными для C++ и полезными при
необходимости работать с компиляторами, не вполне поддерживающими стандарты.
Я благодарю Павла
Кузнецова и Андрея
Тарасевича за плодотворную дискуссию в форуме, непосредственно
предшествовавшую написанию этой статьи и давшую мне некоторые приемы и идеи,
которые были освещены выше.
Это все на сегодня. Пока!
Алекс Jenterjenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке
материалы принадлежат сайту RSDN.