Сообщений 2    Оценка 140        Оценить  
Система Orphus

Адаптер обобщенного указателя на функцию-член класса

Вариации на тему STL

Автор: Михаил Гусаров aka MAG
ФИТ НГУ

Версия текста: 1.0

Предисловие
Проблема обобщенных указателей
Что такое обобщенные указатели и почему они полезны
Но в чем тогда проблема?
Обобщение mem_fun
Проблемы с интерфейсом mem_fun_t
Реализация gen_mem_fun_t
Реализация gen_mem_fun
Проблемы с разными компиляторами
Специализация шаблонных функций - членов шаблонного класса
Проблема “return void”
Частичная специализация
Заключение

Исходные тексты

Предисловие

Я думаю, большинство из тех, кто использует C++ согласятся, что STL – это хорошо. Это удобная, легкая, хорошо переносимая библиотека, которая прекрасно расширяется и не содержит решений, которые были сделаны только в силу вкусов кого-либо из авторов. В этом она совпадает по духу с основным принципом C++, провозглашенным Бьярном Страуструпом в своей книге «Дизайн и эволюция C++» – никогда и никому не навязывать ничего насильственно. Но не все так гладко – часто приходится добавлять в библиотеку возможности, не предусмотренные стандартом. Иногда при этом также приходится бороться с неполной совместимостью компиляторов со стандартом C++.

Проблема обобщенных указателей

Что такое обобщенные указатели и почему они полезны

Представим себе некий объект, который имеет перегруженную операцию operator->(). Мы можем его представить себе как некий обобщенный указатель, который не является указателем в полном смысле этого слова, но «прикидывается» им. Мы можем использовать его для доступа к полям и методам некоего объекта. Можно придумать много разных применений для обобщенных указателей: реализация различных вариантов умных указателей, осуществляющих некоторую форму сборки мусора или просто ведущих статистику обращений к объектам, можно использовать обобщенные указатели для реализации паттерна «Proxy», когда мы вместо объекта используем обобщенный указатель на него, а сам объект прячется где-либо из соображений инкапсуляции, можем использовать их для реализации стратегий ленивых и сверхэнергичных вычислений и для многого другого. Видно, что обобщенные указатели – весьма полезная штука.

СОВЕТ

Для того, чтобы понять всю мощь и красоту обобщенных указателей весьма полезно почитать такие книги, как «Эффективное использование C++» и «Наиболее эффективное использование C++» Скотта Мейерса, «C++: библиотека программиста» Джеффа Элджера, а также более общую книгу «Приемы объектно-ориентированного программирования. Паттерны проектирования» Эриха Гаммы, Ричарда Хелма, Ральфа Джонсона и Джона Влиссидеса.

Но в чем тогда проблема?

Обобщенный указатель всего лишь «прикидывается» указателем и не может быть использован везде, где используются обычные указатели. Например, возьмем адаптер указателя на функцию-член класса из 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 становится тривиальной:

template<class R, class T>
gen_mem_fun_t<R, T> gen_mem_fun(R (T::*pm)())
{
    return gen_mem_fun_t<R, T>(pm);
}

Проблемы с разными компиляторами

Специализация шаблонных функций - членов шаблонного класса

К сожалению, вышеприведенный код не будет компилироваться на компиляторах, не поддерживающих специализацию шаблонов-функций – членов шаблонов классов.

ПРИМЕЧАНИЕ

К таким относятся, например, 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

К счастью, на помощь нам опять приходит частичная специализация:

template<class T, class TT>
struct gen_mem_fun_operator<void, T, TT>
{
    void operator()(TT p, void (T::*pm)()) { (p.operator->()->*pm)(); }
};

template<class T>
struct gen_mem_fun_operator<void, T, T*>
{
    void operator()(T* p, void (T::*pm)()) { (p->*pm)(); }
};

Частичная специализация

К сожалению, не все компиляторы поддерживают частичную специализацию шаблонных классов.

