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


  void run()  // (4)

  {

      int eventID = 0;

      //Some actions

      callbackHandler(ptrClass, eventID);  // (5)

  }


private:

  std::function<void(ClassName*, int)> callbackHandler;  // (6)

  ClassName* ptrClass = nullptr;                         // (7)

};


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


Итак, модифицировав инициатор из Листинг 53 п. 4.6.2, мы реализовали отдельный инициатор для вызова методов-членов. Используя частичную специализацию шаблона, можно сделать так, чтобы оба инициатора объявлялись одинаковым способом (Листинг 58).

Листинг 58. Использование специализации шаблона-инициатора для вызова методов класса

template<typename unused>  // (1)

class Initiator

{

  // Implementation for origin initiator

};


template<typename ClassName>  // (2)

class Initiator<ClassName>

{

  // Implementation for class method call initiator

};


В строке 1 объявлен исходный класс, но теперь он является шаблоном с пакетом параметров. Пакет параметров здесь не используется, он нужен только для дальнейшей специализации.

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


Использование двух типов инициатора (исходного и специализированного) для вызова методов класса приведено в Листинг 59, здесь используется преобразование вызовов из Листинг 54 п. 4.6.3.

Листинг 59. Использование инициатора с оболочкой std::function для вызова методов класса

class Executor

{

public:

  static void staticCallbackHandler(Executor* executor, int eventID) {}

  void callbackHandler(int eventID) {}

  void operator() (int eventID) {}

};


int main()

{

  Executor executor;


  Initiator initiator;  // (1)

  initiator.setup(CallbackConverter<void(Executor::*)(int)>(&Executor::callbackHandler, &executor));  // (2)

  initiator.run();


  Initiator<Executor> initiatorForClass;                // (3)

  initiatorForClass.setup(&Executor::callbackHandler);  // (4)

  initiatorForClass.setupInstance(&executor);           // (5)

  initiatorForClass.run();

}


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

В строке 3 объявлен специализированный инициатор для вызова методов класса, он инстанциируется типом Executor. В строке 4 настраивается указатель на метод класса, в строке 5 настраивается указатель на экземпляр класса.


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

4.6.6. Перенаправление вызовов

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

Графически использование связывания продемонстрировано на Рис. 18. Пусть инициатор вызывает функцию 1, которая на вход принимает аргумент 1. Исполнитель реализует обратный вызов с помощью функции 2, которая принимает на вход два аргумента. Вместо функции 1 инициатору назначается объект связывания, который имеет перегруженный оператор вызова функции с сигнатурой 1. Указанный объект хранит дополнительный параметр, значение которому присваивается во время инициализации. Перегруженный оператор, в свою очередь, вызывает функцию 2, первому аргументу передает сохраненный параметр, а второму аргументу передает значение аргумента из функции 1. Таким образом, осуществляется перенаправление вызова из функции 1 в функцию 2.


Рис. 18. Перенаправление вызовов


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

Листинг 60. Перенаправление вызовов

#include <functional>


void NativeHandler(int eventID)  // (1)

{

  //here eventID is 10

}


void AnotherHandler(int contextID, int eventID)  // (2)

{

  //here eventID is 1, contextID is 10;

}


int main()

{

  int eventID = 1; int contextID = 10;


  std::function<void(int)> fnt;  // (3)

  fnt = NativeHandler;           // (4)

  fnt(eventID);                  // (5) NativeHandler will be called


  fnt = std::bind(AnotherHandler, contextID, std::placeholders::_1);  // (6)

  fnt(eventID);  // (7) AnotherHandler will be called

}


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

В строке 6 вызывается функция bind, которая из переданных аргументов формирует объект связывания. На вход std::bind передается имя новой функции-обработчика и аргументы, которые будут передаваться в эту функцию. Первому аргументу здесь будет назначено значение contextID, а второму аргументу будет назначено значение 1-го по порядку аргумента из исходной функции. Здесь конструкция std::placeholders определяет номер аргумента в исходной функции, который будет подставлен в качестве аргумента в перенаправляемую функцию.

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


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

Листинг 61. Перенаправление вызовов для методов-членов класса

#include <functional>


class CallbackHandler

{

public:

  void NativeHandler(int eventID)

  {

    //eventID = 1;

  }

  void AnotherHandler(int contextID, int eventID)

  {

    //eventID = 1, contextID = 10;

  }

};


int main()

{

  using namespace std::placeholders; // (1)


  int eventID = 1; int contextID = 10;

  CallbackHandler handler;


  std::function<void(CallbackHandler*, int)> fnt;

  fnt = &CallbackHandler::NativeHandler;

  fnt(&handler, eventID); // NativeHandler will be called


  fnt = std::bind(&CallbackHandler::AnotherHandler, _1, contextID, _2); // (2)

  fnt(&handler, eventID); // AnotherHandler will be called

}


Здесь в строке 1 мы использовали using namespace, что сокращает объявление позиций аргументов: как видно из строки 2, мы сразу пишем позицию без использования std::placeholders, что значительно компактнее и проще для восприятия. Здесь в исходной функции присутствует неявный параметр с номером 1, который определяет экземпляр класса. Этот параметр назначается первому (неявному) параметру новой функции, а второй параметр исходной функции eventID назначается последнему параметру новой функции.


В общем случае могут быть 4 варианта перенаправления вызовов:

из функции в функцию (пример в Листинг 60);

из функции в метод класса;

из метода класса в другой метод этого же класса (пример в Листинг 61);

Назад Дальше