Пример использования распределителя приведен в Листинг 76.
Листинг 76. Использование распределителя для статического набораstruct FO
{
void operator() (int eventID) {}
void callbackHandler(int eventID) {}
};
void ExternalHandler(int eventID) {}
int main()
{
FO fo;
int eventID = 0;
auto lambda = [](int eventID) {};
auto callbackToMethod = std::bind(&FO::callbackHandler, fo, std::placeholders::_1);
StaticDistributorVoid distributor(ExternalHandler, fo, callbackToMethod, lambda); // (1)
distributor(eventID); // (2)
}
Как видим, использование очень простое: в строке 1 объявляется распределитель, в конструктор передаются объекты вызова, через перегруженный оператор 2 производятся вызовы сохраненных объектов.
5.5.2. Распределение с возвратом результатов
Если нужно получить значения, возвращаемые вызовами, то в распределителе необходимо модифицировать перегруженный оператор (Листинг 77).
Листинг 77. Распределитель для статического набора с возвратом результатовtemplate<typename CallObjects> // (1)
class StaticDistributorReturn
{
public:
StaticDistributorReturn(CallObjects objects) : callObjects(objects) {} // (2)
auto& tuple() { return callObjects; } // (3)
template<typename CallData> // (4)
auto operator() (CallData callData)
{
return DistributeReturn(callObjects, callData);
}
private:
std::tuple<CallObjects> callObjects; // (5)
};
В строке 4 объявлен перегруженный оператор с возвращаемым типом auto. Указанный тип будет выведен из значения, возвращаемого соответствующей распределяющей функцией. (реализацию см. в Листинг 73 п. 5.4.1).
Пример использования распределителя приведен в Листинг 78.
Листинг 78. Использование распределителя для статического набора с возвратом результатовstruct FO
{
int operator() (int eventID) { return 10; }
int callbackHandler(int eventID) { return 0; }
};
struct SResult
{
unsigned int code;
const char* description;
};
SResult ExternalHandler(int eventID)
{
return SResult{ 1, "this is an error" };
}
int main()
{
FO fo;
int eventID = 0;
auto lambda = [](int eventID) { return 0.0; };
auto callbackToMethod = std::bind(&FO::callbackHandler, fo, std::placeholders::_1);
StaticDistributorReturn distributor(ExternalHandler, fo, callbackToMethod, lambda); // (1)
auto [resExtHandler, resFoOperator, resFoMethod, resLambda] = distributor(eventID); // (2)
}
В строке 1 объявляется распределитель, в конструктор передаются объекты вызова. Через перегруженный оператор 2 производятся вызовы хранимых объектов, результаты возвращаются с помощью структурных привязок.
К сожалению, мы не можем использовать рассмотренную реализацию для объектов, которые не возвращают результатов. Это связано с тем, что результаты выполнения вызовов возвращаются через кортеж, а он не может хранить типы void. Для таких вызовов нужно использовать реализацию, рассмотренную в предыдущем параграфе.
5.5.3. Параметризация возвращаемого значения
Итак, у нас имеется отдельная реализация распределителя для случая, когда результаты вызовов не требуются, и отдельная реализация для случая, когда необходимо получать возвращаемые значения. Обе реализации одинаковы, за исключением перегруженного оператора. Как сделать общую реализацию для обеих случаев? Разместить два перегруженных оператора в одном классе не получится, потому что они различаются только типом возвращаемого значения. Можно предложить следующее решение: ввести в шаблон дополнительный параметр, который указывает, нужно ли возвращать результаты выполнения вызовов, и в зависимости от этого по-разному формировать перегруженный оператор с помощью условной компиляции. Реализация приведена в Листинг 79.
Листинг 79. Условная компиляция в зависимости от типа возвращаемого значенияtemplate<typename CallObjects> // (1)
class StaticDistributor
{
public:
StaticDistributor(CallObjects objects) : callObjects(objects) {} // (2)
auto& tuple() { return callObjects; } // (3)
template<typename CallData>
auto operator() (CallData callData) // (4)
{
{
#define callObject std::get<0>(callObjects) // (5)
#define callObjType decltype(callObject) // (6)
#define callObjInstance std::declval<callObjType>() // (7)
#define testCall callObjInstance(callData) // (8)
#define retType decltype(testCall) // (9)
//if constexpr (std::is_same_v<void, decltype(std::declval<decltype(std::get<0>(callObjects))>()(callData))>) // (10)
if constexpr (std::is_same_v<void, retType>) // (11)
return Distribute2(callObjects, callData); // (12)
else
return DistributeReturn(callObjects, callData); // (13)
}
private:
std::tuple<CallObjects> callObjects;
};
В строках 1 4 код идентичен реализации распределителя в предыдущих случаях (Листинг 75 п. 5.5.1, Листинг 77 п. 5.5.2). Интерес представляет реализация перегруженного оператора (строка 4).
Макросы в строках 5 9 предназначены только для облегчения понимания кода, без них конструкция получается запутанной (строка 10).
В строке 5 мы получаем объект вызова, для которого будет проверяться, возвращает ли он значение. Мы запрашиваем нулевой элемент кортежа, поскольку предполагается, что кортеж содержит хотя-бы один объект (иначе зачем распределять вызовы для пустого кортежа?).
В строке 6 определяется тип объекта, который мы запросили. В строке 7 объявляется мета-экземпляр объекта соответствующего типа. Мы говорим «мета-экземпляр», потому что реально объект не создается, но его характеристики используются компилятором для анализа. Конструкция declval необходима, чтобы не было ошибки в случае, если объект не имеет конструктора по умолчанию.
В строке 8 производится мета-вызов с передачей параметров. Мета-вызов здесь имеет тот же смысл, что и мета-экземпляр, т. е. в реальности вызов не производится, а используется для анализа. В строке 9 определяется тип значения, возвращаемого мета-вызовом.
В строке 11 проверяется, является ли тип возвращаемого значения void, и в этом случае вызывается распределяющая функция без возврата результатов (строка 12). В противном случае вызывается распределяющая функция, возвращающая результаты (строка 13).
Использование распределителя с условной компиляцией приведено в Листинг 80.
Листинг 80. Условная компиляция в зависимости от типа возвращаемого значенияstruct FOReturn
{
int operator() (int eventID) {return 10;}
};
struct FOVoid
{
void operator() (int eventID) { /*do something*/ }
};
struct SResult
{
unsigned int code;
const char* description;
};
SResult ExternalReturn(int eventID)
{
return SResult{ 1, "this is an error" };
}
void ExternalVoid(int eventID)
{
}
int main()
{
int eventID = 0;
FOReturn foRet;
FOVoid foVoid;
auto lambdaRet = [](int eventID) { return 0.0; };
auto lambdaVoid = [](int eventID) {};
using FunPtrRet = SResult(*)(int);
using LambdaTypeRet = decltype(lambdaRet);
using FunPtrVoid = void(*)(int);
using LambdaTypeVoid = decltype(lambdaVoid);
StaticDistributor<FOReturn, FunPtrRet, LambdaTypeRet> distributor1(foRet, ExternalReturn, lambdaRet); // (1)
StaticDistributor<FOVoid, FunPtrVoid, LambdaTypeVoid> distributor2(foVoid, ExternalVoid, lambdaVoid); // (2)
auto results = distributor1(eventID);
distributor2(eventID);
}
Как видим, в обоих случаях объявляется один и тот же распределитель, а из свойств объектов распределения будет генерироваться соответствующий перегруженный оператор.
5.6. Динамический набор получателей
5.6.1. Распределение в динамическом наборе
В предыдущих параграфах мы рассматривали статический набор получателей, когда типы и количество получателей определены на этапе компиляции и остаются неизменными. Теперь рассмотрим динамический набор, когда типы и количество получателей заранее неизвестны и изменяются в процессе выполнения программы. В какой-то степени реализация здесь получается проще: у нас не будет специализаций, рекурсий, выведения типов и прочей так называемой «шаблонной магии», все решается обычными методами классического программирования.
Итак, поскольку количество объектов заранее не определено, для их хранения необходим динамический контейнер. Однако он не может хранить объекты непосредственно, поскольку они могут иметь разные типы, а динамический контейнер работает с данными одного строго определенного типа. Выходом будет хранить универсальные аргументы, а уже в них сохранять объекты вызова. Структурная схема изображена на Рис. 24.