Выбрать главу

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

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

Обратные вызовы часто используются в системных и C++ API. При использовании в системных API на реализацию обратных вызовов накладываются ограничения.

Рассмотрев общую концепцию, приступим к обзору способов реализации обратных вызовов.

2. Реализация обратных вызовов

2.1. Указатель на функцию

2.1.1. Концепция

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

Рис. 10. Обратный вызов с указателем на функцию

2.1.2. Инициатор

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

Листинг 1.Иинициатор с указателем на функцию

typedef void(*ptr_callback) (int eventID, void* pContextData);  // (1)

ptr_callback ptrCallback = NULL;  // (2)

void* contextData = NULL;         // (3)

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

{

   ptrCallback = pPtrCallback;

   contextData = pContextData;

}

void run()                            // (5)

{

  int eventID = 0;

  //Some actions

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

}

В строке 1 объявлен тип – указатель на функцию, в строке 2 объявлена переменная этого типа, в строке 3 объявлен указатель на данные контекста. В строке 4 объявлена функция для настройки указателей, в которой инициализируются соответствующие переменные. В строке 5 объявлена функция запуска, внутри этой функции инициатор в строке 6 производит вызов функции по сохраненному указателю. Сигнатура функции, объявленная в строке 1, в качестве первого параметра принимает значение, которое передается инициатором, т. е. информацию вызова, а второй параметр – это контекст. Указанная сигнатура здесь только для примера; конечно же, в зависимости от поставленных задач количество параметров и их порядок может быть произвольным. Мы также опустили моменты, связанные с созданием потока, ожиданием окончания работы сервера и т. п. – для понимания принципов организации вызова это несущественно.

Итак, мы реализовали инициатор в процедурно-ориентированном дизайне. Приведенная реализация имеет серьезный недостаток: указатель на функцию и указатель на контекст хранятся в глобальных переменных. Это создает множество проблем: изменения настроек указателей в разных частях программы не изолированы, т. е. влияют друг на друга; инициатор может работать только с одним-единственным исполнителем; невозможна одновременная работа нескольких потоков. Выходом из сложившейся ситуации будет реализация инициатора в объектно-ориентированном дизайне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 производится вызов функции по соответствующему указателю. Как видим, объектная реализация практически полностью повторяет процедурную, только все объявления сделаны внутри класса. Другими словами, мы провели инкапсуляцию данных и процедур внутри некоторой сущности, в качестве которой выступает класс.

вернуться

2

Мы здесь (и в дальнейших листингах тоже) не будем разделять заголовочные файлы и файлы реализации: это всего лишь пример, а разделение загромождает описание и усложняет понимание.

вернуться

3

Конечно же, описанные проблемы могут быть решены и в процедурном дизайне, но код при этом значительно усложняется. В общем-то, объектно-ориентированная парадигма и разрабатывалась как средство борьбы с возрастающей сложностью программного кода.