Глава 15
Разные функции
15.0. Введение
В этой главе рассматриваются некоторые аспекты C++, которые плохо вписываются в тематику любой другой главы: указатели функций и членов, константные переменные и функции- члены, независимые операторы (т.е. не члены класса) и несколько других тем.
15.1. Применение указателей функций для их обратного вызова
Проблема
Планируется использование некоторой функции func1, которая на этапе выполнения должна вызывать другую функцию func2. Однако по той или иной причине нельзя внутри функции func1 жестко закодировать имя функции func2. Возможно, имя функции func2 неизвестно на этапе компиляции, или func1 относится к программному интерфейсу независимого разработчика, и она не может быть изменена и перекомпилирована В любом случае вам придется воспользоваться функцией обратного вызова (callback function).
Решение
При использовании указанных выше функций объявите func1 с указателем на функцию в качестве своего аргумента и передайте ей адрес func2 на этапе выполнения. Используйте typedef, чтобы программа легче читалась и отлаживалась. Пример 15.1 показывает, как можно реализовать функцию обратного вызова, используя указатель на функцию.
Пример 15.1. Функция обратного вызова
#include <iostream>
// Пример функции обратного вызова
bool updateProgress(int pct) {
std::cout << pct << "% complete...n";
return(true);
}
// Этот typedef делает программный код более понятным
typedef bool (*FuncPtrBoolInt)(int);
// Функция, которая выполняется достаточно длительное время
void longOperation(FuncPtrBoolInt f) {
for (long l=0; l < 100000000; l++)
if (l % 10000000 == 0)
f(l/1000000);
}
int main() {
longOperation(updateProgress); // нормально
}
Обсуждение
В ситуации, которая показана в примере 15.1, применение указателя на функцию является хорошим решением, если UpdateProgress и longOperation ничего не должны знать друг о друге. Например, функцию, которая обновляет индикатор состояния процесса в диалоговом окне пользовательского интерфейса (user interface — UI), в окне консольного режима или где-то еще, не заботит контекст, в котором она вызывается. Аналогично функция longOperation может быть частью некоторого программного интерфейса загрузки данных, которого не заботит место вызова: из графического UI, из окна консольного режима или из фонового процесса.
Сначала потребуется определить сигнатуру функции, которую вы планируете вызывать, и создать для нее typedef. Оператор typedef — ваш помощник в тех случаях, когда приходится иметь дело с указателями функций, потому что они имеют не очень привлекательный синтаксис. Рассмотрим, как обычно объявляется такой указатель на примере переменной f, которая содержит адрес функции, принимающей единственный аргумент целого типа и возвращающей значения типа boolean. Это может выглядеть следующим образом
bool (*f)(int); // f - имя переменной
Вы можете справедливо возразить, что здесь нет ничего особенного и я просто излишне драматизирую ситуацию. Но что вы скажете, если требуется определить вектор vector таких указателей?
vector<bool (*)(int)> vf;
Или их массив?
bool (*af[10])(int);
Форма представления указателей на функции отличается от обычных переменных С++, которые обычно задаются в виде (квалифицированного) имени типа, за которым идет имя переменной. Поэтому они вносят путаницу при чтении программного кода.
Итак, в примере 15.1 я использовал следующий typedef.
typedef bool (*FuncPtrBoolInt)(int);
Сделав это, я могу свободно объявлять указатели функций с сигнатурой, возвращающей значение bool и принимающей единственный аргумент, как это я бы делал для параметра любого другого типа, например.
void longOperation(FuncPtrBoolInt f) { // ...
Теперь все, что надо сделать в longOperation, — это вызвать f, как если бы это была любая обычная функция.
f(l/1000000);
Таким образам, здесь f может быть любой функцией, которая принимает аргумент целого типа и возвращает bool. Предположим, что в вызывающей функции longOperation не требуется обеспечивать продвижение индикатора состояния процесса. Тогда ей можно передать указатель на функцию без операций.
bool whoCares(int i) {return(true);}
//...
longOperation(whoCares);
Более важно то, что выбор функции, передаваемой longOperation, может осуществляться динамически на этапе выполнения.
15.2. Применение указателей для членов класса
Проблема
Требуется обеспечить адресную ссылку на данное-член или на функцию-член.
Решение
Используйте имя класса и оператор области видимости (::) со звездочкой для правильного квалифицирования имени. Пример 15.2 показывает, как это можно сделать.
Пример 15.2. Получение указателя на член класса
#include <iostream>
#include <string>
class MyClass {
public:
MyClass() : ival_(0), sval_("foo") {}
~MyClass() {}
void incr() {++ival_;}
void decr() {ival_--;}
private:
std::string sval_;
int ival_;
};
int main() {
MyClass obj;
int MyClass::* mpi = &MyClass::ival_; // Указатели на
std::string MyClass::* mps = &MyClass::sval_; // данные-члены
void (MyClass::*mpf)(); // Указатель на функцию-член, у которой
// нет параметров и которая возвращает void
void (*pf)(); // Обычный указатель на функцию
int* pi = &obj.ival_; // int-указатель, ссылающийся на переменную-член
// типа int, - все нормально.
mpf = &MyClass::incr; // Указатель на функцию-член. Вы не можете
// записать это значение в поток. Посмотрите в
// отладчике, как это значение выглядит.
pf = &MyClass::incr; // Ошибка: &MyClass::inc не является экземпляром
// функции
std::cout << "mpi = " << mpi << 'n';
std::cout << "mps = " << mps << 'n';
std::cout << "pi = " << pi << 'n';
std::cout << "*pi = " << *pi << 'n';
obj.*mpi = 5;
obj.*mps = "bar";
(obj.*mpf)(); // теперь obj.ival_ равно 6
std::cout << "obj.ival_ = " << obj.ival_ << 'n';
std::cout << "obj.sval_ = " << obj.sval_ << 'n';
}
Обсуждение
Указатели на члены класса выглядят и работают иначе, чем обычные указатели. Прежде всего, они имеют «смешной» синтаксис (не вызывающий смех, но странный). Рассмотрим следующую строку из примера 15.2.
int MyClass::* mpi = &MyClass::ival_;
Здесь объявляется указатель и ему присваивается значение целого типа, которым оказывается член класса MyClass. Две вещи отнимают это объявление от обычного int*. Во-первых, вам приходится вставлять имя класса и оператор области видимости между типом данного и звездочкой. Во-вторых, при выполнении операции присваивания этому указателю на самом деле не назначается какой то определенный адрес памяти. Значение &MyClass::ival_ не является каким-то конкретным значением, содержащимся в памяти; оно ссылается на имя класса, а не на имя объекта, но тогда что же это такое на самом деле? Можно представить это значение как смешение данного-члена относительно начального адреса объекта.
Переменная mpi должна использоваться совместно с экземпляром класса, к которому она применяется. Немного ниже в примере 15.2 располагается следующая строка, которая использует mpi для присваивания целого числа значению, на которое ссылается указатель mpi.
obj.*mpi = 5;
obj является экземпляром класса MyClass. Ссылка на член с использованием точки (или ->, если у вас имеется указатель на obj) и разыменование mpi позволяют вам получить ссылку на obj.ival_.
Указатели на функции-члены действуют фактически так же. В примере 15.2 объявляется указатель на функцию-член MyClass, которая возвращает void и не имеет аргументов.
void (MyClass::*mpf)();
Ему можно присвоить значение с помощью оператора адресации.
mpf = &MyClass::incr;
Для вызова функции заключите основное выражение в скобки, чтобы компилятор понял ваши намерения, например:
(obj.*mpf)();
Однако имеется одно отличие в применении указателей на данные-члены и указателей на функции члены. Если необходимо использовать обычный указатель (не на член класса) на данное-член, просто действуйте обычным образом.