Читать книгу Обратные вызовы в C++ - Виталий Евгеньевич Ткаченко - Страница 9

2. Реализация обратных вызовов
2.3. Указатель на метод-член класса

Оглавление

2.3.1. Концепция

В предыдущей главе мы рассматривали использование указателя на статический метод класса, в который в качестве контекста передавали указатель на экземпляр класса. А почему бы нам напрямую не вызвать метод-член класса, минуя прослойку в виде статического метода, из которого вызывается метод-член класса? Для этого нам понадобятся указатель на класс и указатель на метод.

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


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


2.3.2. Инициатор

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

Листинг 10. Инициатор с указателем на метод-член класса

class Executor;  // (1)


class Initiator  // (2)

{

public:

  using ptr_callback_method = void(Executor::*)(int);  // (3)


  void setup(Executor* argCallbackClass, ptr_callback_method argCallbackMethod)    // (4)

  {

      ptrCallbackClass = argCallbackClass; ptrCallbackMethod = argCallbackMethod;  // (5)

  }


  void run()  // (6)

  {

      int eventID = 0;

      //Some actions

      (ptrCallbackClass->*ptrCallbackMethod)(eventID);  // (7)

  }


private:

  Executor* ptrCallbackClass = nullptr;             // (8)

  ptr_callback_method ptrCallbackMethod = nullptr;  // (9)

};


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

2.3.3. Исполнитель

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

Листинг 11. Исполнитель с указателем на метод-член класса

class Executor                       // (1)

{

public:

  void callbackHandler(int eventID)  // (2)

  {

      //It will be called by initiator

  }

};


int main()                                                 // (3)

{

  Initiator initiator;                                     // (4)

  Executor executor;                                       // (5)

  initiator.setup(&executor, &Executor::callbackHandler);  // (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. Преимущества и недостатки реализации обратных вызовов с помощью указателя на метод-член класса.


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

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

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

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

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

6

Вообще, множественное наследование – неоднозначный механизм, который часто подвергается критике. В большинстве современных языков (например, Java, C#, Ruby и др.) множественное наследование не поддерживается. Тем не менее, в C++ множественное наследование существует, поэтому необходимо рассмотреть и такой случай.

Обратные вызовы в C++

Подняться наверх