template<std::size_t Index, typename CallObjects, typename CallData> // (1)
struct TupleIterator
{
static void IterateTupleItem(CallObjects& callObjects, CallDatacallData) // (2)
{
const std::size_t idx = std::tuple_size_v<CallObjects> Index; // (3)
std::get<idx>(callObjects)(callData); // (4)
TupleIterator<Index 1, CallObjects, CallData>::IterateTupleItem(callObjects, callData); // (5)
}
};
template<typename CallObjects, typename CallData> // (6)
struct TupleIterator<0, CallObjects, CallData> // (7)
{
static void IterateTupleItem(CallObjects& callObjects, CallData callData) // (8)
{
}
};
template<typename CallObjects, typename CallData> // (9)
void Distribute2(std::tuple<CallObjects> callObjects, CallData callData) // (10)
{
TupleIterator // (11)
<
sizeof(CallObjects), // (12)
std::tuple<CallObjects>, // (13)
CallData // (14)
>
::IterateTupleItem(callObjects, callData); // (15)
}
В строке 1 объявляется шаблон структуры. Параметрами шаблона выступают индекс элемента кортежа, сам кортеж и пакет параметров, который определяет данные, передаваемые в вызываемый объект.
Внутри структуры в строке 2 объявлена функция, осуществляющая выполнение вызова для элемента кортежа. Входными параметрами этой функции будет кортеж объектов вызова и пакет данных вызова, элемент кортежа определяется индексом параметром шаблона. Функция объявлена статической, чтобы не объявлять экземпляр структуры в процессе вызова. По сути дела, структура здесь не несет функциональной нагрузки, она выступает в качестве оболочки, чтобы обеспечить специализацию функции по индексу (поскольку непосредственная специализация шаблонов функций невозможна).
В строке 3 осуществляется пересчет индекса: от размера (количества элементов) кортежа отнимается текущий индекс. Это необходимо для того, чтобы обход кортежа осуществлялся в прямом порядке, от первого элемента к последнему. Если не выполнять пересчет индексов, то обход будет происходить в обратном порядке.
В строке 4 осуществляется вызов объекта. С помощью вызова get по пересчитанному индексу осуществляется доступ к соответствующему элементу кортежа. Для указанного элемента выполняется вызов, на вход ему передается пакет данных callData, распакованный в список аргументов.
В строке 5 происходит рекурсивный вызов. Объявляется структура с новым значением параметра-индекса, уменьшенным на единицу. Вызывается соответствующая функция с передачей кортежа объектов и пакета параметров, и процесс повторяется заново.
С каждой итерацией значение индекса уменьшается, и когда оно станет равным нулю, необходимо остановить итерации, поскольку все элементы кортежа будут посещены. Для этой цели в строке 6 объявлена специализация структуры для нулевого индекса. Тело функции в этой структуре пустое, таким образом, рекурсия будет завершена.
В строке 9 объявлен шаблон распределяющей функции. Этот шаблон имеет два пакета параметров: пакет объектов вызова и пакет данных вызова, типы содержимого пакетов будут выводиться из входных аргументов. В строке 10 объявляется сама функция, которая на вход принимает два аргумента: кортеж объектов вызова и пакет данных вызова.
В строке 11 запускается процесс итерации путем инстанциирования шаблона TupleIterator. Аргументами шаблона выступают: количество объектов вызова (строка 12), вычисляется с помощью операции sizeof применительно к соответствующему пакету параметров; кортеж объектов вызова (строка 13); данные, передаваемые в вызов (строка 14). В строке 15 вызывается стартовая функция итерации с передачей соответствующих аргументов. Как видим, начальное значение индекса равно количеству объектов вызова, которое затем с каждой новой итерацией будет уменьшаться на единицу, в то время как пересчитываемый индекс, соответственно, увеличивается.
5.3.4. Способ 3: объекты и данные в кортежах
При использовании данного способа реализация практически повторяет рассмотренную в предыдущем параграфе, только вместо пакета данных будет использоваться кортеж (Листинг 70).
Листинг 70. Распределение при упаковке объектов и данных в кортежиtemplate<std::size_t Index, typename CallObjects, typename CallData> // (1)
struct TupleIterator3
{
static void IterateTupleItem(CallObjects& callObjects, CallData& callData) // (2)
struct TupleIterator3
{
static void IterateTupleItem(CallObjects& callObjects, CallData& callData) // (2)
{
const std::size_t idx = std::tuple_size_v<CallObjects> Index; // (3)
std::apply(std::get<idx>(callObjects), callData); // (4)
TupleIterator3<Index 1, CallObjects, CallData>::IterateTupleItem(callObjects, callData); // (5)
}
};
template<typename CallObjects, typename CallData> // (6)
struct TupleIterator3<0, CallObjects, CallData> // (7)
{
static void IterateTupleItem(CallObjects& callObjects, CallData& callData) // (8)
{
}
};
template<typename CallObjects, typename CallData> // (9)
void Distribute3(std::tuple<CallObjects> callObjects, std::tuple<CallData> callData) // (10)
{
TupleIterator3 // (11)
<
sizeof(CallObjects), // (12)
std::tuple<CallObjects>, // (13)
std::tuple<CallData> // (14)
>
::IterateTupleItem(callObjects, callData); // (15)
}
По сравнению с Листинг 69 п. 5.3.3 изменения здесь следующие. Входными параметрами распределяющей функции (строка 10) являются кортеж объектов и кортеж данных (ранее параметр для данных задавался пакетом). В объявлениях шаблонов структур для обхода кортежа (строки 1, 6) параметр, определяющий данные вызова, объявляется как тип (ранее это был пакет). Вызов объекта (строка 4) осуществляется через std::apply (ранее объект вызывался непосредственно). И еще здесь изменены имена структур, чтобы избежать конфликта имен с предыдущей реализацией.
5.3.5. Сравнение способов
В Листинг 71 приведен пример распределения вызовов с использованием различных способов настройки сигнатуры, в качестве данных выступают два числовых значения.
Листинг 71. Распределение вызовов с заданной сигнатуройvoid ExternalHandler(int eventID, int contextID) {}
struct FO
{
void callbackHandler(int eventID, int contextID) {}
void operator() (int eventID, int contextID) {}
};
int main()
{
int eventID = 0, contextID = 1;
FO fo;
auto lambda = [](int eventID, int contextID) {};
auto cb2cl = std::bind(&FO::callbackHandler, fo, _1, _2);
Distribute1(std::tuple(eventID, contextID), ExternalHandler, fo, cb2cl, lambda);
Distribute2(std::tuple(ExternalHandler, fo, cb2cl, lambda), eventID, contextID);
Distribute3(std::tuple(ExternalHandler, fo, cb2cl, lambda), std::tuple(eventID, contextID));
}
С точки зрения эффективности все три способа, в общем-то, равноценны. С точки зрения дизайна можно сказать следующее: первый способ самый простой в реализации; второй способ позволяет легко модифицировать код для сбора дополнительной информации при выполнении вызовов; третий способ позволяет передавать дополнительные параметры в функцию распределения, если это необходимо.
5.3.6. Настройка сигнатуры для перенаправления
В рассмотренных выше примерах мы предполагали, что все получатели используют одну и ту же сигнатуру вызова. Но что делать, если они имеют разные сигнатуры? Нам необходимо разработать какой-то объект, который бы обеспечивал следующее: настройку входной сигнатуры, в которую передаются данные вызова; настройку выходной сигнатуры, которая определяется получателем; преобразование одной сигнатуры в другую. По сути дела, необходимо обеспечить перенаправление вызовов, что решается с помощью инструментов STL, а именно объектов связывания (см. п. 4.6.2). В этом случае в функцию распределителя вместо объекта-получателя передается объект-связывание, который осуществляет перенаправление вызова с заданной сигнатурой. Пример реализации приведен в Листинг 72; здесь в качестве распределяющей функции используется реализация из Листинг 69 п. 5.3.3.
Листинг 72. Перенаправление вызовов с настройкой сигнатурыvoid NativeHandler(int eventID)
{
}
void ExternalHandler(int eventID, int contextID)
{
}
struct FO
{
void operator() (int eventID, int contextID) {}
void callbackHandler(int eventID, int contextID) {}
};
int main()
{
int eventID = 0, contextID = 0;
FO fo;
auto lambda = [](int eventID, int contextID) {};
Distribute2(std::tuple( // (1)
NativeHandler, // (2)
std::bind(ExternalHandler, std::placeholders::_1, contextID), // (3)
std::bind(&FO:: callbackHandler, fo, std::placeholders::_1, contextID), // (4)
std::bind(&FO::operator(), fo, std::placeholders::_1, contextID), // (5)