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

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

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

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

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

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

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

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

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

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

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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Читайте также:  Failed to install the hcmon driver

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

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

void funTest(int x)
<
int z =x;
>

static void __cdecl funSTest(int x)
<
int z =x;
>

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

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

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

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

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

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

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

//объявление указателя на функцию
vo >

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

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

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

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

//объявление указателя на статическую функцию
vo >

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

Читайте также:  Ветчина в ветчиннице отзывы

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

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

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

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

//запись указателей на функции в вектор
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 функций, выполняя их в определенном порядке, который очень просто изменяется.

Указатели на функции-члены не часто встречаются в программистской практике,
но иногда они оказываются очень полезными. Они аналогичны указателям на
обычные функции, но при вызове функции-члена нужно передавать объект
(помимо аргументов). Синтаксис таких указателей описывается следующим примером:

Есть в с++ одна интересная особенность: в этом языке не существует типа для результата
работы функций (test.*pt). К нему можно только применить оператор вызова функции.

В стандарте языка C++ сказано: "Если результатом выполнения операторов .* и ->* является функция, этот результат может быть использован лишь в качестве операнда оператора вызова функции ()".

21 декабря 2005 (Обновление: 25 янв. 2006)

  • ITcrusader
  • Постоялец

Не понимаю, а зачем. Я здесь увидел лишь альтернативный способ вызвать функцию, причем, способ нечитабельный) Может я что-то упустил из вида?

  • jabberx
  • Постоялец

Иногда удобней пользоваться массивом из указателей на разные функции, чем катать простыни из switch или if.. else if

  • asvp
  • Постоялец

Я такую штуку применял:
1. чтобы избавится от толстого switch.
2. при динамической подмене функции в зависимости от железа.

  • Andrey
  • Постоялец

ITcrusader
>Не понимаю, а зачем. Я здесь увидел лишь альтернативный способ вызвать функцию, причем, способ нечитабельный) Может я что-то упустил из вида?
Я для консольных команд так делал.
В консольную команду можно биндить практически любой метод класса. Не затачивая его специально для этого. Так что это очень полезная штука.

  • Feo
  • Постоялец

Указатель на метод класса? Извращение же.
Если тебе нужен указатель на метод класса – ты пишешь неправильный код.

  • asvp
  • Постоялец

> Указатель на метод класса? Извращение же.
Возможно, ща начнется .
> Если тебе нужен указатель на метод класса – ты пишешь неправильный код.
Это всего лишь один из инструментов при написании программы. Причем хороший инструмент.
Хороший прог видит такие вещи сразу.

  • StiX
  • Удалён

asvp
> Причем хороший инструмент.
Это как раз плохой инструмент – он усложняет флоу и понимаемость программы

  • Volodar
  • Постоялец

Вполне нормальная практика. Пользуюсь и довольно часто. У себя обвернуты указатели в шаблоны и фабричную функцию создания, дабы облегчить вызовы. Есть хорошая статья на эту тему: http://www.rsdn.ru/article/cpp/delegates.xml

  • ONeiLL
  • Постоялец

Юзаю у себя в своём GUI для вызова какого-либо фанка при нажатии кнопки. Очень полезная вещь.

  • Grey304
  • Удалён

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

  • Smrdis
  • Участник

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

  • StiX
  • Удалён

Smrdis
лучше по возможности не использовать буст, потому, что буст:
> это ужасно нечитабильно
😀

  • Smrdis
  • Участник

StiX
> лучше по возможности не использовать буст, потому, что буст:
А std::tr1::bind, пойдет?

  • ITcrusader
  • Постоялец

asvp
> Хороший прог видит такие вещи сразу.

Ну, допустим я никудышный прог. Но за 6 лет использования C++ ни разу не довелось использовать данную возможность, о предоставлении которой языком я знал.
Мне не хотелось бы разводить холивар (если бы хотел, создал бы тему типа "C++ – не более чем извращенный способ прострелить себе ногу" или "apple рулит, Android – говно)" или "открывающая скобка в определениях функций должна быть в одной строке с заголовком функции" 🙂 )
На тему целесообразности использования указателей на функции члены класса ответ я ищу, в том числе, самостоятельно. НО пока не нашел достойного примера применения. Если таков имеется, с удовольствием бы глянул:)

Читайте также:  Dell venue 8 windows 10

Сначала рассмотрим указатели на обычные функции, а затем перейдем к функциям-членам класса.

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

Указатель на функцию — это переменная, которая хранит адрес кода функции в памяти.

Примеры использования

1. Функции как аргументы других функций

Функции сортировки qsort

необходимо передать указатель на функцию compar , задающую порядок сортировки (по возрастанию, по убыванию и т. п.).

2. Callback’и

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

для чего нужно передать ей: координаты кнопки, текст надписи на ней, и функцию (тот самый callback), вызываемую при нажатии на кнопку.

Синтаксис

Рассмотрим следующий пример:

Здесь func — указатель на функцию, принимающую один аргумент ( int ) и возвращающую void .

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

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

вместо ожидаемого, даст объявление функции, возвращающей указатель на void. Чтобы *func сработало раньше, нужно его также взять в скобки. В итоге получим:

Разобрать более сложные случаи поможет правило чтения по спирали.

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

Инициализация

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

Вызов

Чтобы вызвать функцию my_int_func , на которую указывает указатель func , вы должны разыменовать этот указатель (*func) (помним про скобки, изменяющие приоритет операторов!). Это работает, но в действительности все проще: компилятор трактует вызов указателя как вызов функции, на которую тот указывает, а разыменование происходит автоматически:

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

Постепенно подходим к основной теме статьи. Предыдущие примеры работали как в С, так и в С++. Теперь мы переходим к использованию классов и, следовательно, С++.

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

Как видно, использование указателей на функции в качестве аргументов методов класса ничего нового не привносит.

Метод класса, отличается от обычной функции, в частности, тем, что неявно содержит среди своих аргументов указатель на объект того класса, которому он принадлежит ( this ). В результате тип "указатель на функцию-член" должен отличаться от типа "указатель на функцию". Чем же?

Допустим, у нас есть функция:

Если это — обычная функция или статический метод класса, то ее тип нам уже известен:

А если это нестатический метод класса (к примеру, класса A ), то ее тип запишется так:

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

Далее мы будем рассматривать только нестатические методы, и использовать термины "функция-член класса" и "метод" как синонимы.

Изменим предыдущий пример, внося тип указатель-на-функцию и переменную этого типа внутрь класса:

Бросается в глаза различие в вызове метода и функции

Действительно, нестатический метод класса относится к конкретному экземпляру класса (объекту) и потому вызывается через this-> , а так как switchFunc — это указатель, то понадобилось его разыменование.

Зачем нужны скобки, я уже говорил.

Обращает на себя внимание и вызов функции, соответствующей типу function

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

Напомню, что ->* используется, когда стоящий слева аргумент представляет собой указатель на объект (как в нашем случае this ), а .* — когда он является ссылкой на объект. Пример обращения к полям и методам через .* приведен ниже:

Борьба с ошибками

Возьмите себе за правило: как только нужно создать указатель на функцию — использовать typedef . Всегда.

Для упрощения громоздких вызовов вроде

можно использовать макросы. Например, такой:

Тогда указанный выше вызов запишется как

Это тот случай, когда использование макросов полезно.

Читайте также

Комментарии

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