Указатели на функции обеспечивают несколько чрезвычайно интересных, эффективных и изящных программных методов. Вы можете использовать их, чтобы заменить switch/if-операторы, реализовать ваше собственное позднее связывание (late-binding) или обратные вызовы (callbacks). К сожалению, вероятно из-за их сложного синтаксиса, в большинстве компьютерных книг и документации к ним относятся весьма неприязненно. Если о них и говорится, то довольно-таки кратко и поверхностно. Они в меньшей степени подвержены ошибкам, чем обычные указатели, вы никогда не будете выделять или освобождать для них память. Все, что вы должны сделать, - это понять, чем они являются и изучить их синтаксис. Но имейте в виду: вы всегда должны спрашивать себя, действительно ли вы нуждаетесь в указателе на функцию. Приятно реализовать собственное позднее связывание, но использование существующих структур C++ может сделать ваш код удобочитаемым и более понятным. Один из аспектов в случае позднего связывания - время выполнения: если вы вызываете виртуальную функцию, ваша программа должна определить, какую функцию нужно вызвать. Это делается посредством виртуальной таблицы (V-Table), содержащей все возможные функции. На каждый вызов затрачивается некоторое время, и возможно вы можете сократить время, используя указатели на функции вместо виртуальных функций. Возможно, и нет...
Указатели на функции - это указатели, т.е. переменные, которые указывают на адрес функции. Вы должны помнить, что запущенная программа получает определённую область в оперативной памяти. И исполняемый скомпилированный программный код, и используемые переменные, размещаются в этой памяти. Таким образом, функция в коде программы так же как, например, символьное поле, ничто иное, как адрес. Очень важно, как вы, или лучше ваш компилятор/процессор, интерпретируете память, на которую указывает указатель.
Когда вы хотите вызвать функцию 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); }
Важное замечание: Указатель на функцию всегда указывает на функцию со строго определённой сигнатурой! Так все функции, к которым вы хотите обращаться через указатель, должны иметь такие же входные параметры и возвращаемое значение (что и в объявлении указателя прим. пер.).
Рассматривая синтаксис, можно выделить два типа указателей на функции: один из них - это указатели на обычные функции 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++
Обычно, вы не думаете о соглашении о вызове функций (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
Присвоить указателю на функцию адрес функции довольно просто. Вы просто берёте имя соответствующей и известной функции или функции-члена. Не смотря на то, что для большинства компиляторов это необязательно, для написания переносимого кода перед именем функции вы можете поместить оператор взятия адреса &. Вы можете использовать полное имя функции-члена, включив имя класса(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
Вы можете использовать операторы сравнения (==, !=) так же, как обычно. В следующем примере проверяется, действительно ли 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;
В 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;
Вы можете передать указатель на функцию в качестве входного параметра функции. Например, вам это понадобится, если вы хотите передать указатель в функцию обратного вызова. Представленный ниже код показывает, как передать указатель на функцию, которая возвращает 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); }
Немного хитро, но указатель на функцию может быть возвращаемым значением функции. В следующем примере есть два решения того, как возвратить указатель на функцию, которая принимает два аргумента 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; // вызов функции через указатель }
Оперирование массивами указателей на функции очень интересно. Это даёт возможность выбрать функцию, используя индекс. Синтаксис кажется трудным и часто приводит в смущение. Ниже вы найдёте два пути объявления и использования массивов указателей на функции в 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; }
Указатели на функции обеспечивают концепцию функций обратного вызова. Если вы не уверены относительно того, как использовать указатели на функции, вернитесь к разделу "Введение в Указатели на Функции". Я попробую ввести понятие функций обратного вызова, используя известную функцию сортировки, qsort. Эта функция сортирует элементы области в соответстви с пользовательской сортировкой. Область может содержать элементы любого типа; она передается в функцию сортировки через укзатель на void. Также должны быть переданы размер элемента и общее количество элементов в области. Теперь вопрос: как может функция сортировки отсортировать элементы без какой-либо информации об их типе? Ответ прост: функция получает указатель на функцию сравнения, которая принимает void-указатели на два элемента области, сравнивает элементы и возвращает результат, закодированный как int. Таким образом каждый раз, когда алгоритму сортировки требуется результат сравнения двух элементов, он только вызывает функцию сравнения через указатель функции: выполняет обратный вызов!
Я только беру объявление функции 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); // проиводится обратный вызов /* использование результата */ }
В следующем приметре сортируется массив элементов типа 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"); }
Точно так же, как это осуществляется в функциях 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); }
Функторы это - функции с состоянием. В 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; // вызов, используя функцию }; // производный шаблонный класс templateclass 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);}; // выполнение функции-члена };
В следующем примере у нас имеется два учебных класса, они предоставляют функцию именуемую '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 TSpecificFunctorspecFuncA(&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; }
В данном примере смущает преобразование указателя на объект класса TClassA к void* при передаче в функцию DoItA, а затем обратное преобразование к указателю на TClassA в функции-обертке TClassA::Wrapper_To_Call_Display (то же самое относится и к примеру Б). Подобные конструкции подойдут (потому, как другого ничего не остаётся), если необходимо работать с функциями типа pthread_create()(libpthread.so), которые в качестве входных параметров принимают void*. В любом случае этот пример будет работать правильно только, если в DoItA действительно передаётся указатель на TClassA. Но что произойдёт, если по ошибке вместо указателя на TClassA будет передано нечто другое? Компилятор не распознает ошибки, ничто не помешает преобразовать к void* указатель на int, например. Для наглядности я немного изменил пример (exampleА.cpp):
#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; }