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


  initiator.run();                                         // (7)

}


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

2.3.4. Управление контекстом

Рассматриваемая реализация позволяет осуществлять управление контекстом тремя способами: настройка экземпляра класса-исполнителя, настройка указателя на метод, переопределение виртуальных функций. Это приводит к интересным эффектам.

Пусть у нас будут объявления классов-исполнителей с наследованием, как показано в Листинг 12. Графически иерархия наследования изображена на Рис. 13.

Листинг 12. Классы-исполнители с наследованием

class Executor

{

public:

  virtual void callbackHandler1(int eventID);

  virtual void callbackHandler2(int eventID);

};


class Executor1: public Executor

{

public:

  void callbackHandler1(int eventID) override;

};


class Executor2: public Executor

{

public:

  void callbackHandler2(int eventID) override;

};


class Executor3: public Executor1, public Executor2

{

};


Рис. 13. Иерархия наследования классов-исполнителей


Итак, будем назначать различные указатели на экземпляры классов и методы-члены, как показано в Листинг 13.

Листинг 13. Настройка указателей на классы и методы

int main()

{

  Initiator initiator;

  Executor  executor;

  Executor1 executor1;

  Executor2 executor2;

  Executor3 executor3;


  initiator.setup(&executor, &Executor::callbackHandler1);   // (1)

  initiator.setup(&executor, &Executor::callbackHandler2);   // (2)

  initiator.setup(&executor1, &Executor::callbackHandler1);  // (3)

  initiator.setup(&executor1, &Executor::callbackHandler2);  // (4)

  initiator.setup(&executor2, &Executor::callbackHandler1);  // (5)

  initiator.setup(&executor2, &Executor::callbackHandler2);  // (6)


  //initiator.setup(&executor3, &Executor::callbackHandler1); //Incorrect, base class is ambiguous  // (7)

  //initiator.setup(&executor3, &Executor::callbackHandler2); //Incorrect, base class is ambiguous  // (8)


  initiator.setup((Executor1*)&executor3, &Executor::callbackHandler1);  // (9)

  initiator.setup((Executor1*)&executor3, &Executor::callbackHandler2);  // (10)

  initiator.setup((Executor2*)&executor3, &Executor::callbackHandler1);  // (11)

  initiator.setup((Executor2*)&executor3, &Executor::callbackHandler2);  // (12)

}


В строках 1 и 2 все прозрачно: какой метод назначен, такой и будет вызван.

В строке 3 мы назначаем указатель на метод Executor::callbackHandler1, но поскольку в классе Executor1 он переопределен, будет вызван метод Executor1::callbackHandler1.

В строке 4 мы назначаем указатель на Executor::callbackHandler2; в классе Executor1 такого метода нет (т.е. он не переопределен), поэтому будет вызван метод базового класса Executor::callbackHandler2.

В строке 5 мы назначаем указатель на Executor::callbackHandler1; в классе Executor2 метод не переопределен, поэтому будет вызван метод базового класса Executor::callbackHandler2.

В строке 6 мы назначаем указатель на Executor::callbackHandler2; в классе Executor2 он переопределен, поэтому будет вызван метод Executor2:: callbackHandler2.

С классом Executor3 ситуация еще интереснее, поскольку он использует множественное наследование6. Мы не можем напрямую назначать указатели на методы базового класса, как это приведено в строках 7 и 8, потому что если взглянуть на иерархию наследования, то можно увидеть, что к базовому классу можно добраться двумя путями через Executor1 либо через Executor2. Таким образом, компилятор не знает, по какому пути выполнять поиск методов, и выдает ошибку. По указанной причине мы должны явно указать в цепочке наследования класс-предшественник. Если в пути наследования какая-нибудь функция окажется переопределена, то она будет вызвана, в противном случае будет вызвана функция базового класса.

В строке 9 мы в качестве предшественника указываем класс Executor1 и назначаем указатель на метод callbackHandler1. В Executor1 этот метод переопределен, и он будет вызван. В строке 10 мы назначаем указатель на метод callbackHandler2; в Executor1 этот метод не переопределен, поэтому будет вызван метод базового класса Executor::callbackHandler2. Если мы в качестве предшественника будем указывать Executor2, как это показано в строках 11 и 12, то получится все наоборот: в строке 11 будет вызван метод базового класса Executor:: callbackHandler1, а в строке 12 будет вызван соответствующий переопределенный метод Executor2::callbackHandler2.

Для наглядности сведем результаты в Табл. 3.


Табл. 3. Вызовы методов по цепочке наследования


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

2.3.5. Синхронный вызов

Реализация инициатора для синхронного вызова представлена в Листинг 14. В отличие от асинхронного вызова, здесь аргументы не хранятся, а передаются как входные параметры функции.

Листинг 14. Инициатор для синхронного обратного вызова с указателем на метод-член класса

class Executor;

using ptr_method_callback_t = void(Executor::*)(int);


void run(Executor* ptrClientCallbackClass, ptr_method_callback_t ptrClientCallbackMethod)

{

int eventID = 0;

//Some actions

(ptrClientCallbackClass->*ptrClientCallbackMethod)(eventID);

}

2.3.6. Преимущества и недостатки

Преимущества и недостатки реализации обратных вызовов с помощью указателя на метод член класса приведены в Табл. 4.


Табл. 4. Преимущества и недостатки реализации обратных вызовов с помощью указателя на метод-член класса


Гибкость. Управлять контекстом можно тремя способами, подобные возможности отсутствуют в других реализациях.

Отсутствие трансляции контекста. Контекст транслировать не нужно, метод-член имеет полный доступ к содержимому класса.

Сложность. Код получается довольно громоздким и запутанным.

Тип класса должен объявляться в инициаторе. Здесь достаточно только предварительного объявления класса. Полное объявление класса в инициаторе делать необязательно и даже нежелательно, потому что логически это обработчик обратного вызова, то есть он относится к исполнителю и должен быть в нем реализован. Тем не менее, требование предварительного объявления класса ограничивает независимость исполнителя: он может использовать только те типы классов, которые были предварительно объявлены в инициаторе.

Инициатор должен хранить указатель на метод и указатель на класс. Увеличивается расход памяти.

2.4. Функциональный объект

2.4.1. Концепция

С точки зрения C++ функциональный объект это класс, который имеет перегруженный оператор вызова функции7.

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


Рис. 14. Реализация обратного вызова с помощью функционального объекта.


2.4.2. Инициатор

Предварительно необходимо объявить функциональный объект (см. Листинг 15), потому что его объявление должен видеть как инициатор, так и исполнитель.

Листинг 15.Объявление функционального объекта

class CallbackHandler

{

public:

  void operator() (int eventID) //This is an overloaded operator

  {

    //It will be called by server

  };

};


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

Листинг 16. Инициатор с функциональным объектом

class Initiator  // (1)

{

public:

  void setup(const CallbackHandler& callback)  // (2)

  {

    callbackObject = callback;

  }


  void run()  // (3)

  {

    int eventID = 0;

    //Some actions

    callbackObject(eventID);  // (4)

  }


private:

  CallbackHandler callbackObject;  // (5)

};


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

Назад Дальше