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


Итак, мы реализовали инициатор в процедурно-ориентированном дизайне. Приведенная реализация имеет серьезный недостаток: указатель на функцию и указатель на контекст хранятся в глобальных переменных. Это создает множество проблем: изменения настроек указателей в разных частях программы не изолированы, т. е. влияют друг на друга; инициатор может работать только с одним-единственным исполнителем; невозможна одновременная работа нескольких потоков. Выходом из сложившейся ситуации будет реализация инициатора в объектно-ориентированном дизайне3 (Листинг 2).

Листинг 2. Инициатор с указателем на функцию в объектно-ориентированном дизайне

class Initiator  //(1)

{

public:

  using ptr_callback  =  void(*) (int, void*);                  //(2)


  void setup(ptr_callback pPtrCallback, void* pContextData)    // (3)

  {

      ptrCallback = pPtrCallback; contextData = pContextData;  // (4)

  }


  void run()                               // (5)

  {

      int eventID = 0;

      //Some actions

      ptrCallback (eventID, contextData);  // (6)

}

private:

  ptr_callback ptrCallback = nullptr;      // (7)

  void* contextData = nullptr;             // (8)

};


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


Конечно, поскольку мы программируем на C++, мы должны следовать объектно-ориентированному дизайну, и любые реализации делать в его рамках. Для чего тогда мы привели реализацию инициатора в процедурном дизайне, в стиле языка C? Дело в том, что процедурный дизайн является единственно возможным для проектирования системных API, поскольку в объявлениях интерфейсов таких API допускается использование только глобальных функций и простых структур данных (см. п. 1.4.2).

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

Реализация исполнителя для случая, когда инициатор разработан в процедурном дизайне, представлена в Листинг 3.

Листинг 3. Исполнитель для инициатора в процедурном дизайне

struct СontextData  // (1)

{

    //some context data

};


void callbackHandler(int eventID, void* somePointer)      // (2)

{

  //It will be called by initiator

  СontextData* pContextData = (СontextData*)somePointer;  // (3)

  //Do something here

}


int main()                                 // (4)

{

  СontextData clientContext;               // (5)

  setup(callbackHandler, &clientContext);  // (6)

  run();                                   // (7)

  //Wait finish

}


В строке 1 объявляется тип данных для контекста. Структура здесь показана для примера, в качестве контекста могут выступать любые типы: числа, указатели, смеси и т. п. В строке 2 объявляется функция обработчик обратного вызова, ее сигнатура должна совпадать с сигнатурой, с которой работает инициатор. Указанная функция будет вызвана инициатором, в нее будут переданы два параметра: первый передается инициатором (информация вызова, в нашем случае это eventID), а второй это контекст. Клиент должен интерпретировать контекст; нет другого способа это сделать, кроме как приведением типов (строка 3).

Далее, в строке 4 объявлена основная функция, в которой осуществляются все необходимые операции. В строке 5 объявляются данные контекста; в строке 6 производится настройка обратного вызова, в функцию настройки передаются указатель на функцию-обработчик и указатель на контекст; в строке 7 инициатор запускается.

Реализация исполнителя для случая, когда инициатор реализован в объектно-ориентированном дизайне, представлена в Листинг 4. Как видим, она очень похожа на предыдущую реализацию с той разницей, что мы объявляем экземпляр класса-инициатора (строка 5), и все вызовы осуществляем через вызов соответствующих методов класса.

struct СontextData // (1)

{

  //some context data

};


void callbackHandler(int eventID, void* somePointer) // (2)

{

  //It will be called by initiator

  СontextData* pContextData = static_cast<СontextData*>(somePointer); // (3) cast to context

  //Do something here

}


int main() // (4)

{

  Initiator  initiator;                             // (5)

  СontextData clientContext;                        // (6)

  initiator.setup(callbackHandler, &clientContext); // (7) callback setup

  initiator.run();                                  // (8) initiator has been run

  //Wait finish

}

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

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

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

using ptr_callback  =  void(*) (int, void*);


void run(ptr_callback ptrCallback, void* contextData = nullptr)

{

  int eventID = 0;

  //Some actions

  ptrCallback (eventID, contextData);

}

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

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


Табл. 1. Преимущества и недостатки обратных вызовов с указателем на функцию


Простая реализация. Как мы видели, инициатор реализуется достаточно просто: две переменных, синтаксис вызова функции через указатель очень похож на вызов обычной функции.

Независимость инициатора и исполнителя. Любое изменение кода исполнителя никак не влияет на код инициатора, который при этом остается неизменным

Совместим с кодом на языке C. В некоторых случаях приходится разрабатывать смешанный код, т. е. часть кода пишется C, а часть на С++. Если код исполнителя написан на C++, и этот код должен быть вызван инициатором, написанным на C, то использование указателей на функцию является единственно доступным механизмом. 4

Подходит для реализации любых 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)

Назад Дальше