Скачать

Ларс Хендель. Указатели на Функции

Lars Haendel. The Function Pointer Tutorials.

1.  Введение в Указатели на Функции

Указатели на функции обеспечивают несколько чрезвычайно интересных, эффективных и изящных программных методов. Вы можете использовать их, чтобы заменить switch/if-операторы, реализовать ваше собственное позднее связывание (late-binding) или обратные вызовы (callbacks). К сожалению, вероятно из-за их сложного синтаксиса, в большинстве компьютерных книг и документации к ним относятся весьма неприязненно. Если о них и говорится, то довольно-таки кратко и поверхностно. Они в меньшей степени подвержены ошибкам, чем обычные указатели, вы никогда не будете выделять или освобождать для них память. Все, что вы должны сделать, - это понять, чем они являются и изучить их синтаксис. Но имейте в виду: вы всегда должны спрашивать себя, действительно ли вы нуждаетесь в указателе на функцию. Приятно реализовать собственное позднее связывание, но использование существующих структур C++ может сделать ваш код удобочитаемым и более понятным. Один из аспектов в случае позднего связывания - время выполнения: если вы вызываете виртуальную функцию, ваша программа должна определить, какую функцию нужно вызвать. Это делается посредством виртуальной таблицы (V-Table), содержащей все возможные функции. На каждый вызов затрачивается некоторое время, и возможно вы можете сократить время, используя указатели на функции вместо виртуальных функций. Возможно, и нет...

1.1  Что такое "Указатель на Функцию"?

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

1.2  Вводный Пример или "Как Заменить Оператор Switch"

Когда вы хотите вызвать функцию DoIt() в определенной точке, обозначенной в программе меткой, вы только помещаете вызов функции DoIt() в точке метки в исходном коде. Затем вы компилируете код, и каждый раз, когда программа доходит до метки, вызывается ваша функция. Все Ok. Но что делать, если вы не знаете, в какой момент времени должна быть вызвана функция? Что вы делаете, когда хотите решить это во время выполнения? Возможно, вы захотите использовать так называемую Функцию Обратного Вызова (Callback Function), или выбрать функцию из пула возможных функций. Однако, вы также можете решить проблему, используя оператор switch, где вы вызываете функции точно так, как вы этого хотите, используя разные блоки (branches). Но существует другой способ: используйте указатель на функцию!

В следующем примере мы рассмотрим задачу по выполнению одной из четырех основных арифметических операций. Сначала задача решена с использованием оператора switch. Затем демонстрируется, как то же самое может быть сделано с помощью указателя на функцию. Это только пример, и задача настолько проста, что я предполагаю, никто не будет использовать для этого указатель на функцию;-)

//------------------------------------------------------------------------------------
// 1.2 Вводный Пример или "Как Заменить Оператор Switch"
// Задача: Выполните одну из четырех основных арифметических операций, определенных
// символами '+','-','*' или '/'.

// Четыре арифметических операции ... одна из этих функций выбирается
// во время выполнения с помощью switch или указателя на функцию 
float Plus(float a, float b){ return a+b; }
float Minus(float a, float b){ return a-b; }
float Multiply(float a, float b){ return a*b; }
float Divide(float a, float b){ return a/b; }

// Решение с помощью оператора switch - <opCode> определяющего, 
// какая операция выполнится
void Switch(float a, float b, char opCode){

  float result=0.0;
	
  switch(opCode)// выполнение операции
  {
    case '+': result = Plus(a, b); 
    break;
    case '-': result = Minus(a, b); 
    break;
    case '*': result = Multiply(a, b); 
    break;
    case '/': result = Divide(a, b); 
    break;
  }
  cout<<"Switch: 2+5="<<result<<endl;// отображение результата
}

// Решение с помощью указателя на функцию - <pt2Func>;  
// pt2Func указывет на функцию, принимающую два аргумента типа float и возвращающую также float.
// Указатель на функцию "определяет", какая операция будет выполнена.
void Switch_With_Function_Pointer(float a, float b, float(*pt2Func)(float, float)){
  float result = pt2Func(a, b);// вызов через указатель на функцию
  cout<<"Switch заменён указателем на функцию: 2-5=";// отображение результата
  cout<<result<<endl;
}

// Выполнение кода примера
void Replace_A_Switch(){
  cout<<endl<<"Выполнение функции 'Replace_A_Switch'"<<endl;
  Switch(2, 5,/* '+' будет выполнена выбранная функция 'Plus' */'+');
  Switch_With_Function_Pointer(2, 5,/* указатель на функцию 'Minus' */&Minus);
}

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

2.  Синтаксис Указателей на Функции в C и C++

2.1  Определение Указателя на Функцию

Рассматривая синтаксис, можно выделить два типа указателей на функции: один из них - это указатели на обычные функции C или статические функции-члены C++. Другой тип - это указатели на нестатические функции-члены в C++. Основное различие в том, что для всех указателей на нестатические функции-члены нужен скрытый аргумент: указатель this на экземпляр класса. Всегда помните: Эти два типа указателей несовместимы друг с другом.

