скачать книгу бесплатно
//Some actions
ptrCallback (eventID, contextData);
}
2.1.5. Преимущества и недостатки
Достоинства и недостатки реализации обратных вызовов с помощью указателя на функцию представлены в Табл. 1.
Табл. 1. Преимущества и недостатки обратных вызовов с указателем на функцию
Простая реализация. Как мы видели, инициатор реализуется достаточно просто: две переменных, синтаксис вызова функции через указатель очень похож на вызов обычной функции.
Независимость инициатора и исполнителя. Любое изменение кода исполнителя никак не влияет на код инициатора, который при этом остается неизменным
Совместим с кодом на языке C. В некоторых случаях приходится разрабатывать смешанный код, т. е. часть кода пишется C, а часть – на С++. Если код исполнителя написан на C++, и этот код должен быть вызван инициатором, написанным на C, то использование указателей на функцию является единственно доступным механизмом. [4 - В качестве примера можно привести практику моделирования embedded-систем. В самом общем виде Embedded-системы представляют собой микроконтроллер, который встраивается в какое-либо устройство и выполняет функции управления, мониторинга и контроля. В силу определенных причин так сложилось, что ПО для управляющих контроллеров (такое ПО называют firmware) пишется на языке C. В процессе разработки подобных устройств часто используется моделирование, когда firmware запускается на обычном компьютере в имитационном окружении, а реальные аппаратные устройства заменяются их программными моделями. Модели и имитаторы обычно пишутся на языке C++, а firmware, как правило, написано на C – получается смешанный код.]
Подходит для реализации любых API. Можно реализовать как С++, так и системные API. Для C++ API инициатор разрабатывается в виде набора классов, для системных API – в виде набора функций.
Инициатор хранит контекст исполнителя. Как мы видели, инициатор вынужден сохранять контекст исполнителя. Это усложняет реализацию и способствует увеличению расхода памяти.
Небезопасный способ трансляции контекста. Контекст передается клиенту в виде нетипизированного указателя, интерпретация указателя возлагается на клиента. В большой программной системе это чревато ошибками, поскольку нет никакой возможности проверить корректность полученного указателя.
2.2. Указатель на статический метод класса
2.2.1. Концепция
Графическое изображение обратного вызова с помощью указателя на статический метод класса представлено на Рис. 11. Исполнитель реализуется в виде класса, код упаковывается в статический метод класса, в качестве контекста выступает указатель на экземпляр класса. При настройке указатель на статический метод как аргумент и указатель на класс как контекст сохраняются в инициаторе. Инициатор осуществляет обратный вызов посредством вызова метода, передавая ему требуемую информацию и контекст – указатель на класс.
Рис. 11. Обратный вызов с указателем на статический метод класса
2.2.2. Инициатор
По своей сути статический метод класса – это обычная функция, ограниченная областью видимости класса. Поэтому реализация инициатора, представленная в Листинг 6, практически полностью повторяет реализацию для указателей на функцию, только в качестве контекста выступает указатель на экземпляр класса.
Листинг 6. Инициатор с указателем на статический метод класса
class Executor; //(1)
class Initiator // (2)
{
public:
using ptr_callback_static = void(*) (int, Executor*); // (3)
void setup(ptr_callback_static pPtrCallback, Executor* pContextData) // (4)
{
ptrCallback = pPtrCallback; contextData = pContextData; // (5)
}
void run() // (6)
{
int eventID = 0;
//Some actions
ptrCallback(eventID, contextData); // (7)
}
private:
ptr_callback_static ptrCallback = nullptr; // (8)
Executor* contextData = nullptr; // (9)
};
В строке 1 делается предварительное объявление типа класса исполнителя. В строке 2 объявляется класс – инициатор, в строке 3 объявляется тип указателя на функцию с контекстом – экземпляром класса. В строке 4 объявлена функция для настройки указателей, соответствующие переменные (указатель на статический метод и указатель на контекст – экземпляр класса) объявлены в строках 8 и 9. В строке 6 объявлена функция запуска, внутри этой функции в строке 7 производится вызов функции по соответствующему указателю c передачей информации вызова и контекста.
2.2.3. Исполнитель
Реализация исполнителя приведена в Листинг 7.
Листинг 7. Исполнитель с указателем на статический метод класса
class Executor // (1)
{
public:
Executor(Initiator* initiator) // (2)
{
initiator->setup(callbackHandler, this);
}
static void callbackHandler(int eventID, Executor* executor) // (3)
{
//It will be called by initiator
executor->onCallbackHandler(eventID); // (4)
}
private:
void onCallbackHandler(int eventID) // (5)
{
//Do what is necessary
}
};
int main() // (6)
{
Initiator initiator; // (7)
Executor executor(&initiator); // (8)
initiator.run(); // (9)
//Wait finish
}
В строке 1 объявляется класс – исполнитель. В строке 2 объявляется конструктор с входным параметром – указателем на инициатор, здесь происходит настройка обратного вызова.[5 - Это необязательно делать в конструкторе, соответствующие операции можно выполнить после объявлений экземпляров инициатора и исполнителя в функции main. Однако инициализация в конструкторе представляется более удобной, потому что настройка вызова будет сделана сразу при объявлении экземпляра класса – исполнителя без дополнительных операций.]
В строке 3 объявлен статический метод как обработчик обратного вызова. Входными параметрами здесь являются информация вызова (в нашем случае это eventID) и указатель на контекст, в качестве которого выступает указатель на экземпляр класса. Внутри метода можно обращаться к содержимому класса, используя полученный указатель как квалификатор. Таким образом, прямо здесь можно реализовать код обработчика, а можно вызвать обычный (нестатический) метод класса (строка 4).
Далее, в строке 6 объявлена основная функция, в которой осуществляются все необходимые операции. В строке 7 объявлен класс-инициатор; в строке 8 объявлен класс- исполнитель, в конструктор передается указатель на инициатор; в строке 9 происходит запуск инициатора.
Особенностью реализации исполнителя с помощью указателя на статический метод является возможность работы с инициатором, предназначенным для указателей на функцию. В этом случае метод класса в качестве контекста должен принимать нетипизированный указатель с последующим приведением типов. Пример использования показан в Листинг 8, инициатор здесь используется из Листинг 1 п. 2.1.2.
Листинг 8. Исполнитель с указателем на статический метод класса для инициатора с нетипизированным контекстом
class Executor // (1)
{
public:
Executor() // (2)
{
setup(callbackHandler, this);
}
static void callbackHandler(int eventID, void* somePointer) // (3)
{
//It will be called by initiator
Executor* executor = static_cast<Executor*>(somePointer); // (4)
executor->onCallbackHandler(eventID);
}
private:
void onCallbackHandler(int eventID) // (5)
{
//Do what is necessary
}
};
int main() // (6)
{
Executor executor; // (7)
run(); // (8)
//Wait finish
}
Настройка обратного вызова осуществляется в конструкторе (строка 2). В обработчике обратного вызова (строка 3) мы делаем приведение типов (строка 4), чтобы получить указатель на экземпляр класса. В главной функции (строка 6) происходит запуск инициатора.
2.2.4. Синхронный вызов
Реализация инициатора для синхронного вызова приведена в Листинг 9. Как видим, она практически полностью повторяет реализацию, рассмотренную в предыдущей главе, только в качестве указателя на контекст используется указатель на экземпляр класса.