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


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)

{

public:

  template<typename CallObject>  // (4)

  void addReceiver(Address address, CallObject object)

  {

    callObjects.insert({ address,object } );

  }


  void deleteReceiver(Address address)  // (5)

  {

    callObjects.erase(address);

  }


  Return operator()(Address address, ArgumentList arguments)  // (6)

  {

    auto iterator = callObjects.find(address);  // (7)

    if (iterator != callObjects.end())

    {

      return iterator->second(arguments);    // (8)

    }

    else

    {

      throw std::invalid_argument("Invalid receiver address");  // (9)

    }

  }

private:

  std::map< Address, std::function<Return(ArgumentList)>, AddressCompare  > callObjects;  // (10)

};


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

В строке 2 объявлена частичная специализация, в которой дополнительно представлены параметр для возвращаемого значения Return и пакет параметров ArgumentList для аргументов функции. В строке 3 объявлен класс, который специализируется сигнатурой из указанных параметров.

В строке 4 объявлен шаблон метода для добавления получателя, который принимает адрес address, вызываемый объект object и добавляет их в контейнер. В строке 5 объявлен метод для удаления получателя. Оба метода работают с контейнером, который объявлен в строке 10. Контейнер объявлен как std::map, ключом является адрес, а значением объект std::function с заданной сигнатурой.

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

В строке 7 происходит поиск получателя по адресу. Если получатель найден, то происходит вызов объекта (строка 8). Если получатель не найден, то генерируется исключение (строка 9), иначе какой результат нам возвратить?

5.7.3. Использование адресного распределения

Пример использования адресного распределения приведен в Листинг 85.

Листинг 85. Использование адресного распределения

struct FO

{

  int operator() (int eventID)

  {

    return 10;

  }

};


int ExternalHandler(int eventID)

{

  return 0;

}


struct ReceiverAddress  // (1)

{

  ReceiverAddress(int idGroup = 0, int idNumber = 0)

  {

    group = idGroup; number = idNumber;

  }


  int group;

  int number;

};


template<>

struct std::less<ReceiverAddress>  // (2)

{

  bool operator() (const ReceiverAddress& addr1, const ReceiverAddress& addr2) const

  {

    if (addr1.group < addr2.group)

    {

      return true;

    }

    else

    {

      if (addr1.group == addr2.group)

        return addr1.number < addr2.number;

      else

        return false;

      }

  }

};


int main()

{

  int eventID = 0;

  FO fo;

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


  AddressDistributor<ReceiverAddress, std::less<ReceiverAddress>, int(int)> distributor;  // (3)


  distributor.addReceiver({ 1,1 }, fo);               // (4)

  distributor.addReceiver({ 2,2 }, ExternalHandler);  // (5)

  distributor.addReceiver({ 3,3 }, lambda);           // (6)


  distributor({ 1,1 }, eventID);  // (7)

  distributor({ 2,2 }, eventID);  // (8)

  distributor({ 3,3 }, eventID);  // (9)

}


В строке 1 объявлена структура для адреса, которая состоит из двух полей: идентификатор группы и номер получателя в группе. Сравнить эти две структуры напрямую нельзя, поэтому потребуется реализовать предикат.

В строке 2 объявлен функциональный объект, реализующий предикат для сравнения адресов. Почему именно в таком виде? Дело в том, что std::map требует, чтобы в качестве предиката использовался именно функциональный объект, мы не можем для этого использовать внешнюю функцию или лямбда-выражение. Это связано с тем, что в контейнере предикат хранится в виде переменной с конструктором, тип переменной определяется параметром шаблона. А наличие конструктора может обеспечить только функциональный объект.

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

В STL уже объявлен шаблон структуры для предикатов std::less, параметром которого выступает тип данных, которые необходимо сравнить. Этот предикат принимает на вход две переменные и возвращает true, если первая меньше второй34. std::less реализует арифметическое сравнение, поэтому для типов, которые поддерживают арифметические операции, предикат объявлять не нужно, он будет сгенерирован компилятором. Однако в нашем случае данные арифметически сравниваться не могут, поэтому мы специализируем этот шаблон своим типом (строка 2) и реализуем перегруженный оператор, который будет сравнивать две структуры. При инстанциировании контейнера компилятор сам выберет подходящую специализацию предиката, исходя из типа хранимых элементов.

В строке 3 объявлен объект распределителя путем инстанциирования соответствующего шаблона. Аргументами шаблона выступают тип адреса, предикат для сравнения и сигнатура для вызова объектов. В строках 4, 5, 6 в распределитель добавляются объекты вызова различных типов, в строках 7, 8, 9 эти объекты будут вызваны в соответствии с их адресами.

5.8. Итоги

Под распределением вызовов понимается техника, в которой при вызове единственной функции осуществляется выполнение множества вызовов через соответствующие аргументы. Структурно распределение состоит из следующих компонентов: источник, получатель, распределитель, распределяющая функция.

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

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

Если требуются результаты выполнения вызовов, то они реализуются с помощью отдельной распределяющей функции, которая возвращает результаты в виде кортежа.

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

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

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

Назад Дальше