Указатели на функции члены класса

Указатели на функции члены класса

Правила объявления указателей на функции-члены. [переход...]
Инициализация указателей на функции члены. [переход...]
Вызовы функций по указателю на функцию. [переход...]
Применение указателей на функции-члены. [переход...]

Указатели на функции члены класса нечасто используются программистами, можно даже сказать весьма редко. В доступной литературе и Интернете очень мало информации на эту тему. Отчасти это связано с тем, что большинство задач можно решить другими способами, менее экзотическими, да и странный синтаксис указателей на функции-члены не располагает к их применению. Однако есть такая область, где применение указателей на функции-члены дает замечательный результат, об этом несколько позже, а сейчас о том, что подвигло меня написать эту статью. Как уже отмечалось, синтаксис указателей на функции-члены неприятный и быстро забывается, в MSDN синтаксис указателей на функции-члены описан неполно, поэтому приходится поднимать старые проекты для освежения информации. Попутно попалась статья http://www.rsdn.ru/article/cpp/fastdelegate.xml  (перевод), где весьма уважаемый автор, запугал читателя ужасами применения  указателей на функции-члены и не нашел дополнительных возможностей применения указателей на функции-члены, кроме создания делегатов.

Поэтому было решено поделиться с читателями своим опытом применения указателей на функции-члены и систематизировать справочный материал по этой теме.

Правила объявления указателей на функции-члены

Указатель на функцию член класса объявляется с помощью специального оператора ::*, например указатель на функцию, возвращающую float и принимающую один параметр типа int.

float (CTestP::*ptrFun)(int);

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

typedef void (CTestP::*PTR_FUN)(int);

PTR_FUN pTest;

Объявление, например, для нескольких указателей на функции рисования, может выглядеть так:

typedef void (CTestP::*PTR_FUN_DRAW)(CDC*);
PTR_FUN_DRAW ptrFunDrawLine;
PTR_FUN_DRAW ptrFunDrawRect;

