Kitabı oku: «Обратные вызовы в C++», sayfa 9
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)
{
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)
std::bind(lambda, std::placeholders::_1, contextID) // (6)
),
eventID // (7)
);
}
Входными аргументами распределяющей функции служат кортеж объектов вызова (объявлен в строке 1) и данные вызова (строка 7). В строке 2 в кортеж передается объект вызова с сигнатурой, совпадающей с исходной. В строке 3 передается объект связывания (результат вызова std::bind), в котором исходный вызов перенаправляется в назначенную функцию ExternalHandler. В строке 4 объект связывания перенаправляет вызов в метод-член структуры, в строке 5 – в перегруженный оператор, в строке 6 – в лямбда-выражение.
5.4. Возврат результатов выполнения
5.4.1. Получение возвращаемых значений
До сих пор мы считали, что функции, реализующие код вызова, не возвращают результатов. Однако в некоторых случаях необходимо получить результаты выполнения вызовов. Очевидно, что в этом случае их должна вернуть распределяющая функция. Как же сформировать возвращаемые значение?
Поскольку возвращаемые значения могут иметь различные типы, напрашивается сохранять их в кортеже, который затем будет возвращаться как результат работы распределяющей функции. Но мы же не знаем заранее типы возвращаемых значений, их определяют объекты вызова. Какие тогда типы задавать при инстанциировании переменной-кортежа? Можно предложить следующее решение: при объявлении кортежа не указывать явно хранимые в нем типы, а в конструктор в качестве входных аргументов передать результаты выполнения вызовов. В этом случае типы элементов кортежа будут выведены автоматически.
Но сформировать набор результатов выполнения не так-то просто. Мы не можем перечислить в списке аргументов запрос объекта по индексу и его вызов, ведь количество объектов заранее не известно. Поэтому предварительно необходимо сформировать последовательность индексов, которая разворачивается в контексте запроса и вызова объекта. Реализация приведена в Листинг 73.
Листинг 73. Распределение вызовов с возвратом результатов
template <typename… CallObjects, std::size_t… indices, typename…CallData> // (1)
auto DistributeReturnImpl(std::tuple<CallObjects…>& callObjects, std::index_sequence<indices…>, CallData… callData) // (2)
{
return std::tuple(std::get<indices>(callObjects)(callData…)…); // (3)
}
template<typename… CallObjects, typename…CallData> // (4)
auto DistributeReturn(std::tuple<CallObjects…> callObjects, CallData… callData) // (5)
{
return DistributeReturnImpl( // (6)
callObjects, // (7)
std::make_index_sequence<sizeof…(CallObjects)> (), // (8)
callData…); // (9)
}
Шаблон распределяющей функции объявлен в строке 4, параметрами шаблона являются пакет объектов вызова и пакет данных вызова. Сама функция объявлена в строке 5, входными параметрами являются кортеж вызываемых объектов, параметризованный пакетом объектов, и пакет данных вызова. Возвращаемое значение функции объявлено как auto, что означает, что оно будет выводиться из возвращаемого значения.
Для использования рассматриваемого распределения появляется требование, чтобы все объекты вызова возвращали результаты. Это связано с тем, что кортеж не может хранить типы void. Для вызовов, которые не возвращают результат, можно использовать любой из способов, описанный в главе 5.3.
В строке 6 вызывается вспомогательная функция, которой передается кортеж объектов вызова 7, последовательность индексов 8, данные вызова 9. Последовательность индексов формируется с помощью конструкции std::make_index_sequence, которой на вход в качестве значения передается размер пакета вызываемых объектов (определяется с помощью sizeof…).
В строке 1 объявлен шаблон вспомогательной функции, параметрами шаблона выступают пакет объектов вызова CallObjects, пакет индексов Indices и пакет данных вызова CallData. Сама функция объявлена в строке 2, ее входными параметрами являются: кортеж вызываемых объектов, параметризованный пакетом объектов вызова; последовательность индексов, параметризованная пакетом индексов; пакет данных вызова. Данная функция возвращает кортеж, сформированный по результатам вызова. Для получения элемента кортежа используется вызов std::get, на вход которому передается индекс элемента, и затем происходит вызов полученного элемента, на вход которому передаются данные callData. А поскольку вместо конкретного индекса мы используем последовательность индексов, она будет развернута в набор вызовов get с соответствующими индексами, таким образом, осуществляя вызовы для все элементов кортежа в соответствии с их индексами. Графически рассмотренная операция для трех объектов изображена на Рис. 23.
Рис. 23. Формирование кортежа возвращаемых значений
5.4.2. Анализ результатов
Итак, мы получили возвращаемые значения в виде кортежа. Как нам проанализировать полученные результаты? Существуют следующие способы анализа содержимого кортежа:
• доступ к элементам кортежа по индексу с помощью std::get;
• обход кортежа;
• использование структурных привязок.
Пример анализа значений, возвращаемых распределением вызовов, приведен в Листинг 74.
Листинг 74. Анализ возвращаемых значений
struct FO
{
int operator() (int eventID)
{
return 10;
}
};
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 results = DistributeReturn( std::tuple(fo, ExternalHandler, lambda), eventID); // (1)
int foRes = std::get<0>(results); // (2)
SResult ExtRes = std::get<1>(results); // (3)
double lambdaRes = std::get<2>(results); // (4)
auto [foRes1, ExtRes1, lambdaRes1] = results; // (5)
auto [foRes2, ExtRes2, lambdaRes2] = DistributeReturn(std::tuple(fo, ExternalHandler, lambda), eventID); // (6)
}
После выполнения распределения в строке 1 в переменную results помещен кортеж с результатами выполнения вызова. В строках 2, 3, 4 показано получение результатов с помощью запроса элементов кортежа по индексу, в строке 5 показано использование структурных привязок. В строке 6 показано, как можно использовать структурные привязки без промежуточной переменной results. Обход кортежа здесь не рассматривается, поскольку он был подробно описан в п. 5.3.3.
5.5. Распределитель для статического набора
5.5.1. Распределение без возврата результатов
До сих пор мы выполняли распределение с помощью функции, что вызывает определенные неудобства. Во-первых, вызов распределяющей функции получается громоздким, потому что приходится перечислять все объекты, участвующие в распределении. Во-вторых, требуются дополнительные операции, потому что в зависимости от способа настройки либо объекты вызова, либо аргументы сигнатуры необходимо упаковать в кортеж. Хорошим решением было бы предварительно сохранить нужные объекты, для чего нам понадобится распределитель в виде класса. Реализация приведена в Листинг 75.
Листинг 75. Распределитель для статического набора получателей
template<typename… CallObjects> // (1)
class StaticDistributorVoid
{
public:
StaticDistributorVoid (CallObjects… objects) : callObjects(objects…) {} // (2)
auto& tuple() { return callObjects; } // (3)
template<typename… CallData> // (4)
void operator() (CallData… callData)
{
Distribute2(callObjects, callData…);
}
private:
std::tuple<CallObjects…> callObjects; // (5)
};
В строке 1 объявлен шаблон класса, параметром которого выступает пакет объектов вызова. Кортеж для хранения объектов объявлен в строке 5, он инициализируется в конструкторе 2. Для доступа к кортежу реализован метод 3, который позволяет, если необходимо, изменить его содержимое.
В строке 4 объявлен перегруженный оператор, который осуществляет распределение. Этот оператор вызывает распределяющую функцию (реализацию см. Листинг 69 п. 5.3.3), которую при желании можно сделать членом класса.
Пример использования распределителя приведен в Листинг 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);
}
Как видим, в обоих случаях объявляется один и тот же распределитель, а из свойств объектов распределения будет генерироваться соответствующий перегруженный оператор.