Пример использования распределителя приведен в Листинг 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.
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 определяется тип значения, возвращаемого мета-вызовом.