void run() // (4)
{
int eventID = 0;
//Some actions
callbackHandler(ptrClass, eventID); // (5)
}
private:
std::function<void(ClassName*, int)> callbackHandler; // (6)
ClassName* ptrClass = nullptr; // (7)
};
В строке 1 объявлен шаблон класса. В строке 2 объявлен метод для настройки аргумента, в качестве которого выступает указатель на метод-член. В строке 3 объявлен метод для настройки экземпляра класса. Метод запуска 4 такой же, как и в исходном, за исключением того, что при вызове в аргумент дополнительно передается указатель на класс (строка 5). В строке 6 инстанциируется аргумент для вызова метода класса, в сигнатуре первым параметром выступает указатель на класс, задаваемый параметром шаблона-инициатора. В строке 7 объявлена переменная для хранения указателя на экземпляр класса.
Итак, модифицировав инициатор из Листинг 53 п. 4.6.2, мы реализовали отдельный инициатор для вызова методов-членов. Используя частичную специализацию шаблона, можно сделать так, чтобы оба инициатора объявлялись одинаковым способом (Листинг 58).
Листинг 58. Использование специализации шаблона-инициатора для вызова методов классаtemplate<typename unused> // (1)
class Initiator
{
// Implementation for origin initiator
};
template<typename ClassName> // (2)
class Initiator<ClassName>
{
// Implementation for class method call initiator
};
В строке 1 объявлен исходный класс, но теперь он является шаблоном с пакетом параметров. Пакет параметров здесь не используется, он нужен только для дальнейшей специализации.
В строке 2 объявлен шаблон для вызова методов-членов. Поскольку его имя совпадает с именем предыдущего, компилятор будет считать, что здесь определяется не новый класс, а специализация объявленного ранее. В объявлении указан параметр, предполагается, что в этом качестве будет использоваться имя класса. Теперь, если при инстанциировании шаблона будет задаваться параметр, будет выбрана специализация для вызова методов-членов. При отсутствии параметров будет выбран исходный шаблон.
Использование двух типов инициатора (исходного и специализированного) для вызова методов класса приведено в Листинг 59, здесь используется преобразование вызовов из Листинг 54 п. 4.6.3.
Листинг 59. Использование инициатора с оболочкой std::function для вызова методов классаclass Executor
{
public:
static void staticCallbackHandler(Executor* executor, int eventID) {}
void callbackHandler(int eventID) {}
void operator() (int eventID) {}
};
int main()
{
Executor executor;
Initiator initiator; // (1)
initiator.setup(CallbackConverter<void(Executor::*)(int)>(&Executor::callbackHandler, &executor)); // (2)
initiator.run();
Initiator<Executor> initiatorForClass; // (3)
initiatorForClass.setup(&Executor::callbackHandler); // (4)
initiatorForClass.setupInstance(&executor); // (5)
initiatorForClass.run();
}
В строке 1 объявлен исходный инициатор. В параметры шаблона мы не передаем никаких аргументов, т. е. шаблон инстанциируется подобно обычному классу. В строке 2 происходит настройка инициатора, в качестве аргумента передается объект для преобразования вызовов.
В строке 3 объявлен специализированный инициатор для вызова методов класса, он инстанциируется типом Executor. В строке 4 настраивается указатель на метод класса, в строке 5 настраивается указатель на экземпляр класса.
Какой инициатор лучше использовать для методов класса, исходный с преобразованием или модифицированный с непосредственным вызовом? Трудно однозначно ответить на этот вопрос. С одной стороны, использование специализированного класса противоречит идее обобщенного кода в специализированном классе мы вынуждены повторять всю реализацию, даже в тех частях, где она совпадает с исходной. С другой стороны, упрощается работа с настройкой инициатора нам не нужно использовать класс для преобразования, можно по отдельности изменять указатель на метод и указатель на экземпляр. В общем, выбор остается на усмотрение разработчика.
4.6.6. Перенаправление вызовов
Представьте следующую ситуацию: инициатор вызывает функцию с одной сигнатурой, а в клиенте реализован обработчик с другой сигнатурой. Например, в исполнителе реализована функция обработки нажатия кнопки, которая на вход принимает два параметра идентификатор кнопки и текущее поле редактирования. В то же время инициатор вызывает функцию, передавая ей только один аргумент идентификатор текущей нажатой кнопки, и он ничего не знает об остальных элементах управления. Можно ли сделать так, чтобы инициатор вызывал одну функцию, но при этом бы вызывалась другая функция, другими словами, происходило перенаправление вызова? В стандартной библиотеке для этого существуют специальные объекты связывания std::bind, которые при необходимости могут сохраняться в std::function подобно обычным функциональным объектам.
Графически использование связывания продемонстрировано на Рис. 18. Пусть инициатор вызывает функцию 1, которая на вход принимает аргумент 1. Исполнитель реализует обратный вызов с помощью функции 2, которая принимает на вход два аргумента. Вместо функции 1 инициатору назначается объект связывания, который имеет перегруженный оператор вызова функции с сигнатурой 1. Указанный объект хранит дополнительный параметр, значение которому присваивается во время инициализации. Перегруженный оператор, в свою очередь, вызывает функцию 2, первому аргументу передает сохраненный параметр, а второму аргументу передает значение аргумента из функции 1. Таким образом, осуществляется перенаправление вызова из функции 1 в функцию 2.
Рис. 18. Перенаправление вызовов
Использование перенаправления вызовов представлено в Листинг 60.
Листинг 60. Перенаправление вызовов#include <functional>
void NativeHandler(int eventID) // (1)
{
//here eventID is 10
}
void AnotherHandler(int contextID, int eventID) // (2)
{
//here eventID is 1, contextID is 10;
}
int main()
{
int eventID = 1; int contextID = 10;
std::function<void(int)> fnt; // (3)
fnt = NativeHandler; // (4)
fnt(eventID); // (5) NativeHandler will be called
fnt = std::bind(AnotherHandler, contextID, std::placeholders::_1); // (6)
fnt(eventID); // (7) AnotherHandler will be called
}
В строке 1 объявлен исходный обработчик, в строке 2 обработчик, в который будет перенаправляться вызов. В строке 3 объявлен универсальный аргумент с исходной сигнатурой. В строке 4 аргументу назначена функция, которая будет вызвана при выполнении вызова 5.
В строке 6 вызывается функция bind, которая из переданных аргументов формирует объект связывания. На вход std::bind передается имя новой функции-обработчика и аргументы, которые будут передаваться в эту функцию. Первому аргументу здесь будет назначено значение contextID, а второму аргументу будет назначено значение 1-го по порядку аргумента из исходной функции. Здесь конструкция std::placeholders определяет номер аргумента в исходной функции, который будет подставлен в качестве аргумента в перенаправляемую функцию.
Сформированный объект связывания сохраняется в универсальном аргументе. Если мы теперь выполним вызов (строка 7), то будет вызвана функция, назначенная этому объекту, и этой функции будут переданы соответствующие аргументы.
Аналогичным образом может быть объявлено перенаправление вызовов для методов-членов класса, но здесь должно соблюдаться следующее правило: первому аргументу новой функции должен быть назначен первый аргумент исходной функции, потому что он определяет экземпляр класса, для которого вызывается метод. Пример приведен в Листинг 61.
Листинг 61. Перенаправление вызовов для методов-членов класса#include <functional>
class CallbackHandler
{
public:
void NativeHandler(int eventID)
{
//eventID = 1;
}
void AnotherHandler(int contextID, int eventID)
{
//eventID = 1, contextID = 10;
}
};
int main()
{
using namespace std::placeholders; // (1)
int eventID = 1; int contextID = 10;
CallbackHandler handler;
std::function<void(CallbackHandler*, int)> fnt;
fnt = &CallbackHandler::NativeHandler;
fnt(&handler, eventID); // NativeHandler will be called
fnt = std::bind(&CallbackHandler::AnotherHandler, _1, contextID, _2); // (2)
fnt(&handler, eventID); // AnotherHandler will be called
}
Здесь в строке 1 мы использовали using namespace, что сокращает объявление позиций аргументов: как видно из строки 2, мы сразу пишем позицию без использования std::placeholders, что значительно компактнее и проще для восприятия. Здесь в исходной функции присутствует неявный параметр с номером 1, который определяет экземпляр класса. Этот параметр назначается первому (неявному) параметру новой функции, а второй параметр исходной функции eventID назначается последнему параметру новой функции.
В общем случае могут быть 4 варианта перенаправления вызовов:
из функции в функцию (пример в Листинг 60);
из функции в метод класса;
из метода класса в другой метод этого же класса (пример в Листинг 61);