Так как указатель на функцию ничто иное, как переменная, он должен быть определён как обычно. В следующем примере мы определяем три указателя на функции: pt2Function, pt2Member и pt2ConstMember. Они указывают на функции, которые принимают один аргумент типа float и два аргумента типа char и возвращают int. В примерах на C++ предполагается, что функции, на которые указывают наши указатели, являются (нестатическими) членами TMyClass.

// 2.1 объявление указателя на функцию и инициализация значением NULL
int (*pt2Function)(float, char, char) = NULL;                        // C
int (TMyClass::*pt2Member)(float, char, char) = NULL;                // C++
int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;     // C++

2.2  Соглашение о Вызовах

Обычно, вы не думаете о соглашении о вызове функций (function's calling convention): компилятор принимает __cdecl по умолчанию, если вы не определили другого соглашения. Тем не менее, если вы хотите знать больше - читайте дальше... Соглашение о вызовах (calling convention) говорит компилятору такие вещи как: как передавать аргументы или как генерировать названия функций. Несколько примеров для других calling convention - это __stdcall, __pascal и __fastcall. Соглашение о вызовах принадлежит сигнатуре функции: так функции и указатели на функции с различающимися calling convention несовместимы друг с другом! Для компиляторов Borland и Microsoft вы определяете специальные calling convention между возвращаемым типом и именем функции или указателя. Для GNU GCC вы используете ключевое слово __attribute__: пишете объявление функции, сопровождаемое ключевым словом __attribute__, и затем, в двойных скобках, определяется calling convention. Если кто-то знает больше - дайте знать;-) И если вы хотите знать, что там у вызовов функций "под капотом", вы можете посмотреть главу "Подпрограммы"(Subprograms) в книге Поля Картера(Paul Carter) "PC Assembly Tutorial".

// 2.2 объявление calling convention
void __cdecl DoIt(float a, char b, char c);                             // Borland and Microsoft
void         DoIt(float a, char b, char c)  __attribute__((cdecl));     // GNU GCC

2.3  Присвоение Адреса Указателю на Функцию (Assign an address to a Function Pointer)

Присвоить указателю на функцию адрес функции довольно просто. Вы просто берёте имя соответствующей и известной функции или функции-члена. Не смотря на то, что для большинства компиляторов это необязательно, для написания переносимого кода перед именем функции вы можете поместить оператор взятия адреса &. Вы можете использовать полное имя функции-члена, включив имя класса(class-name) и оператор разрешения контекста(scope-operator) ::. Также вы должны гарантировать, что в области, где происходит присваивание, вам разрешено обратиться к функции напрямую.

// 2.3 присвоение адреса указателю на функцию
//     Замечание: хотя вы можете опустить оператор взятия адреса(&) для большинства компиляторов,
//     вы должны всегда избирать правильный путь для написания переносимого кода.

// C
int DoIt  (float a, char b, char c){ printf("DoIt\n");   return a+b+c; }
int DoMore(float a, char b, char c)const{ printf("DoMore\n"); return a-b+c; }

pt2Function = DoIt;      // короткая форма записи
pt2Function = &DoMore;   // корректное назначение с использованием оператора взятия адреса.

// C++
class TMyClass
{
public:
   int DoIt(float a, char b, char c){ cout << "TMyClass::DoIt"<< endl; return a+b+c;};
   int DoMore(float a, char b, char c) const
         { cout << "TMyClass::DoMore" << endl; return a-b+c; };

   /* продолжение TMyClass... */
};

pt2ConstMember = &TMyClass::DoMore; // корректное назначение, используя оператор взятия адреса
pt2Member = &TMyClass::DoIt; // note:  may also legally point to &DoMore

2.4  Сравнение Указателей на Функции

Вы можете использовать операторы сравнения (==, !=) так же, как обычно. В следующем примере проверяется, действительно ли pt2Function и pt2Member содержат адреса функций DoIt и TMyClass::DoMore. В случае успешного сравнения выводится текстовое сообщение.

// 2.4 сравнение указателей на функции

// C
if(pt2Function >0){                           // проверка, что указатель инициализирован
   if(pt2Function == &DoIt)
      printf("Указатель указывает на DoIt\n"); }
else
   printf("Указатель не инициализирован!!\n");


// C++
if(pt2ConstMember == &TMyClass::DoMore)
   cout << "Указатель указывает на TMyClass::DoMore" << endl;

2.5  Вызов Функции через Указатель на Функцию

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

// 2.5  вызов функции через указатель на Функцию
int result1 = pt2Function    (12, 'a', 'b');          // C короткий вариант
int result2 = (*pt2Function) (12, 'a', 'b');          // C

TMyClass instance1;
int result3 = (instance1.*pt2Member)(12, 'a', 'b');   // C++
int result4 = (*this.*pt2Member)(12, 'a', 'b');       // C++ если можно использовать указатель this

TMyClass* instance2 = new TMyClass;
int result4 = (instance2->*pt2Member)(12, 'a', 'b');  // C++, instance2 - указатель
delete instance2;

2.6  Как Передать Указатель на Функцию в качестве Аргумента?

Вы можете передать указатель на функцию в качестве входного параметра функции. Например, вам это понадобится, если вы хотите передать указатель в функцию обратного вызова. Представленный ниже код показывает, как передать указатель на функцию, которая возвращает int, а принимает float и два char'а:

//------------------------------------------------------------------------------------
// 2.6 Как Передать Указатель на Функцию

//  - указатель на функцию, которая возвращает int, а принимает float и два char'а
void PassPtr(int (*pt2Func)(float, char, char))
{
   int result = (*pt2Func)(12, 'a', 'b');     // вызов используемого указателя
   cout << result << endl;
}

// Исполняемый код примера - 'DoIt' - соответсвующая функция, такая как определены выше в 2.1-4
void Pass_A_Function_Pointer()
{
   cout << endl << "Executing 'Pass_A_Function_Pointer'" << endl;
   PassPtr(&DoIt);
}

2.7  Как Возвратить Указатель на Функцию?

Немного хитро, но указатель на функцию может быть возвращаемым значением функции. В следующем примере есть два решения того, как возвратить указатель на функцию, которая принимает два аргумента float и возвращает float. Если Вы хотите возвратить указатель на функцию-член, вы должны только изменить определения/объявления всех указателей на функции.

//------------------------------------------------------------------------------------
// 2.7 Как Возвратить Указатель на Функцию
//     'Plus' и 'Minus' определены выше. Они принимают два float и возвращают float

// Прямое решение: Функция принимает символ и возвращает указатель на функцию, 
// которая принимают два float и возвращают float. 
//  определяет, какую функцию возвратить.

float (*GetPtr1(const char opCode))(float, float)
{
   if(opCode == '+')
      return &Plus;
   else
      return &Minus; // значение по умолчанию, если введён неверный оператор
}

// Решение с использованием typedef: Определяет указатель на функцию, 
// которая принимает два float и возвращает float
typedef float(*pt2Func)(float, float);

// Функция принимает char и возвращает указатель на функцию, определёный выше через typedef.
//  определяет, какую функцию возвратить.

pt2Func GetPtr2(const char opCode)
{
   if(opCode == '+')
      return &Plus;
   else
      return &Minus; // значение по умолчанию, если введён неверный оператор
}

// Исполняемый код примера
void Return_A_Function_Pointer()
{
   cout << endl << "Выполнение 'Return_A_Function_Pointer'" << endl;

   // объявление указателя на функцию и инициализация значением NULL
   float (*pt2Function)(float, float) = NULL;

   pt2Function=GetPtr1('+');   // получение указателя на функцию 'GetPtr1'
   cout << (*pt2Function)(2, 4) << endl;   // вызов функции через указатель


   pt2Function=GetPtr2('-');   // получение указателя на функцию 'GetPtr2'
   cout << (*pt2Function)(2, 4) << endl;   // вызов функции через указатель
}

2.8  Как Использовать Массивы Указателей на Функции?

Оперирование массивами указателей на функции очень интересно. Это даёт возможность выбрать функцию, используя индекс. Синтаксис кажется трудным и часто приводит в смущение. Ниже вы найдёте два пути объявления и использования массивов указателей на функции в C и C++. В первом варианте используется typedef, во втором - прямое объявление массива. Какой из вариантов предпочтительнее, решать вам.

//------------------------------------------------------------------------------------
// 2.8 Как Использовать Массивы Указателей на Функции?

// C ---------------------------------------------------------------------------------

// typedef: 'pt2Function' может использоваться как тип
typedef int (*pt2Function)(float, char, char);

// иллюстрирует работу с массивом указателей на функции
void Array_Of_Function_Pointers()
{
   printf("\nВыполнение 'Array_Of_Function_Pointers'\n");

   // объявление массивов и инициализация каждого элемента значением NULL,  и  - массивы
   // с 10-ю указателями на функции, которые возвращают int, а принимают float и два char'а.

   // первый вариант - используя typedef
   pt2Function funcArr1[10] = {NULL};

   // второй - прямое объявление массива
   int (*funcArr2[10])(float, char, char) = {NULL};

   // присваивание адресов функций - 'DoIt' и 'DoMore' такие же функции,
   // как и определённые выше в 2.1-4
   funcArr1[0] = funcArr2[1] = &DoIt;
   funcArr1[1] = funcArr2[0] = &DoMore;
   /* дальнейшие присваивания */

   // вызов функции по индексу, указывающему на function pointer
   printf("%d\n", funcArr1[1](12, 'a', 'b'));         //  короткая форма
   printf("%d\n", (*funcArr1[0])(12, 'a', 'b'));      // "корректный" вариант вызова
   printf("%d\n", (*funcArr2[1])(56, 'a', 'b'));
   printf("%d\n", (*funcArr2[0])(34, 'a', 'b'));
}

// C++ -------------------------------------------------------------------------------

// typedef: 'pt2Member' ожет использоваться как тип
typedef int (TMyClass::*pt2Member)(float, char, char);

// иллюстрирует работу с массивом указателей на функции
void Array_Of_Member_Function_Pointers()
{
   cout << endl << "Выполнение 'Array_Of_Member_Function_Pointers'" << endl;

   // объявление массивов и инициализация каждого элемента значением NULL,  и  - массивы
   // с 10-ю указателями на функции, которые возвращают int, а принимают float и два char'а.

   // первый вариант - используя typedef
   pt2Member funcArr1[10] = {NULL};

   // второй - прямое объявление массива
   int (TMyClass::*funcArr2[10])(float, char, char) = {NULL};

   // присваивание адресов функций - 'DoIt' и 'DoMore' такие же функции,
   // как и определённые выше в 2.1-4
   funcArr1[0] = funcArr2nd[1] = &TMyClass::DoIt;
   funcArr1[1] = funcArr2[0] = &TMyClass::DoMore;
   /* дальнейшие присваивания */

   // вызов функции по индексу, указывающему на function pointer
   // замечание: для экземпляра объекта(instance) TMyClass необходимо вызывать функции-члены
   TMyClass instance;
   cout << (instance.*funcArr1[1])(12, 'a', 'b') << endl;
   cout << (instance.*funcArr1[0])(12, 'a', 'b') << endl;
   cout << (instance.*funcArr2[1])(34, 'a', 'b') << endl;
   cout << (instance.*funcArr2[0])(89, 'a', 'b') << endl;
}

3.  Как Осуществить Обратные Вызовы в C и C++?

3.1  Введение в Концепцию Функций Обратных Вызовов

Указатели на функции обеспечивают концепцию функций обратного вызова. Если вы не уверены относительно того, как использовать указатели на функции, вернитесь к разделу "Введение в Указатели на Функции". Я попробую ввести понятие функций обратного вызова, используя известную функцию сортировки, qsort. Эта функция сортирует элементы области в соответстви с пользовательской сортировкой. Область может содержать элементы любого типа; она передается в функцию сортировки через укзатель на void. Также должны быть переданы размер элемента и общее количество элементов в области. Теперь вопрос: как может функция сортировки отсортировать элементы без какой-либо информации об их типе? Ответ прост: функция получает указатель на функцию сравнения, которая принимает void-указатели на два элемента области, сравнивает элементы и возвращает результат, закодированный как int. Таким образом каждый раз, когда алгоритму сортировки требуется результат сравнения двух элементов, он только вызывает функцию сравнения через указатель функции: выполняет обратный вызов!

3.2  Как Выполнить Обратный Вызов в C?

Я только беру объявление функции qsort, которое читается следующим образом: void qsort( void* field, size_t nElements, size_t sizeOfAnElement, int(_USERENTRY *cmpFunc)(const void*, const void*)); field указывает на первый элемент области, которая должна быть отсортирована, nElements - число элементов в области, sizeOfAnElement - размер одного элемента в байтах и cmpFunc - указатель на функцию сравнения. Эта функция сравнения принимает два указателя на void и озвращает int. Синтаксис использования указателя на функцию в качестве параметра выглядит немного странно. Посмотрите только, как объявить указатель на функцию, и вы увидите, что в действительности это одно и то же. Обратный вызов выполняется точно так же, как и обычная функция: только вы используете имя указателя на функцию вместо имени функции. Это показано ниже. Замечание: Все аргументы вызова, кроме указателя на функцию, были опущены, чтобы сконцентрироваться на важных вещах.

   void qsort( ... , int(_USERENTRY *cmpFunc)(const void*, const void*))
   {
      /* алгоритм сортировки  - замечание: item1 и item2 - имеют тип void* */

      int bigger=cmpFunc(item1, item2);  // проиводится обратный вызов

      /* использование результата */
   }

3.3  Код примера использования qsort

В следующем приметре сортируется массив элементов типа float.

   //-----------------------------------------------------------------------------------------
   // 3.3 Как выполнить обратный вызов в C с использованием функции сортировки qsort
   #include <stdlib.h>        // необходимо для:qsort
   #include <time.h>          //       	      randomize
   #include <stdio.h>         //                printf

   // функция сравнения для алгоритма сортировки
   // два элемента передаются как указатели на void, преобразуются и сравниваются
   int CmpFunc(const void* _a, const void* _b)
   {
      // вы должны выполнить явное приведение к правильному типу
      const float* a = (const float*) _a;
      const float* b = (const float*) _b;

      if(*a > *b) return 1;              // первый элемент больше, чем второй -> return 1
      else
         if(*a == *b) return  0;         // равны -> return 0
         else         return -1;         // второй элемент больше, чем первый -> return -1
   }

   // пример использования qsort()
   void QSortExample()
   {
      float field[100];

      ::randomize();                     // инициализация генератора случайных чисел
      for(int c=0;c<100;c++)             // заполнение массива случайными числами
         field[c]=random(99);

      // сортировка, используя qsort()
      qsort((void*) field, /*количество элементов*/ 100, /*размер элемента*/ sizeof(field[0]),
            /*функция сравнения*/ CmpFunc);

      // отображение первых десяти элементов отсортированного массива
      printf("Первые десять элементов отсортированного массива ...\n");
      for(int c=0;c<10;c++)
         printf("элемент #%d содержит %.0f\n", c+1, field[c]);
      printf("\n");
   }

3.4  Как Осуществить Обратный Вызов в Статческой Функции-Члене?

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

3.5  Как Осуществить Обратный Вызов в Нестатческой Функции-Члене? Подход Обертки(The Wrapper Approach)

Указатели на нестатические функции-члены отличаются от простых укзателей на функции в C, поскольку им требуется передавать объект класса через указатель на void. Таким образом, простые указатели на функции и нестатические функции-члены имеют разные и несовместимые сигнатуры! Если вы хотите обратиться к члену определенного класса, вы только должны в вашем коде заменить простой указатель на функцию указателем на функцию-член. Но что вы можете сделать, если захотите вызвать нестатичский член произвольного класса? Это несколько затруднительно. Вам необходимо написать статическую функцию-член в качестве обёртки. Статическая функция-член имеет такую же сигнатуру как функция в C! Потом вы приводите указатель на объект, функцию-член которого вы хотите вызвать, к void* и передаёте его в обёртку в качестве дополнительного параметра или через глобальную переменную. Если вы используете глобальную переменную, очень важно удостовериться, что она всегда будет указывать на правильный объект! Конечно, вы также можете передать параметры вызова для функции-члена. Обёртка приводит указатель на void к указателю на объект соответствуюего класса и вызывает функцию-член. Ниже приводится два примера.

Пример А:  Указатель, инстанцированный в классе, передается в качестве дополнительного параметра

Функция DoItA делает что-то с объектами класса TClassA, который содержит обратный вызов. Таким образом указатель на объект класса TClassA и указатель на статическую функцию-обёртку TClassA::Wrapper_To_Call_Display передаются в DoItA. Эта обёртка является функцией обратного вызова. Вы можете написать другие произвольные классы такие же, как TClassA и использовать их с DoItA, пока они обеспечивают необходимые функции. Замечание: это решение может быть полезным, если вы проектируете собственный интерфейс обратного вызова. Оно намного лучше чем второе решение, которое использует глобальную переменную.

   //-----------------------------------------------------------------------------------------
   // 3.5 Пример A: Обратный вызов функции-члена, используя дополнительный параметр
   // Задача: Функция 'DoItA' делает что-то, что содержит обратный вызов функции-члена 'Display'.
   // Таким образом используется функция-обёртка 'Wrapper_To_Call_Display'.

   #include <iostream.h>   // необходимо для: cout

   class TClassA
   {
   	public:

      void Display(const char* text) { cout << text << endl; };
      static void Wrapper_To_Call_Display(void* pt2Object, char* text);

      /* другие члены TClassA */
   };


   // статическая функция-обёртка для обратного вызова функции-члена Display()
   void TClassA::Wrapper_To_Call_Display(void* pt2Object, char* string)
   {
       // точное приведение к указателю на TClassA
       TClassA* mySelf = (TClassA*) pt2Object;

       // вызов функции-члена
       mySelf->Display(string);
   }

   // функция делает что-то, содержащее обратный вызов
   // замечание: конечно эта функция так же может быть функцией-членом
   void DoItA(void* pt2Object, void (*pt2Function)(void* pt2Object, char* text))
   {
      /* что-то делаем... */

      pt2Function(pt2Object, "привет, я обратный вызов через параметр ;-)");  // выполняется обратный вызов
   }

   // исполнение кода примера
   void Callback_Using_Argument()
   {
      // 1. создается объект класса TClassA
      TClassA objA;

      // 2. вызов 'DoItA' для 
      DoItA((void*) &objA, TClassA::Wrapper_To_Call_Display);
   }

Пример Б:  Указатель, инстанцированный в классе, хранится в глобальной переменной

Функция DoItB делает что-то с объектами класса TClassA, который содержит обратный вызов. Указатель на статическую функцию-обёртку TClassA::Wrapper_To_Call_Display передаётся в DoItA. Эта обёртка и есть функция обратного вызова. Обёртка использует глобальную переменную void* pt2Object и выполняет точное приведение её к указателю на инстанс TClassB. Очень важно, что вы всегда инициализируете глобальную переменную так, чтобы указать на правильный инстанс класса. Вы можете написать другие произвольные классы такие же, как TClassB и использовать их с DoItA, пока они обеспечивают необходимые функции. Замечание: это решение может оказаться полезным, если вы используете интерфейс обратного вызова, который не может быть изменён. Это не очень хорошее решение, потому чтоиспользование глобальной переменной очень опасно и может послужить причиной серьёзных ошибок.

   //-----------------------------------------------------------------------------------------
   // 3.5 Пример B: Обратный вызов функции-члена, используя глобальную переменную
   // Задача: Функция 'DoItA' делает что-то, что содержит обратный вызов функции-члена 'Display'. 
   // Таким образом используется функция-обёртка 'Wrapper_To_Call_Display'.

   #include <iostream.h>   // необходимо для: cout

   void* pt2Object;        // global variable which points to an arbitrary object

   class TClassB
   {
   public:

      void Display(const char* text) { cout << text << endl; };
      static void Wrapper_To_Call_Display(char* text);

      /* другие члены класса TClassB */
   };

   // статическая функция-обёртка для обратного вызова функции-члена Display()
   void TClassB::Wrapper_To_Call_Display(char* string)
   {
       // точное приведение глобальной переменной  к указателю на TClassB
       // предупреждение:  ДОЛЖЕН указывать на соответствующий объект!
       TClassB* mySelf = (TClassB*) pt2Object;

       // вызов функции-члена
       mySelf->Display(string);
   }

   // функция делает что-то, содержащее обратный вызов
   // заиечание: конечно эта функция так же может быть функцией-членом
   void DoItB(void (*pt2Function)(char* text))
   {
      /* что-то делаем... */

      pt2Function("Привет, я обратный вызов, используя глобальную переменную ;-)");   // выполняется обратный вызов
   }

   // исполнение кода примера
   void Callback_Using_Global()
   {
      // 1. создается объект класса TClassA
      TClassB objB;

      // 2. установка глобальной переменной, которая используется в статической функции-обёртке
      // важно: никогда не забывайте делать это!!
      pt2Object = (void*) &objB;

      // 3. вызов 'DoItB' для 
      DoItB(TClassB::Wrapper_To_Call_Display);
   }

4.  Функторы для инкапсуляции Указателей на Функции в C и C++

4.1  Что такое Функторы?

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

4.2  Как Имплементировать Функторы?

Во-первых, вам нужен базовый класс TFunctor который предоставляет виртуальную функцию с именем Call или виртуальный перегруженный оператор () с помощью которого, вы сможете вызвать функцию-член. Что вы предпочтёте, перегруженный оператор или функцию наподобие Call, - зависит от вас. От базового класса вы наследуете шаблонный класс TSpecificFunctor, который инициализируется конструктором, принимающим указатель на объект и указатель на функцию-член. Производный класс переопределяет функцию Call и/или оператор () базового класса: в переопределённой версии вы вызываете функцию-член, используя сохранённые указатели на объект и функцию-член. Если вы не уверены, как испоьзовать указатели на функции, обратитесь к моему Введению в Указатели на Функции.

   //-----------------------------------------------------------------------------------------
   // 4.2 Как Имплементировать Функторы

   // абстрактный базовый класс
   class TFunctor
   {
   		public:

      // две возможные функции для вызова функции-члена; виртуальные по причине наследования
      // для вызова функции классы будут использовать указатель на объект и указатель на функцию-член
      virtual void operator()(const char* string)=0;  // вызов, используя оператор
      virtual void Call(const char* string)=0;        // вызов, используя функцию
   };


   // производный шаблонный класс
   template  class TSpecificFunctor : public TFunctor
   {
   private:
      void (TClass::*fpt)(const char*);   // указатель на функцию-член
      TClass* pt2Object;                  // указатель на объект

   public:

      // constructor - принимает указатель на объект, указатель на член
      // и сохраняет их в двух закрытых переменных
      TSpecificFunctor(TClass* _pt2Object, void(TClass::*_fpt)(const char*))
         { pt2Object = _pt2Object;  fpt=_fpt; };

      // перегруженный оператор "()"
      virtual void operator()(const char* string)
       { (*pt2Object.*fpt)(string);};              // выполнение функции-члена

      // перегруженная функция "Call"
      virtual void Call(const char* string)
        { (*pt2Object.*fpt)(string);};             // выполнение функции-члена
   };

4.3  Пример Использования Функторов

В следующем примере у нас имеется два учебных класса, они предоставляют функцию именуемую 'Display', которая ничего не возвращает (void) и требует в качестве входного параметра строку (const char*). Мы создаем массив из двух указателей на TFunctor и инициализируем его двумя указателями на TSpecificFunctor, которые инкапсулируют указатели на объекты и указатели на члены TClassA и TClassB соответственно. Затем мы используем массив функторов для вызова соответствующих функций-членов. Чтобы выполнить вызовы функций, указатель на объект не требуется, и вы не должны больше беспокоиться о классах!

   //-----------------------------------------------------------------------------------------
   // 4.3 Пример Использования Функторов

   // dummy class A
   class TClassA{
   public:

      TClassA(){};
      void Display(const char* text) { cout << text << endl; };

      /* другие члены класса TClassA */
   };

   // dummy class B
   class TClassB{
   public:

      TClassB(){};
      void Display(const char* text) { cout << text << endl; };

      /* другие члены класса TClassB */
   };


   // функция main
   int main(int /*argc*/, char* /*argv[]*/)
   {
      // 1. создание объектов класса TClassA и TClassB
      TClassA objA;
      TClassB objB;


      // 2. создание объектов класса TSpecificFunctor...
      //    a ) функтор, который инкапсулирует указатель на объект и член класса TClassA
      TSpecificFunctor specFuncA(&objA, &TClassA::Display);

      //    b) функтор, который инкапсулирует указатель на объект и член класса TClassA
      TSpecificFunctor specFuncB(&objB, &TClassB::Display);


      // 3. создание и инициализация массива указателей на базовый класс TFunctor
      TFunctor* vTable[] = { &specFuncA, &specFuncB };


      // 4. использование массива для вызова фунций-членов без обращения к объектам
      vTable[0]->Call("TClassA::Display called!");        // посредством функции "Call"
      (*vTable[1])   ("TClassB::Display called!");        // с помощью оператора "()"


      // нажать 'Enter' для завершения
      cout << endl << "Для завершения нажмите 'Enter'!" << endl;
      cin.get();

      return 0;
   }

5.  Специальные Ссылки

5.1  Введения в Указатели на Функции

Использование Указателей на Функции-Члены
Подробная статья от borland-сообщества. Первоначально предназначенная для их продукта BC3.1, но фактически независимая платформа и все еще обновляемая. [Статья]
Классы, содержащие Указатели на Члены
Глава 15 Аннотаций C++ Френка Броккена(Frank Brokken). [Руководство]
C++ FAQ-Lite
FAQ от группы новостей news.comp.lang.c++. Эта ссылка указывает на раздел об указателях на функции-члены. [FAQ]
Указание на Члены Класса
Короткая и практичная статья о том как работать с указателями на функции-члены. [Руководство]
Объявление Указателей на Функции и Выполнение Обратных Вызовов.
Короткая и практичная статья об указателях на функции в C. Также рассказывает кое-что о принадлежащем сигнатуре функции соглашении о вызовах. [Руководство]
Замечания об Указателях на Функции
Короткая страничка руководства пытается продемонстрировать идею компонентного программирования: разделение алгоритма и данных. [Руководство]

5.2  Обратные Вызовы и Функции

Обратные вызовы в C++
Бесплатная библиотека обратных вызовов и научная, но все еще практичная документация по обратным вызовам, освещающая почти все возможные способы выполнения вызовов. Полный исходный текст для каждого шага, представленного в документации. [Сайт]
Использование Шаблонных Функторов в Обратных Вызовах в C++
Детальная документация от Рича Хикки(Rich Hickey) о концепции обратных вызовов и использовании шаблонных функторов для их реализации. Документация к бесплатной бибилиотеке обратных вызовов Рича Хикки(Rich Hickey). Так как его сайт закрыт, вы можете скачать библиотеку здесь. [Статья]
Использование Шаблонных Функторов в Обратных Вызовах в C++ II
Руководство по использованию механизмов обратных вызовов Ричи Хикки(Rich Hickey). Также объясняет, почему другие, более тривиальные решения, плохи. [Руководство]
Майелин(Myelin): Как писать Обратные Вызовы на C++ и COM
Короткая статья, объясняющая, как делать обратные вызовы, используя абстрактный класс. [Статья]
Майелин(Myelin): Создание делегатов в C++
Как реализовать делегаты в C++ ... [Статья]
Обратные Вызовы в C++: Объектно-Ориентированный Подход (The OO Way).[Статья]
Решение для Обратных Вызовов в C++
Выполнение обратных вызовов членов, используя шаблоны. [Статья]
Указатели на Функции-Члены и Самые Быстрые Делегаты C++
Как реализовать делегаты в C++ ... [Статья]
Обратные Вызовы
Короткая страничка об обратных вызовах и функторах. [Руководство]
Сравнение Механизмов Обратных Вызовов
В статье сравниваются удобство и простота исполнения пяти альтернатив для осуществления механизмов обратных вызовов в C++. [Статья]
libsigc++
Наиболее полная библиотека обратных вызовов подобного рода с возожностью подключать абстрактные обратные вызовы к членам класса, функциям или функциональным объектам. [Библиотека]
Библиотека Функций Boost
Обертки функциональных объектов для различных вызовов(calls) или обратных вызовов(callbacks) Дага Грегора(Doug Gregor). Документация! Скачивать с www.boost.org [Библиотека]
Библиотека Функций Boost
Усовершенствованные адаптеры функциональных объектов Марка Роджерса(Mark Rodgers). Документация! Скачивать с www.boost.org. [Библиотека]

5.3  Разное

Руководство по Ассемблированию на ПК
Хорошее учебное пособие по трансляции. Здесь вы сможете проникнуть в суть того, что происходит во время вызова функции. Включает в себя соглашения о вызовах и описание, как связать трансляцию и код C. [Руководство]
Механизм Виртуальных Функций
Короткая страничка о механизме виртуальных функций, используемом компилятором MS Visual C++. [Статья]

Замечания

Раздел 3.5. Пример A

В данном примере смущает преобразование указателя на объект класса TClassA к void* при передаче в функцию DoItA, а затем обратное преобразование к указателю на TClassA в функции-обертке TClassA::Wrapper_To_Call_Display (то же самое относится и к примеру Б). Подобные конструкции подойдут (потому, как другого ничего не остаётся), если необходимо работать с функциями типа pthread_create()(libpthread.so), которые в качестве входных параметров принимают void*. В любом случае этот пример будет работать правильно только, если в DoItA действительно передаётся указатель на TClassA. Но что произойдёт, если по ошибке вместо указателя на TClassA будет передано нечто другое? Компилятор не распознает ошибки, ничто не помешает преобразовать к void* указатель на int, например. Для наглядности я немного изменил пример (exampleА.cpp):

Нетрудно догадаться, что функция DoItA, получив в качестве входного параметра указатель на int, отработает, мягко говоря, не так как ожидалось. Вместо значения TClassA::x отобразится неопределенный набор бит, проще говоря - мусор.
#include <iostream> 

class TClassA
{
	int a;
   	int x;
  public:
	TClassA(){};
	TClassA(int x_):x(x_){};
  	void Display(const char* text) { std::cout << text <<"..."<< x <<'\n'; };
        static void Wrapper_To_Call_Display(void* pt2Object, char* text);
};

void TClassA::Wrapper_To_Call_Display(void* pt2Object, char* string)
{
	TClassA* mySelf = (TClassA*) pt2Object;
  	mySelf->Display(string);
}
   
void DoItA(void* pt2Object, void (*pt2Function)(void* pt2Object, char* text))
{
	pt2Function(pt2Object, привет, я обратный вызов через параметр ;-)");//
}
   
void Callback_Using_Argument()
{
	TClassA objA(123); 
  	int objAi=0;
     
  	DoItA((void*)&objA, TClassA::Wrapper_To_Call_Display);//отобразит значение TClassA::x
  	DoItA((void*)&objAi, TClassA::Wrapper_To_Call_Display);//отобразит какой-то мусор 
}
   
int main()
{
	Callback_Using_Argument();
  	return 0;
}
Возможное решение данной проблемы - сделать Wrapper_To_Call_Display и DoItB шаблонными функциями (exampleB.cpp). Поскольку для int нет подходящей специализации, компилятор выдаст сообщение об ошибке.
#include <iostream> 

class TClassA
{
	int a;
	int x;
  public:
	TClassA(){};
	TClassA(int x_):x(x_){};
  	void Display(const char* text) { std::cout << text <<"..."<< x <<'\n'; };
        static void Wrapper_To_Call_Display(void* pt2Object, char* text);
};

class TClassB
{
	int b;
  	int x;
  public:
  	TClassB(){};
	TClassB(int x_):x(x_){};  
  	void Display(const char* text) { std::cout << text <<"..."<< x <<'\n'; };
        template<class T> static void Wrapper_To_Call_Display(T* pt2Object, char* text);
};
   
template<class T> void TClassB::Wrapper_To_Call_Display(T* pt2Object, char* string)
{
	pt2Object->Display(string);
}     
   
template<class T> void DoItB(T* pt2Object, void (*pt2Function)(T* pt2Object, char* text))
{
	pt2Function(pt2Object, "привет, я обратный вызов через параметр ;-)");
}   
     
void Callback_Using_Argument()
{
	TClassA objA;
	TClassB objB(456);  
  	int b=0;
     
  	DoItB(&objB, &TClassB::Wrapper_To_Call_Display);//работает
  	DoItB(&objA, &TClassB::Wrapper_To_Call_Display);//и это тоже	   
////	DoItB(&b, &TClassB::Wrapper_To_Call_Display);//а здесь компилятор ругнется примерно так: 
/*	-- exampleB.cpp: In function `static void TClassB::Wrapper_To_Call_Display<int>(int *, char *)':
	-- exampleB.cpp:50:   instantiated from here
	-- exampleB.cpp:27: request for member `Display' in `*pt2Object', which is of non-aggregate type `int'*/
     
}
   
int main()
{
   	Callback_Using_Argument();
   	return 0;
}

Сайт управляется системой uCoz