Оценить:
 Рейтинг: 0

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

Год написания книги
2021
<< 1 ... 5 6 7 8 9 10 11 12 13 >>
На страницу:
9 из 13
Настройки чтения
Размер шрифта
Высота строк
Поля

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 - Вообще, множественное наследование – неоднозначный механизм, который часто подвергается критике. В большинстве современных языков (например, Java, C#, Ruby и др.) множественное наследование не поддерживается. Тем не менее, в C++ множественное наследование существует, поэтому необходимо рассмотреть и такой случай.]. Мы не можем напрямую назначать указатели на методы базового класса, как это приведено в строках 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

};

};
<< 1 ... 5 6 7 8 9 10 11 12 13 >>
На страницу:
9 из 13