Обратные вызовы в C++ - Виталий Евгеньевич Ткаченко 23 стр.


    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, typenameCallData>              // (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, typenameCallData>                               // (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 производятся вызовы сохраненных объектов.

Назад Дальше