ПРИМЕЧАНИЕ

К таким относится и Microsoft Visual C++ 6.0/7.0

Для решения этой проблемы можно использовать паттерн «traits», специфичный для C++. К сожалению, он не сможет помочь в случае, когда один из параметров шаблона специализируется типом, зависящим от другого параметра шаблона, но в случае проблемы «return void» он помочь сможет.

ПРИМЕЧАНИЕ

Вопрос, реально ли вообще симулировать частичную специализацию шаблонов, где специализируемый параметр шаблона зависит от неспециализируемого, на компиляторе, не поддерживающем частичную специализацию шаблонов и поддерживающем специализацию вообще только для глобальных классов и функций, остается открытым. Я такой возможности не вижу. Таким образом, создать без помощи препроцессора код нашего адаптера, компилирующийся и под gcc и под Visual C++, не представляется возможным.

Введем вспомогательный класс

template<class R>
struct gen_mem_fun_traits
{
    template<class T>
    struct signature
    {
        typedef gen_mem_fun_base_t<R, T> base;
    };
};

template<>
struct gen_mem_fun_traits<void>
{
    template<class T>
    struct signature
    {
        typedef void_gen_mem_fun_base_t<T> base;
    };
};

Этот класс специализирован для специального случая функции, возвращающей void. Таким образом, хоть нам и придется ввести дополнительный класс для функций, возвращающих void, для клиента это будет выглядеть единообразно: gen_mem_fun_traits<rettype>::signature<memberclass>::base.

Сами по себе ветви вычислений различных вариантов тривиальны:

template<class R, class T>
struct gen_mem_fun_base_t
{
protected:
    gen_mem_fun_base_t(R (T::*pm_)()) : pm(pm_) {}

public:

    template<class TT>
    R operator()(TT p) { return (p.operator->()->*pm)(); }

    template<>
    R operator()(T* p) { return (p->*pm)(); }

private:
    R (T::*pm)();
};

template<class T>
struct void_gen_mem_fun_base_t
{
protected:
    void_gen_mem_fun_base_t(void (T::*pm_)()) : pm(pm_) {}

public:

    template<class TT>
    void operator()(TT p) { (p.operator->()->*pm)(); }

    template<>
    void operator()(T* p) { (p->*pm)(); }

private:
    void (T::*pm)();
};

Теперь определим сам gen_mem_fun_t:

template<class R, class T>
struct gen_mem_fun_t : gen_mem_fun_traits<R>::template signature<T>::base
{
    typedef gen_mem_fun_traits<R>::template signature<T>::base base_;

    explicit gen_mem_fun_t(R (T::*pm_)()) : base_(pm_) {}
};

Один момент здесь требует пояснения: typedef используется для того, чтобы компилятор понял, какому предку нужно передать в конструктор наш указатель на функцию-член.

И, наконец, gen_mem_fun вообще остался без изменений:

template<class R, class T>
gen_mem_fun_t<R, T> gen_mem_fun(R (T::*pm)())
{
    return gen_mem_fun_t<R, T>(pm);
}

Полный исходный текст адаптера можно скачать здесь: gen_mem_fun.zip

Заключение

Надеюсь, читатель понял, что создание адаптера как такового не было основной целью этой статьи, тем более что гораздо более общий вариант такого адаптера под названием bind находится в библиотеке boost. Основная задача, которая стояла передо мной, была такова: дать читателю некоторые навыки и умения, позволяющие не пасовать перед необходимостью внести какие-либо дополнения или изменения в STL, а также познакомить с некоторыми приемами, специфичными для C++ и полезными при необходимости работать с компиляторами, не вполне поддерживающими стандарты.

Я благодарю Павла Кузнецова и Андрея Тарасевича за плодотворную дискуссию в форуме, непосредственно предшествовавшую написанию этой статьи и давшую мне некоторые приемы и идеи, которые были освещены выше.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 2    Оценка 140        Оценить