В результате объявленные указатели на функции члены приобретают осмысленный вид и ими намного проще и легче оперировать.  Указатели на функции члены имеют основное ограничение, состоящее в том, что они могут указывать на функции только того класса, где объявлены.  Можно приводить один тип указателя на функцию к другому типу. Не разрешается приводить указатель на функцию к указателю на void.  В упомянутой статье указано еще и такое ограничение:
" Во-первых, нельзя использовать указатель на функцию-член для статического метода. В этом случае нужно использовать обычный указатель на функцию (так что название «указатель на функцию-член» несколько некорректно, на самом деле это «указатель на нестатическую функцию-член»)". В другой статье (Michael D. Crawford "Pointers to C++ Member Functions" http://www.goingware.com/tips/member-pointers.html) есть фраза "You cannot use pointers to member functions to store the address of a static function (use an ordinary, non-member function pointer for that)", совпадающая по смыслу с утверждением предыдущего автора. Оба автора не совсем правы, они не учли, что функции обратного вызова (callback) могут быть членами класса, если они объявлены как статические. В этом случае указывается соглашение о вызове, которое определено следующим образом:  #define CALLBACK __stdcall. Если объявить указатель на такую функцию и затем инициализировать его, то вызов статической функции выполняется без каких либо проблем. Можно пойти далее и объявить статические функции с другими типами соглашений о вызове (__cdecl, __fastcal). Если в объявлении указателя на статическую функцию указать любое из перечисленных соглашений о вызове, то и тогда это ограничение не действует в компиляторе MSVC. Возможно есть какие-то тонкие ограничения или особые случаи, но при проверках они не обнаружились. О других компиляторах сказать ничего не могу, нет их в моем распоряжении. Например объявление указателя на статическую функцию funSTest можно записать так:

static void __cdecl funSTest(int x)
{
  int z =x;
}

typedef void (__cdecl *PTR_SFUN)(int);

PTR_SFUN pSTest;

Следует отметить, что в MSDN нет на этот счет никаких упоминаний.

Инициализация указателей на функции члены

Объявим два указателя на функции, не статическую и статическую.

typedef void (CTestP::*PTR_FUN)(int); //указатель на не статическую

PTR_FUN pTest;

typedef void (__cdecl *PTR_SFUN)(int); // указатель на статическую

PTR_SFUN pSTest;

Инициализация указателя на функцию в конструкторе своего класса или в функции своего класса для не статической функции производится так:

pTest = &CTestP::funTest;

или

pTest = funTest; //компилятор MSVC допускает        

Инициализация указателя на функцию в конструкторе своего класса или в функции своего класса для статической функции производится так:

pSTest = &(CTestP::funSTest);

или

pSTest = CTestP::funSTest; //компилятор MSVC допускает

Компилятор допускает инициализацию списком как для указателя на статическую функцию, так и для указателя на не статическую функцию:

CTestP():pTest(&CTestP::funTest)
{ }

В другом классе  указатель на не статическую  функцию член может быть одновременно объявлен и инициализирован способом, указанным в MSDN:

void (CTestP::*pfun)(int)= &CTestP::funTest;

Для статической функции объявление и инициализация будет выглядеть иначе:

void (*pfun)(int)= &(CTestP::funSTest);

Последние две формы одновременного объявления и инициализации могут использоваться как для локального объявления указателя на функцию, так и глобального объявления в файле реализации.

Если объявленные указатели на функции не инициализированы в своем классе, хотя непонятно почему этого не сделать, их можно инициализировать и в другом классе, но для этого потребуется объект класса собственника или указатель на объект:

CTestP obj;

obj.pTest= &CTestP::funTest;

Вызовы функций по указателю на функцию

Синтаксис вызовов функций по указателю на функцию зависит от того, где это происходит, в своем классе или в другом классе, где существует объект класса-владельца функции или указатель на него. Например, в классе-владельце определены две функции,  не статическая funTest и статическая funSTest. Рассмотрим синтаксис вызовов этих функций в различных случаях.

void  funTest(int x)
{
    int z =x;
}

static void  __cdecl funSTest(int x)
{
    int z =x;
}

Вызовы функций по указателю в своем классе:

class CTestP
{

void SomeFun()
{

(this->*funTest)(7); //вызов не статической функции
(funTest)(7);        //тоже вызов не статической функции
(CTestP::pSTest)(5);  // вызов статической функции

}

}

Вызов (funTest)(7); выполняется если функция SomeFun определена в заголовочном файле, в случае определения в файле реализации вызов должен выполняться с помощью оператора ->*

(this->*funTest)(7);

Вызовы функций по указателю в другом классе:

//обработчик правой кнопки мыши

LRESULT CTestView::OnRClick(UINT, WPARAM, LPARAM, BOOL& )
{

CTestP obj; //объект
CTestP* pObj = &obj; //указатель на объект

//объявление указателя на функцию
void (CTestP::*pfun)(int)= &CTestP::funTest;

//вызов не статической функции по указателю, объявленному локально
(obj.*pfun)(55);    

//вызов не статической функции по указателю на нее, который предварительно определен и инициализирован в классе (obj.*obj.pTest)(33);

// тоже с использованием указателя на объект
(pObj->*pObj->pTest)(99); //вызов не статической функции

(pObj->CTestP::pSTest)(99); //вызов статической функции

//объявление указателя на статическую функцию
void (*p)(int)= &(CTestP::funSTest);

(*p)(44); //вызов статической функции по указателю объявленному локально

//вызов статической функции может быть произведен и таким образом
(*CTestP::funSTest)(7);

//или по указателю на статическую функцию с указанием объекта
(*obj.CTestP::pSTest)(57);       //вызов статической функции
(*obj.pSTest)(57);               //компилятор допускает и так

}

Также можно произвести вызовы функций обеих типов используя статический указатель на объект-владелец функций.

static CTestP* m_pTest; //статический указатель на себя

//обработчик правой кнопки мыши
LRESULT CTestView::OnRClick(UINT, WPARAM, LPARAM, BOOL& )
{

//вызов функции по указателю на статическую функцию с использованием статического указателя на объект-владелец   (*CTestP::m_pTest->pSTest)(60);

//вызов функции по указателю на не статическую функцию с использованием статического указателя на объект-владелец   (CTestP::m_pTest->*pObj->pTest)(80);

}

Теперь, когда синтаксис указателей на функции прояснился и подвергся некоторой систематизации можно задаться вопросом, а как это можно эффективно использовать? Для ответа на этот вопрос приведу несколько цитат из упомянутой статьи.

"Но чем же они полезны? Пытаясь выяснить это, я провел большой поиск по коду, опубликованному в Интернете. И я нашел два общих случая использования указателей на функции-члены:

…….Наиболее интересное применение указателей на функции-члены – это определение сложных интерфейсов. Этим способом можно реализовать некоторые впечатляющие вещи, но я нашел не так уж много примеров. В большинстве случаев, эти вещи можно выполнить более элегантно при помощи виртуальных функций, или произведя рефакторинг. Наиболее частое применение указатели на функции-члены находят во фреймворках разного типа. Они образуют ядро системы сообщений MFC.

………….В своих поисках я не смог найти много примеров хорошего использования указателей на функции-члены, кроме как во время компиляции. При всей своей сложности они не добавляют ничего особого в язык. Очень трудно опровергнуть заключение, что в С++ указатели на функции-члены имеют неполноценный дизайн".

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

Применение указателей на функции-члены

            Идея использования указателей на функции  члены появилась у меня во время размышлений как оптимизировать построение изменяющейся графики GDI в обработчике OnDraw MFC или OnPaint (не MFC).  Ни для кого не является секретом, что реализация графической картинки изменяющейся в ответ на действия пользователя является не простой задачей. Это старая проблема и для ее решения MFC предлагает использовать метафайлы (класс CMetaFileDC), также и WTL предлагает поддержку метафайлов (класс CEnhMetaFileDC). Однако не всегда использование метафайлов является оправданным и оптимальным. Есть более простое и легкое решение, которое можно использовать либо самостоятельно, либо как дополнение к метафайлам. Рассмотрим идею решения на простом часто встречающемся примере.

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

Затем Вы начинаете думать как эти три функции, и в какой последовательности будут вызываться для рисования в обработчике при выполнении тех или иных действиях пользователя. Например, координатная сетка может рисоваться поверх графика или на заднем плане, или вообще отсутствовать, или появляться при нажатии на "горячую" клавишу, заказчик может придумать что угодно. Организуем эти функции однотипно:

      void DrawGraphic(Graphics & ); //рисование одного процесса
      void FillBk(Graphics & );      //заливка фона
      void DrawAxis(Graphics & );    //рисование осей и оцифровка

Функции принимают только один параметр указатель на контекст рисования, это может Graphics, если используется GDI+ или CDC для MFC. Остальные данные и атрибуты рисования могут храниться в виде данных класса, к которым функции могут обращаться в любой момент.  Если в одну из функций крайне необходимо передать какие-то внешние данные, то можно во все функции добавить еще один параметр по умолчанию - указатель типа  void* p = NULL, с помощью которого можно осуществить передачу данных.  В классе рисования CDraw объявляем указатели на функции:

typedef void (CDraw::*PTR_FunDraw)(Graphics &);

//указатели на функции рисования
PTR_FunDraw fillBk;
PTR_FunDraw drawAxis;
PTR_FunDraw drawGraphic;

Инициализируем указатели в конструкторе класса CDraw.

fillBk = &CDraw::FillBk;
drawAxis = &CDraw::DrawAxis;
drawGraphic = &CDraw::DrawGraphic;

 Далее создаем массив или список указателей на функции, удобно использовать контейнеры стандартной библиотеки STL или динамические массивы MFC:

vector<PTR_FunDraw> m_vdr;//вектор STL

//запись указателей на функции в вектор
m_vdr.push_back(fillBk);
m_vdr.push_back(drawAxis);
m_vdr.push_back(drawGraphic);

Главную функцию рисования класса  CDraw  представим как цикл, в котором последовательно из массива вызываются функции рисования по их указателям на функции. В обработчике OnPaint вызывается только одна эта функция!  Вызов в цикле выглядит так:

if(m_vdr[i] != 0)
{
  (this->*m_vdr[i])(g); //параметр g – ссылка на объект Graphics
}

Поле для управления списком открывается необозримое, в любом месте можно ненужным в данный момент указателям присвоить NULL и соответствующие функции не будут вызываться, можно легко изменить порядок выполнения рисования, присвоив указателям значения нужных функций, можно осуществить присвоение значений указателям и их обнуление по таймеру, тогда получиться анимация картинки. Можно создать несколько векторов указателей и копировать их в главный вектор, который выполняет рисование. Можно вектор сортировать, очищать, обрабатывать с помощью алгоритмов. Сам вектор или список может содержать не просто указатели, а произвольный тип, в котором указатель на функцию является одним из элементов, остальные элементы могут нести дополнительную информацию индивидуальную для каждой функции, например ее номер или символьное имя.

Наконец этот подход можно использовать для выполнения любого ряда действий, а не только рисования, фактически можно организовать сценарий из N функций, выполняя их в определенном порядке, который очень просто изменяется.

Дополнительные сведения об указателях на функции-члены можно найти в указанной статье http://www.rsdn.ru/article/cpp/fastdelegate.xml. Проверка синтаксиса и функций рисования производилось на компиляторе VS C++ .Net 2003. Тестовый проект, в котором проверяется синтаксис указателей на функции можно получить здесь. Arc\Pointers.zip. Предложенный подход к использованию указателей на функции применен при написании программы Termo.

[в начало]

Hosted by uCoz