void run(ptr_callback ptrCallback, void* contextData = nullptr) // (1)
{
int eventID = 0;
ptrCallback(eventID, contextData);
}
void run(ptr_callback_static ptrCallback, Executor* contextData = nullptr) // (2)
{
int eventID = 0;
ptrCallback(eventID, contextData);
}
void run(Executor* ptrClientCallbackClass, ptr_callback_method ptrClientCallbackMethod) // (3)
{
int eventID = 0;
(ptrClientCallbackClass->*ptrClientCallbackMethod)(eventID);
}
void run(Executor callbackHandler) // (4)
{
int eventID = 0;
callbackHandler(eventID);
}
Можно заметить, что все реализации, по сути, одинаковы, отличаются только типы и количество входных аргументов. Поэтому, можно попытаться сделать шаблон. Возьмем наиболее простой случай, когда функция на вход принимает только один параметр (Листинг 25):
template
void run(CallbackArgument callbackHandler)
{
int eventID = 0;
//Some actions
callbackHandler(eventID);
}
Получившийся шаблон подходит для реализации вызовов с помощью функциональных объектов (в Листинг 25 это строка номер 4), а также для лямбда-выражений. В последнем случае в качестве типа аргумента будет подставлен тип лямбда-выражения, определяемый компилятором.
Что же нам делать для остальных реализаций? Для указателей на функцию и указателей на статический метод (строки 1 и 2) можно сделать отдельный шаблон с двумя параметрами (Листинг 26):
template
void run(CallbackArgument callbackHandler, Context* context)
{
int eventID = 0;
//Some actions
callbackHandler(eventID, context);
}
Однако такое решение противоречит идее обобщенного кода: для нового типа данных мы реализуем новый код, который дублирует предыдущий, за исключением самого вызова. Как следствие, если в коде инициатора нужно сделать изменения, их придется переносить на все объявленные функции. Но это еще не все: указанное решение не покрывает случая использования указателей на метод-член класса, синтаксис вызова которого отличается от синтаксиса вызова внешней функции. Таким образом, придется реализовать еще один шаблон функции для вызова метода класса. При этом он должен будет иметь другое имя, иначе возникнет конфликт с предыдущим определением: количество входных параметров одинаково, и компилятор не знает, какую реализацию шаблона подставлять при инстанциировании.
Вот если бы мы могли для всех аргументов использовать единый общий параметр, тогда все реализации могли быть описаны с помощью одного единственного шаблона. Решить эту задачу можно путем преобразования вызовов.
4.2.2. Преобразование вызовов
Для преобразования вызовов используется функциональный объект, в котором хранятся данные, необходимые для осуществления обратного вызова. Объявляется перегруженный оператор, который принимает информацию вызова. Реализация оператора выполняет требуемый вызов, передавая ему на вход полученную информацию вызова и, дополнительно, хранимые данные18.
Вначале рассмотрим вызовы через указатели на функцию. Создадим шаблон для функционального объекта, в котором будем хранить указатель на функцию и контекст. Перегрузим оператор вызова функции, в реализации которого по хранимому указателю вызовем функцию-обработчик и передадим ей хранимый контекст (Листинг 27).
template
class CallbackConverter // (2)
{
public:
CallbackConverter (Function argFunction = nullptr, Context argContext = nullptr) // (3)
{
ptrFunction = argFunction; context = argContext;
}
void operator() (int eventID) // (4)
{
ptrFunction(eventID, context); // (5)
}
private:
Function ptrFunction; // (6)
Context context; // (7)
};