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


class function<Return(ArgumentList)>

{

public:


  Return operator()(ArgumentList arguments)  // (3)

  {

  }

};


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

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

Описанная реализация всего лишь демонстрирует настройку сигнатуры. Практической пользы от нее немного, потому что тело перегруженного оператора пустое, и вызов осуществлен не будет. Используя описанную технику, добавим настройку сигнатуры к аргументу, реализующему стирание типов (Листинг 48).

Листинг 48. Стирание типов с настройкой сигнатуры

template <typename unused>

class UniArgument;


template<typename Return, typename  ArgumentList>

class UniArgument<Return(ArgumentList)>  // (1)

{

private:

  struct Callable

  {

    virtual Return operator()(ArgumentList arguments) = 0;  // (3)

  };


  std::unique_ptr<Callable> callablePointer;


  template <typename Argument>

  struct CallableObject : Callable

  {

    Argument storedArgument;


    CallableObject(Argument argument) : storedArgument(argument) { }


    Return operator() (ArgumentList arguments) override  // (8)

    {

      //return storedArgument(arguments);

      return std::invoke(storedArgument, arguments);     // (9)

    }

  };


public:

  Return operator() (ArgumentList arguments)        // (10)

  {

    return callablePointer->operator()(arguments);  // (11)

  }


  template <typename Argument>

  void operator = (Argument argument)

  {

    callablePointer.reset(new CallableObject<Argument>(argument));

  }

};


По сравнению с реализацией для фиксированной сигнатуры (Листинг 45 п. 4.5.1) изменения здесь следующие. Класс аргумента (строка 1) объявляется в виде шаблона. Параметрами шаблона выступают Return тип значения, возвращаемого функцией, и ArgumentList пакет параметров, определяющих типы передаваемых в функцию аргументов. При объявлении перегруженных операторов (строки 3, 8, 10), вместо конкретного типа возвращаемого значения подставляется параметр шаблона Return, вместо конкретных типов входных параметров подставляется ArgumentList. В местах, где происходит вызов оператора, пакет параметров раскрывается (строки 9 и 11), что означает, что вместо arguments будет подставлен список переменных с типами, заданными в пакете параметров.


Теперь в универсальном аргументе можно настраивать сигнатуру, как это продемонстрировано в Листинг 49.

Листинг 49. Использование аргумента с настройкой сигнатуры

void ExternalHandler1(int eventID) {/*Do something*/}            // (1)

int  ExternalHandler2(int eventID, int contextID) { return 0; }  // (2)


struct CallbackHandler  // (3)

{

  void operator() (int eventID) {}

  bool operator() (int eventID, int contextID) { return false; }

};


int main()

{

  int capturedValue = 100;

  CallbackHandler callbackObject;           // (4)


  UniArgument<void(int)> argument1;         // (5)

  UniArgument<bool(int, int)> argument2;    // (6)


  argument1 = ExternalHandler1;  // (7)

  argument2 = ExternalHandler2;  // (8)


  argument1 = callbackObject;    // (9)

  argument2 = callbackObject;    // (10)


  argument1 = [capturedValue](int eventID) {/*Do something*/};                           // (11)

  argument2 = [capturedValue](int eventID, int contextID) { /*DoSomething*/return 0; };  // (12)


  argument1(3);               // (13)

  int res = argument2(4, 5);  // (14)


  return res;

}


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

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

В строках 7 и 8 в аргумент передаются внешние функции. В строках 9 и 10 передается функциональный объект, у которого, в зависимости от настроенной сигнатуры будет вызван соответствующий перегруженный оператор. В строках 11 и 12 передаются лямбда-выражения. В строках 13 и 14 осуществляются вызовы в соответствии с заданной сигнатурой.

4.5.3. Вызов метода класса

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

До появления стандарта C++17 реализация указанного способа была достаточно сложной: пришлось бы объявлять еще один объект, который наследовался от Callable и осуществлял вызов метода; для создания соответствующего объекта пришлось бы объявить дополнительный перегруженный оператор присваивания, который в качестве входного аргумента принимал указатель на метод. Но в новом стандарте появилась функция std::invoke, которая определяет тип принимаемого объекта вызова и осуществляет вызов для соответствующего типа. Таким образом, для поддержки вызова метода класса необходимо в реализации CallableObject изменить одну-единственную строчку:

Return operator() (ArgumentList arguments) override  // (8)

{

  //return storedArgument(arguments);

  return std::invoke(storedArgument, arguments);  // (9)

}


На удивление просто, не правда ли?

Использование универсального аргумента для вызова метода класса представлено в Листинг 50.

Листинг 50. Использование универсального аргумента для вызова метода класса

struct CallbackHandler

{

  void handler1(int eventID) {};

  bool handler2(int eventID, int contextID) { return false; };

};


int main()

{

  CallbackHandler callbackObject;


  UniArgument<void(CallbackHandler*, int)> argument1;       // (1)

  UniArgument<bool(CallbackHandler*, int, int)> argument2;  // (2)


  argument1 = &CallbackHandler::handler1;  // (3)

  argument2 = &CallbackHandler::handler2;  // (4)

  argument1(&callbackObject, 100);         // (5)

  argument2(&callbackObject, 0, 1);        // (6)

}


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


Итак, универсальный аргумент практически готов. Нам осталось реализовать оператор копирования, оператор присваивания и некоторые другие операции. Но мы этим заниматься не будем: разработчики стандартной библиотеки уже обо всем позаботились, поэтому темой следующей главы будет обзор инструментов STL для организации обратных вызовов24.

4.6. Использование стандартной библиотеки

4.6.1. Организация вызовов

В стандартной библиотеке имеется полиморфный класс оболочка std::function, предназначенная для организации вызовов различных типов. Этот класс идеально подходит на роль универсального аргумента. Кроме рассмотренных техник стирания типа и настройки сигнатуры, в нем реализовано множество других вещей: конструктор копирования, оператор присваивания, поддержка указателей на методы класса, проверка настройки аргумента, локальный буфер для хранения аргумента и многое другое. Мы не будем рассматривать реализацию std::function, потому что, во-первых, она достаточно сложная, а, во-вторых, может изменяться в зависимости от версии и платформы. При желании читатель сможет сделать это самостоятельно, проанализировав исходный код, мы же сосредоточимся на практическом использовании класса-оболочки.

Насколько сложна реализация std::function, настолько же просто ее использование. По аналогии с универсальным аргументом, рассмотренном в предыдущей главе, достаточно объявить экземпляр класса с нужной сигнатурой, после чего ему можно назначать различные объекты вызовов (Листинг 51).

Назад Дальше