Обратные вызовы в C++ - читать онлайн бесплатно, автор Виталий Евгеньевич Ткаченко, ЛитПортал
bannerbanner
Полная версияОбратные вызовы в C++
Добавить В библиотеку
Оценить:

Рейтинг: 4

Поделиться
Купить и скачать
На страницу:
9 из 13
Настройки чтения
Размер шрифта
Высота строк
Поля
Листинг 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               // (1)

auto  DistributeReturnImpl(std::tuple& callObjects, std::index_sequence, CallData… callData)  // (2)

{

  return std::tuple(std::get(callObjects)(callData…)…);                         // (3)

}


template                               // (4)

auto DistributeReturn(std::tuple callObjects, CallData… callData)  // (5)

{

  return DistributeReturnImpl(                                                     // (6)

    callObjects,                                                                   // (7)

    std::make_index_sequence (),                           // (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  // (1)

class StaticDistributorVoid

{

public:

  StaticDistributorVoid (CallObjects… objects) : callObjects(objects…) {}  // (2)

  auto& tuple() {  return callObjects; }  // (3)


  template          // (4)

  void operator() (CallData… callData)

  {

    Distribute2(callObjects, callData…);

  }

private:

  std::tuple 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  // (1)

class StaticDistributorReturn

{

public:

  StaticDistributorReturn(CallObjects… objects) : callObjects(objects…) {}  // (2)

  auto& tuple() { return callObjects; }  // (3)


  template         // (4)

  auto operator() (CallData… callData)

  {

      return DistributeReturn(callObjects, callData…);

  }

private:

  std::tuple 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  // (1)

class StaticDistributor

{

public:

  StaticDistributor(CallObjects… objects) : callObjects(objects…) {}  // (2)

  auto& tuple() { return callObjects; }  // (3)


  template 

  auto operator() (CallData… callData)  // (4)

  {


#define callObject std::get<0>(callObjects)          // (5)

#define callObjType decltype(callObject)             // (6)

#define callObjInstance std::declval()  // (7)

#define testCall callObjInstance(callData…)        // (8)

#define retType decltype(testCall)                   // (9)


  //if constexpr (std::is_same_v(callObjects))>()(callData…))>)  // (10)

  if constexpr (std::is_same_v)         // (11)

    return Distribute2(callObjects, callData…);      // (12)

  else                              

    return DistributeReturn(callObjects, callData…);  // (13)

  }

private:

  std::tuple 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  distributor1(foRet, ExternalReturn, lambdaRet);  // (1)

  StaticDistributor  distributor2(foVoid, ExternalVoid, lambdaVoid);  // (2)


  auto results = distributor1(eventID);

  distributor2(eventID);

}


Как видим, в обоих случаях объявляется один и тот же распределитель, а из свойств объектов распределения будет генерироваться соответствующий перегруженный оператор.

5.6. Динамический набор получателей

5.6.1. Распределение в динамическом наборе

В предыдущих параграфах мы рассматривали статический набор получателей, когда типы и количество получателей определены на этапе компиляции и остаются неизменными. Теперь рассмотрим динамический набор, когда типы и количество получателей заранее неизвестны и изменяются в процессе выполнения программы. В какой-то степени реализация здесь получается проще: у нас не будет специализаций, рекурсий, выведения типов и прочей так называемой «шаблонной магии», все решается обычными методами классического программирования.

Итак, поскольку количество объектов заранее не определено, для их хранения необходим динамический контейнер. Однако он не может хранить объекты непосредственно, поскольку они могут иметь разные типы, а динамический контейнер работает с данными одного строго определенного типа. Выходом будет хранить универсальные аргументы, а уже в них сохранять объекты вызова. Структурная схема изображена на Рис. 24.


Рис. 24. Структурная схема распределителя для динамического набора получателей


Оптимальным решением будет реализация распределителя в виде класса, который, кроме выполнения распределения, будет поддерживать операции с контейнером. Конечно же, проектировать динамический контейнер и универсальный аргумент не нужно – в STL имеется все необходимое. Контейнер, в общем-то, можно использовать любой, а на роль универсального аргумента нет ничего лучше, чем std::function. Реализация приведена в Листинг 81.

Листинг 81. Распределитель для динамического набора получателей

template class DynamicDistributor;  // (1)


template  // (2)

class DynamicDistributor

{

public:

  template 

  void addCallObject(CallObject object)        // (3)

  {

    callObjects.push_back(object);

  }


  void operator ()(ArgumentList… arguments)  // (4)

  {

    for (auto& callObject : callObjects)

    {

      callObject(arguments…);

    }

  }

private:

  std::list< std::function > callObjects;  // (5)

};


В строке 1 объявлена общая специализация шаблона. Реализация класса здесь отсутствует, поскольку для каждой сигнатуры она будет различной. В строке 2 объявлен шаблон для частичной специализации, в котором два аргумента: тип возвращаемого значения и пакет параметров, передаваемых на вход вызова. Подобную конструкцию мы использовали, когда рассматривали настройку сигнатуры для универсального аргумента (п. 4.5.2).

В строке 3 объявлен метод, который добавляет объект вызова в контейнер, сам контейнер объявлен в строке 5. Тип контейнера мы выбираем список, поскольку он не перемещает элементов при вставке/удалении, а произвольный доступ здесь не требуется. Типом хранимых данных в контейнере является объект std::function, аргументы которого задаются исходя из параметров в объявлении шаблона класса.

В строке 4 объявлен перегруженный оператор, который осуществляет распределение вызовов, т. е. является распределяющей функцией. Он обходит элементы контейнера и осуществляет вызов в соответствии с списком аргументов, типы которых задаются в пакете параметров шаблона.

5.6.2. Получение возвращаемых значений

Как получить возвращаемые значения для динамического набора? На момент вызова распределяющей функции количество получателей может быть любым, и, соответственно, число возвращаемых значений заранее не определено. Использовать динамический контейнер как возвращаемое значение функции является плохой идеей: во-первых, заполнение контейнера и создание его копии в стеке требует значительного расхода времени и увеличивает фрагментацию памяти; во-вторых, если возвращаемое значение не используется, то все вышеописанное будет работать «вхолостую», выполняя совершенно ненужные операции. Использовать контейнер как входной параметр – это тоже идея не очень: мы вынуждаем привязаться к контейнеру определенного типа, а если нам результаты нужно хранить в других структурах? А если нам вообще их не нужно хранить, а нужно всего лишь проверить? Вопросы, вопросы… Можно предложить следующее решение: для возврата результата использовать обратный вызов, а пользователь сам решает, что делать с возвращаемыми значениями. Реализация приведена в Листинг 82.

Листинг 82. Возврат значений для динамического набора получателей

template 

class DynamicDistributor

{

  /**********************……**********************************/


  template  // (1)

  void operator()(CallbackReturn callbackReturn, ArgumentList… arguments)

  {

    for (auto& callObject : callObjects)

    {

        callbackReturn(callObject(arguments…));  // (2)

    }

  }

private:

  std::list< std::function > callObjects;

};


Реализация совпадает с Листинг 82 п. 5.6.1, только добавляется еще один перегруженный оператор. Его шаблон объявлен строке 1, параметром шаблона является тип аргумента, через который будет выполняться обратный вызов. В строке 2 происходит вызов объекта, результат возвращается через аргумент, переданный как входной параметр функции.


Пример распределения вызовов для динамического набора получателей приведен в Листинг 83.

Листинг 83. Распределение вызовов для динамического набора получателей

struct FO

{

  int operator() (int eventID) { return 10; }

  int callbackHandler(int eventID) { return 100; }

};


int ExternalHandler(int eventID)

{

  return 0;

}


int main()

{

  int eventID = 0;


  FO fo;

  auto lambda = [](int eventID) { return 0; };

  auto binding = std::bind(&FO::callbackHandler, fo, std::placeholders::_1);


  DynamicDistributor distributor;       // (1)


  distributor.addCallObject(fo);                  // (2)

  distributor.addCallObject(ExternalHandler);     // (3)

  distributor.addCallObject(binding);             // (4)

На страницу:
9 из 13