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


Итак, на каждом шаге рекурсивного вызова из пакета извлекается очередной параметр, а размер исходного пакета уменьшается. Таким образом, в итоге все параметры будут извлечены, и пакет станет пустым. Эта ситуация обрабатывается путем объявления функции с пустым пакетом параметров, т. е. функции, которая на вход не принимает ни одного аргумента (строка 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)

Назад Дальше