Итак, на каждом шаге рекурсивного вызова из пакета извлекается очередной параметр, а размер исходного пакета уменьшается. Таким образом, в итоге все параметры будут извлечены, и пакет станет пустым. Эта ситуация обрабатывается путем объявления функции с пустым пакетом параметров, т. е. функции, которая на вход не принимает ни одного аргумента (строка 1). Тело этой функции пустое, в ней происходит возврат управления, и по цепочке рекурсивных вызовов управление возвращается в исходную точку в строке 6.
Рис. 22. Рекурсивное развертывание пакета параметров для трех аргументов
Использование распределения вызовов для статического набора получателей приведено в Листинг 64.
Листинг 64. Распределение вызова для статического набораvoid ExternalHandler() // (1)
{
}
struct FO
{
void callbackHandler() {}
void operator() () {}
};
int main()
{
FO fo; // (2)
auto lambda = []() {}; // (3)
auto cb2cl = std::bind(&FO::callbackHandler, fo); // (4)
Distribute(ExternalHandler, fo, cb2cl, lambda); // (5)
}
В строках 1, 2, 3, 4 объявлены соответствующие объекты вызова: внешняя функция, функциональный объект, лямбда-выражение, объект для вызова метода класса. Для вызова метода класса в строке 4 объявляется объект связывания (см. п. 4.6.6), в строке 5 происходит распределение вызовов.
5.2.2. Передача данных
Если в вызов необходимо передавать данные, то для этого в описанные выше функции необходимо ввести дополнительный параметр (Листинг 65).
Листинг 65. Распределяющая функция для статического набора получателей с передачей данныхtemplate <typename CallData> // (1)
void Call(CallData& data)
{
}
template <typename CallData, typename First, typenameOthers> // (2)
void Call(CallData data, First& first, Others&rest)
{
first(data); // (3)
Call(data, rest); // (4)
}
template <typename CallData, typename CallObjects> // (5)
void Distribute(CallData data, CallObjects objects)
{
Call(data, objects); // (6)
}
Приведенная реализация повторяет Листинг 63 п. 5.2.1, только теперь в функциях к объектам вызова добавляется параметр data для передачи данных.
Пример распределения для статического набора получателей с передачей данных представлен в Листинг 66.
Листинг 66. Распределение вызовов для статического набора получателейvoid ExternalHandler(int eventID) // (1)
{
}
struct FO
{
void callbackHandler(int eventID) {}
void operator() (int eventID) {}
};
int main()
{
using namespace std::placeholders;
FO fo; // (2)
auto lambda = [](int eventID) {}; // (3)
auto cb2cl = std::bind(&FO::callbackHandler, fo, _1); // (4)
int eventID = 0; // (5)
Distribute(eventID, ExternalHandler, fo, cb2cl, lambda); // (6)
}
В строках 1, 2, 3, 4 объявлены соответствующие объекты вызова: внешняя функция, функциональный объект, лямбда-выражение, объект для вызова метода класса. Для вызова метода класса в строке 4 объявляется объект связывания (см. п. 4.6.6), в строке 5 объявляется переменная для передачи данных. В строке 6 происходит распределение вызовов, первым параметром передается аргумент данных eventID.
5.3. Настройка сигнатуры для передачи данных
5.3.1. Общая концепция
В рассмотренной выше реализации распределения с передачей данных (п. 5.2.2) есть один недостаток: данные, передаваемые в вызов, имеют заранее прописанную сигнатуру. В нашем случае предполагается, что это единственная числовая переменная. Если нам понадобится другая сигнатура, т. е. другой набор и типы переменных, нам придется повторять всю реализацию распределения, изменяя только сам вызов. Можно ли настроить сигнатуру, как это мы делали в универсальном аргументе? Тогда мы определяли сигнатуру с помощью пакета параметров, но теперь у нас пакет параметров используется для задания объектов вызова.
Получается, нам необходим еще один пакет параметров. В общем случае допускается объявлять шаблон функции с несколькими пакетами30, однако в этом случае для вывода типов пакета используется схема раскрытия. По этой причине необходимо, чтобы все пакеты параметров раскрывались параллельно в рамках одной синтаксической конструкции (Листинг 67), что для нашей задачи не подходит: мы должны вначале раскрыть пакет объектов вызова, а затем для каждого элемента пакета раскрыть пакет сигнатуры. Здесь нужно какое-то другое решение.
template<typenameFirst, typenameSecond>
void init(std::pair<First,Second>)
{
}
int main()
{
init(std::make_pair(1, 2), std::make_pair(3,4), std::make_pair(0.3, 1e5));
}
Поскольку пакет параметров в нашем случае может быть только один, необходима структура данных, в которую можно упаковать объекты различных типов. На эту роль лучше всего подойдет кортеж.
Кортеж это структура данных, которая используется для хранения объектов различных типов.
В STL кортеж реализуется шаблонным классом std::tuple, параметрами шаблона являются типы, которые будут храниться в кортеже. Этот класс как нельзя лучше подойдет для наших целей, потому что объекты вызова у нас также задаются параметрами шаблона.
Итак, у нас есть два набора: объекты вызова и данные, передаваемые в вызов. Какой набор упаковать в кортеж, а какой в пакет параметров? Рассмотрим различные способы упаковки наборов.
5.3.2. Способ 1: объекты в пакет, данные в кортеж
При использовании данного способа реализация распределения практически совпадает с описанной в Листинг 65 п. 5.2.2 с той разницей, что для передачи данных используется не переменная, а кортеж (Листинг 68).
Листинг 68. Распределение при упаковке объектов в пакет и данных в кортежtemplate <typename CallData>
void Call(CallData& data) // (1)
{
}
template <typename CallData, typename First, typenameOthers>
void Call(CallData& data, First& first, Others&rest) // (2)
{
std::apply(first, data); // (3)
Call(data, rest); // (4)
}
template <typename CallData, typename CallObjects>
void Distribute1(std::tuple<CallData> data, CallObjects objects) // (5)
{
Call(data, objects); // (6)
}
Распределяющая функция объявлена в строке 5. Входными параметрами функции являются кортеж данных вызова data и пакет объектов вызова objects, типы их содержимого задаются параметрами шаблона. Внутри этой функции, в строке 6, происходит первый вызов рекурсивной функции, которой передаются соответствующие аргументы кортеж и пакет.
Рекурсивная функция объявлена в строке 2. Эта функция извлекает очередной объект из пакета и осуществляет его вызов (строка 3). Здесь используется функция стандартной библиотеки std::apply, которая преобразует содержимое кортежа в список аргументов. Далее, в строке 4, пакет с оставшимися параметрами передается в рекурсивный вызов Call, и процесс повторяется до завершения рекурсии.
5.3.3. Способ 2: объекты в кортеж, данные в пакет
При использовании данного способа необходимо пройти по всем элементам кортежа и осуществить вызовы хранимых в нем объектов, передавая на вход пакет данных. Как осуществить обход содержимого кортежа?
Доступ к элементам кортежа осуществляется с помощью вызова
std::get<index>(tuple),
где index это порядковый номер элемента (начиная с 0), tuple имя переменной-кортежа. Проблема в том, что индексы должны быть заранее определены как числовые константы, использование переменной для задания индекса не допускается31. Поэтому здесь нельзя использовать ни циклы, ни функции с входным аргументом индексом.
Можно попробовать объявить шаблон функции, в которой индекс задается параметром шаблона, а внутри функции изменить индекс и осуществить рекурсивный вызов. По идее, в этом случае для каждого индекса должна была бы сгенерироваться отдельная специализированная функция, однако стандарт не допускает специализацию шаблонов функций32. Но специализация шаблонов классов допустима, поэтому выходом будет обернуть функцию в класс оболочку и уже для класса объявлять специализацию по индексам. Реализация приведена в Листинг 69.
Листинг 69. Распределение при упаковке объектов в кортеж и данных в пакет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)