Название книги в оригинале: Гусаров Михаил. Вариации на тему STL. Адаптер обобщенного указателя на функцию-член класса

A- A A+ White background Book background Black background

На главную » Гусаров Михаил » Вариации на тему STL. Адаптер обобщенного указателя на функцию-член класса.





Читать онлайн Вариации на тему STL. Адаптер обобщенного указателя на функцию-член класса. Гусаров Михаил.

Михаил Гусаров

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

 Сделать закладку на этом месте книги

Предисловие

 Сделать закладку на этом месте книги

Я думаю, большинство из тех, кто использует 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);

}

Заключение

 Сделать закладку на этом месте книги

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

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



убрать рекламу












На главную » Гусаров Михаил » Вариации на тему STL. Адаптер обобщенного указателя на функцию-член класса.

Close