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


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


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

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

template<typename unused> class DynamicDistributor;  // (1)


template<typename Return, typename ArgumentList>  // (2)

class DynamicDistributor<Return(ArgumentList)>

{

public:

  template <typename CallObject>

  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<Return(ArgumentList )> > callObjects;  // (5)

};


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

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

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

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

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

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

template <typename Return, typename ArgumentList>

class DynamicDistributor<Return(ArgumentList)>

{

  /********************************************************/


  template<typename CallbackReturn >  // (1)

  void operator()(CallbackReturn callbackReturn, ArgumentList arguments)

  {

    for (auto& callObject : callObjects)

    {

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

    }

  }

private:

  std::list< std::function<Return(ArgumentList )> > callObjects;

};


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


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

Пример распределения вызовов для динамического набора получателей приведен в Листинг 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<int(int)> distributor;       // (1)


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

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

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

  distributor.addCallObject(lambda);              // (5)


  distributor(eventID);                           // (6)


  auto onReturnValue = [](int callResult) {};     // (7)

  distributor(onReturnValue, eventID);            // (8)

}


В строке 1 инстанциирован класс распределителя с заданной сигнатурой функции. В строке 2, 3, 4, 5 в распределитель добавляются объекты вызова различного типа. В строке 6 запускается распределение вызовов, в результате которого будут вызваны добавленные объекты. В строке 7 объявлено лямбда-выражение для получения результатов, при вызове соответствующего оператора 8 это выражение будет вызвано для каждого возвращаемого значения.


Касательно модификации содержимого контейнера наш распределитель поддерживает только одну операцию добавление получателя. Ни удаление, ни модификация получателей не поддерживается. Это связано с тем, что получатели не идентифицированы, и поэтому невозможно узнать, в каком элементе контейнера хранится соответствующий объект вызова33. Далее мы рассмотрим, как можно решить указанную проблему.

5.7. Адресное распределение

5.7.1. Понятие адресного распределения

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

Как это реализовать? Прежде всего, необходимо как-то идентифицировать получателей, для чего вводится понятие адреса. Каждому получателю присваивается адрес, и с каждым адресом связывается универсальный аргумент, который хранит объект вызова. Таким образом, зная адреса получателей, можно осуществлять вызовы только для конкретных объектов. Попутно решается задача изменения списка получателей: по заданному адресу возможно удаление/изменение соответствующего аргумента.

Что может быть адресом? Все что угодно: числа, строки, структуры и т. п. Единственное требование, предъявляемое к адресу, заключается в том, что он должен быть уникальным, в противном случае невозможно однозначно идентифицировать получателя. Мы сделаем тип адреса параметром шаблона, а пользователь сам решит, что использовать в качестве адреса.

Теперь в функцию распределителя, помимо данных, будет передаваться адрес. Источник должен найти аргумент, которому соответствует полученный адрес, и выполнить для него вызов. Для поиска необходимо сравнивать адреса, но ведь мы не знаем их типы: теперь это параметр шаблона, и тип используемого адреса станет известен только после инстанциирования. По этой причине мы не можем производить сравнение адресов напрямую, для этого необходимо использовать предикаты (см. п. 4.3.3).

Какой выбрать контейнер? На эту роль лучше других подойдет std::map. Во-первых, не нужно вводить новую структуру для хранения адреса и аргумента, контейнер реализует ее естественным образом в виде пары «ключ-значение». И, во-вторых, std::map осуществляет быстрый поиск по ключу, в качестве которого выступает адрес. Структурная схема изображена на Рис. 25.


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

5.7.2. Адресный распределитель

Реализация адресного распределителя приведена в Листинг 84.

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

template <typename Address, typename AddressCompare, typename Function> class AddressDistributor;  // (1)


template <typename Address, typename AddressCompare, typename Return, typename ArgumentList>    // (2)

class AddressDistributor<Address, AddressCompare, Return(ArgumentList)>                         // (3)

Назад Дальше