Обратные вызовы в C++ - читать онлайн бесплатно, автор Виталий Евгеньевич Ткаченко, ЛитПортал
bannerbanner
Полная версияОбратные вызовы в C++
Добавить В библиотеку
Оценить:

Рейтинг: 4

Поделиться
Купить и скачать
На страницу:
12 из 13
Настройки чтения
Размер шрифта
Высота строк
Поля

Итак, класс для анализа готов. Теперь можно вызвать метод для итерации по элементам контейнера, и в качестве обратного вызова передать экземпляр соответствующего вспомогательного класса. Метод будет вызывать перегруженный оператор, и таким образом, мы узнаем минимальное либо максимальное значение (Листинг 101).

Листинг 101. Поиск минимального и максимального значений (SensorControl.cpp)

SensorValue SensorControl::getMinValue(SensorNumber first, SensorNumber last)

{

  checkInitialize();


  FindMinMaxValue fmv(first, last, FindMinMaxValue::MIN_VALUE);

  sensorContainer_->forEachSensor(fmv);

  return fmv.result();

}


SensorValue SensorControl::getMaxValue(SensorNumber first, SensorNumber last)

{

  checkInitialize();


  FindMinMaxValue fmv(first, last, FindMinMaxValue::MAX_VALUE);

  sensorContainer_->forEachSensor(fmv);

  return fmv.result();

}

6.3. Разработка системного API

6.3.1. API как оболочка

Уже после того, как классы модуля были разработаны, протестированы и начали использоваться в системе, появилось новое требование – ввести поддержку системного API. Как известно, в интерфейсах системных API можно использовать только внешние функции и простые структуры данных в стиле C; классы и другие специфические конструкции C++ использовать нельзя (см. п. 1.4.2). Так что же, все теперь придется переписывать? Можно предложить следующее решение: использовать интерфейс API как оболочку для вызова методов класса. Концептуальный пример приведен в Листинг 102.

Листинг 102. Концептуальный пример реализации API как оболочки

using ControlPointer = std::unique_ptr;

ControlPointer g_SensorControl(sensor::ISensorControl::createControl());


void initialize () // This function is declared in the header file as part of API interface

{

  g_SensorControl->initialize();

}


Однако не все так просто, перед нами встают следующие проблемы.

1. В исходной реализации мы использовали специфические типы C++, такие, как std::function, smart pointers и т. п., что не допускается в интерфейсах системных API. Какие типы использовать взамен?

2. Для обработки ошибок в исходной реализации мы использовали исключения. Как сейчас обрабатывать ошибки, ведь в интерфейсах API исключения недопустимы?

3. В исходной реализации мы в каждом потоке могли объявить отдельный интерфейсный класс и работать с ним независимо от остальных потоков. Как теперь обеспечить многопоточную работу, ведь отдельные потоки вызывают одни и те же интерфейсные функции?

4. В исходной реализации драйвер настраивался путем создания нового класса и передаче его в интерфейсный класс. Как теперь настраивать драйвер, если в интерфейсах API нельзя использовать классы?

5. Как организовать обратные вызовы?


Рассмотрим, как эти проблемы можно решить.

6.3.2. Объявления типов

В исходной реализации общие типы объявлены в SensorDef.h, но мы не можем просто перенести их в интерфейс API из-за использования специфических конструкций С++. Поэтому нам придется повторить эти объявления в стиле C с использованием простых типов, которые можно будет использовать в интерфейсных функциях. Объявления представлены в Листинг 103.

Листинг 103. Объявления типов для интерфейса API (SensorLib.h)

#ifdef _WINDOWS  // (1)

  #ifdef LIB_EXPORTS

    #define LIB_API __declspec(dllexport)

  #else

    #define LIB_API __declspec(dllimport)

  #endif

  #else

    #define LIB_API

#endif


typedef uint32_t SensorNumber;       // (2)

typedef double SensorValue;          // (3)

typedef uint32_t CheckAlertTimeout;  // (4)


typedef uint32_t SensorType;         // (5)

typedef uint32_t DriverType;         // (6)

typedef uint32_t AlertRule;          // (7)


typedef void(*SensorValueCallback)(SensorNumber, SensorValue, void*);               // (8)

typedef CheckAlertTimeout(*SensorAlertCallback)(SensorNumber, SensorValue, void*);  // (9)

typedef SensorValue(*OnSimulateReadValue)(SensorNumber, int, void*);                // (10)

typedef int (*OnSimulateOperable)(SensorNumber, void*);                             // (11)


enum eSensorType  // (12)

{

  SENSOR_SPOT = 0,

  SENSOR_SMOOTH = 1,

  SENSOR_DERIVATIVE = 2,

};


enum eDriverType  // (13)

{

  DRIVER_SIMULATION = 0,

  DRIVER_USB = 1,

  DRIVER_ETHERNET = 2

};


enum  eAlertRule  // (14)

{

  ALERT_MORE = 0,

  ALERT_LESS = 1

};


В строке 1 объявлены определения для экспортируемых функций. Эти объявления необходимы для компиляции динамической библиотеки в среде Windows, для других платформ они неактуальны.

В строках 2–4 объявлены типы, которые будут использоваться для входных параметров интерфейсных функций. Это те же объявления, которые использовались в исходной реализации (SensorDef.h, см. п. 6.2.2).

В строках 5–7 вместо перечислений C++ объявляются простые числовые типы. В экспортируемых функциях нежелательно использовать перечисления как типы входных параметров, потому что размер этих типов в C явно не определен. Вместо этого перечисления используются в качестве числовые констант, они объявлены соответственно в строках 12–14.

В строках 8–11 объявлены типы указателей на функцию для выполнения обратных вызовов. Как видим, в отличие от исходной реализации здесь присутствует дополнительный параметр для указания контекста вызова.

6.3.3. Интерфейс API и обработка ошибок

Исходя из концепции «API как оболочка», сигнатура интерфейсных функций API должна повторять сигнатуру методов интерфейсного класса. Однако здесь мы сталкиваемся с некоторыми проблемами, одна из которых – это обработка ошибок.

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

1) функция возвращает результат, для которого некоторое предопределенное значение говорит о том, что произошла ошибка. Код ошибки возвращается с помощью отдельного вызова;

2) код ошибки возвращается через дополнительный параметр функции;

3) все функции возвращают результат выполнения, который является кодом ошибки.

Ни один способов не является идеальным, каждый имеет свои достоинства и недостатки. Так, в первом способе возникают сложности, если результат, возвращаемый функцией, не имеет значений, которые недопустимы и могут сигнализировать о возникновении ошибки36. Во втором способе для всех вызовов придется использовать дополнительную переменную – код ошибки, даже если он нас не интересует. В третьем способе, если функция возвращает результат, то для него приходится использовать отдельный входной параметр, что не всегда удобно.

В нашем случае мы выберем третий способ, исходя из следующих соображений: объявления функций будут выглядеть единообразно; возникновение ошибки можно узнать непосредственно в момент вызова, (например, в операторе if); если функция не возвращает значений, то ей не нужно передавать никакие дополнительные параметры. Объявления интерфейсных функций с возвратом ошибок представлены в Листинг 104.

Листинг 104. Интерфейс системного API (SensorLib.h)

typedef unsigned int ErrorCode;


LIB_API ErrorCode initialize();

LIB_API ErrorCode shutDown();

LIB_API ErrorCode assignDriver(DriverType type);

LIB_API ErrorCode getAssignedDriver(DriverType* type);

LIB_API ErrorCode getSensorDriver(SensorNumber number, DriverType* type);

LIB_API ErrorCode addSensor(SensorType type, SensorNumber number);

LIB_API ErrorCode deleteSensor(SensorNumber number);

LIB_API ErrorCode isSensorExist(SensorNumber number, int* isExist);

LIB_API ErrorCode isSensorOperable(SensorNumber number, int* isOperable);

LIB_API ErrorCode getSensorValue(SensorNumber number, SensorValue* value);

LIB_API ErrorCode querySensorValue(SensorNumber number, SensorValueCallback callback, void* pContextData);

LIB_API ErrorCode readSensorValues(SensorValueCallback callback, void* pContextData);

LIB_API ErrorCode getMinValue(SensorNumber first, SensorNumber last, SensorValue* value);

LIB_API ErrorCode getMaxValue(SensorNumber first, SensorNumber last, SensorValue* value);

LIB_API ErrorCode setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс, void* pContextData);

LIB_API ErrorCode resetAlert(SensorNumber number);

LIB_API ErrorCode setSimulateReadCallback(OnSimulateReadValue callback, void* pContextData);

LIB_API ErrorCode setSimulateOperableCallback(OnSimulateOperable callback, void* pContextData);


В реализации этих функций мы будем возвращать код ошибки, получая его из перехваченного исключения. В качестве примера рассмотрим реализацию функции для получения значения датчика (Листинг 105).

Листинг 105. Функция для получения значения датчика

ErrorCode getSensorValue(SensorNumber number, SensorValue* value)

{

  ErrorCode error = ERROR_NO;  // (1)


  try

  {

    *value = g_SensorControl->getSensorValue(number);  // (2)

  }

  catch (sensor::sensor_exception& e)  // (3)

  {

    error = e.code();                  // (4)

  }

  return error;                        // (5)

}


В строке 1 объявляем переменную – код возврата. В строке 2 осуществляем вызов метода класса, который заключен в блок try. В строке 3 осуществляется перехват исключения, в строке 4 присваивается код ошибки, который возвращается в строке 5.


Итак, мы придумали, как в интерфейсных функциях осуществлять обработку ошибок. Теперь перед нами встает следующая проблема: как настраивать типы драйверов, ведь в исходной реализации для этого используются классы? Прежде чем перейти к решению этой задачи, остановимся на реализации многопоточной работы, поскольку используемые там конструкции нам понадобятся в дальнейшем.

6.3.4. Многопоточная работа

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

До появления стандарта C++ 11 использовать локальную память потока было непросто: для этого требовалось явное обращение к функциям операционной системы, что усложняло реализацию и делало код платформенно-зависимым. В C++ 11 появилось ключевое слово thread_local, и это сильно упростило жизнь: если в объявлении переменной добавить указанный спецификатор, то она становится локальной в рамках потока, т. е. каждый новый создаваемый поток будет иметь независимый экземпляр соответствующей переменной. Таким образом, достаточно экземпляр интерфейсного класса ISensorControl объявить как thread_local, и теперь для каждого потока будет существовать отдельный независимый экземпляр класса (Листинг 106).

Листинг 106. Объявление экземпляра класса как локального для текущего выполняемого потока (SensorLib.cpp)

using ControlPointer = std::unique_ptr;

thread_local ControlPointer g_SensorControl(sensor::ISensorControl::createControl());

6.3.5. Настройка драйвера

В исходной реализации в начале работы мы создавали необходимый класс драйвера, который затем передавали интерфейсному классу (Листинг 107). Но в интерфейсах системных API мы классы использовать не можем, как поступить в этом случае? Можно предложить следующее решение: класс драйвера создавать внутри API, а в функцию настройки передавать идентификатор, в соответствии с которым будет создан соответствующий драйвер (Листинг 108).

Листинг 107. Настройка драйвера в исходной реализации

ISensorControl sensorControl = ISensorControl::createControl;

DriverPointer driver = IDriver::createDriver(DRIVER_SIMULATION);

driver->initialize();

sensorControl->assignDriver(driver);


Листинг 108. Настройка драйвера в системном API (SensorLib.h)

thread_local sensor::DriverPointer g_DriverSimulation;  // (1)

thread_local sensor::DriverPointer g_DriverUSB;         // (2)

thread_local sensor::DriverPointer g_DriverEthernet;    // (3)


void CreateDriver(sensor::DriverType driverType, sensor::DriverPointer& driverPointer)  // (4)

{

  if (!driverPointer)

  {

    driverPointer = sensor::IDriver::createDriver(driverType);

    driverPointer->initialize();

  }


  g_SensorControl->assignDriver(driverPointer);

}


ErrorCode assignDriver(DriverType driverType)  // (5)

{

  ErrorCode error = ERROR_NO;


  try

  {

    EnumConverter conv;

    conv.convert (driverType, {sensor::DriverType::Simulation, sensor::DriverType::Usb, sensor::DriverType::Ethernet});  // (6)


    if (conv.error())

    {

      return ERROR_INVALID_ARGUMENT;

    }

    switch (conv.result())  // (7)

    {

      case sensor::DriverType::Simulation:

      {

        CreateDriver(sensor::DriverType::Simulation, g_DriverSimulation);

      }

      break;


      case sensor::DriverType::Usb:

      {

        CreateDriver(sensor::DriverType::Usb, g_DriverUSB);

      }

      break;


      case sensor::DriverType::Ethernet:

      {

        CreateDriver(sensor::DriverType::Ethernet, g_DriverEthernet);

      }

      break;

    }

  }

  catch (sensor::sensor_exception& e)

  {

    error = static_cast(e.code());

  }


  return error;


}


В строках 1–3 объявляются указатели для хранения классов всех возможных типов драйверов. В строке 4 объявлена вспомогательная функция для создания драйвера. Эта функция проверяет, создан ли драйвер соответствующего типа, при необходимости создает, инициализирует и передает его в интерфейсный класс.

В строке 5 приведена реализация интерфейсной функции для настройки драйвера. В строке 6 конвертируется переданное числовое значение в перечисление C++ (будет рассмотрено ниже). В строке 7 объявлен оператор switch, в котором анализируется полученное значение перечисления, и вызывается вспомогательная функция с соответствующими параметрами.


В функции API для задания типа драйвера используются числовые значения, а в интерфейсном классе используются перечисления C++. Для того, чтобы сконвертировать числовое значение в перечисление, используется вспомогательный класс EnumConverter (Листинг 109)

Листинг 109. Конвертер числовых значений в перечисление (EnumConverter.h)

template   // (1)

class EnumConverter

{

public:

  template                                     // (2)

  void convert(ConvValueType value, std::initializer_list list)  // (3)

  {

      isError_ = true;

      for (Enum item : list)                              // (4)

      {

          if (static_cast(item) == value)  // (5)

          {

              result_ = item;                             // (6)

              isError_ = false;

              break;

          }

      }

  };


  bool error() const { return isError_; }

  Enum result() const { return result_; }

private:

  bool isError_;

  Enum result_;

};


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

6.3.6. Обратные вызовы

Касательно обратных вызовов мы имеем следующую ситуацию. В системном API контекст вызова передается с помощью указателей на данные, по-другому организация передачи контекста здесь невозможна (см. п. 2.1.2). В интерфейсном классе указатель на данные не используется, поскольку в C++ имеется множество гораздо более изящных способов передачи контекста. Вот тут-то нам и понадобится перенаправление вызовов (см. п. 4.6.2). Реализация одной из интерфейсных функций API, использующей перенаправление вызовов, приведена в Листинг 110.

Листинг 110. Перенаправление вызовов в реализации интерфейсной функции (SensorLib.cpp)

ErrorCode readSensorValues(SensorValueCallback callback, void* pContextData)

{

  ErrorCode error = ERROR_NO;


  try

  {

    using namespace std::placeholders;

    g_SensorControl->readSensorValues(std::bind(callback,_1,_2,pContextData));   // (1)

  }

  catch (sensor::sensor_exception& e)

  {

    error = e.code();

  }

  return error;

}


В общем-то, вся реализация заключается в вызове метода интерфейсного класса (строка 1), в который вместо непосредственно обратного вызова передается объект связывания. Функция обратного вызова, объявленная в интерфейсе API, принимает 3 входных параметра: номер датчика, значение датчика и указатель на контекст. Когда будет происходить обратный вызов, то объект связывания вызовет назначенную функцию, в которую передаст первые два параметра исходной функции, а в третий параметр будет передан переданный указатель на контекст.

6.4. Итоги

На примере разработки модуля управления датчиками кратко описаны типовые этапы проектирования: описание технического задания; оформление сценариев функционирования системы; декомпозиция и формирование архитектуры. Затем рассмотрена реализация классов с акцентом на использовании обратных вызовов. И в заключение показан процесс создания системного API и трудности, с которыми сталкивается разработчик при реализации концепции «API как оболочка». Как можно увидеть в рассмотренном примере, в практике разработки ПО существует множество ситуаций, когда целесообразно использовать обратные вызовы как элементы дизайна компонентов системы.

Заключение

Итак, наше повествование подходит к концу, пора подвести некоторые итоги.

Обратный вызов – это паттерн, в котором какой-либо исполняемый код как аргумент передается в другой код. Ожидается, что через сохраненный аргумент исполняемый код будет запущен в какой-то момент времени. Типовые задачи, решаемые с помощью обратных вызовов, следующие: запрос данных; вычисления по запросу; перебор элементов; уведомления о событиях.

В C++ обратные вызовы реализуются с помощью следующих механизмов: указатель на функцию; указатель на статический метод класса; указатель на метод-член класса; функциональный объект; лямбда-выражение. Все они имеют свои достоинства и недостатки, и нельзя однозначно сказать, какой является наилучшим, все зависит от поставленных задач и требований к проектируемой системе.

Чтобы определить реализацию, наиболее подходящую для конкретной ситуации, целесообразно использовать метод интегральных оценок, который предлагает простые и эффективные процедуры выбора оптимального решения. Указанный метод можно использовать не только применительно к обратным вызовам, но также и в других случаях, когда необходимо выбрать наиболее подходящее архитектурное решение из множества возможных.

Шаблоны, основанные на принципах параметрического полиморфизма, открывают разработчику новые горизонты. С их помощью появляется возможность создавать обобщенный код, т. е. код, независимый от данных. Применительно к обратным вызовам шаблоны позволяют без особых усилий обеспечить требования, которые крайне сложно, а иногда и невозможно реализовать обычными средствами C++. К ним относятся возможность хранения аргумента настраиваемого типа (универсальный аргумент), настройка сигнатуры вызова, выполнение вызовов для набора аргументов различных типов и т. п.

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

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

Список литературы и интернет-источников

Здесь приводится список упомянутых книг, а также литература и интернет-ресурсы для углубленного изучения рассмотренных тем.


1. Басс Л., Клементс П., Кацман Р. Архитектура программного обеспечения на практике. Спб, Питер, 2006. – 574 с.

Фундаментальное введение в теорию и практику построения программной архитектуры систем. Приведены методики сравнительного анализа архитектурных решений.

2. Вандевурд Д., Джосаттис Н., Грегор Д. Шаблоны C++. Справочник разработчика. Спб, Альфа-книга, 2018. – 848 с.

Максимально полно охватывает разнообразные аспекты использования шаблонов в C++, подходит как как в качестве справочного, так и учебного пособия.

3. Галовиц Я. C++ 17 STL. Стандартная библиотека шаблонов. Спб., Питер, 2018. – 432 с.

Отличная книга для изучения стандартной библиотеки STL.

4. Гамма Э., Хелм Р., Джонсон Р., Влиссидес Д. «Приемы объектно-ориентированного проектирования. Паттерны проектирования». Спб, Питер, 2020. – 368 с.

Рассматриваются наиболее употребительные паттерны проектирования и их использование в решении задач. Не привязана к конкретному языку программирования. Отличается легкостю и доступностью изложения.

5. Касперски Крис. Техника оптимизации программ. Эффективное использование памяти. Спб, БХВ-Петербург, 2003. – 560 с.

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

6. Леоненков А. В. Самоучитель UML 2. СПб, БХВ-Петербург, 2007. – 576 с.

Простое и доступное изложение основ UML.

7. Орлов С. А. Программная инженерия. Технологии разработки программного обеспечения. Спб, Питер, 2018. – 640 с.

Рассматривается методология разработки программного обеспечения, организации и процессы проектирования больших программных систем.

8. Пикус Ф. Г. Идиомы и паттерны проектирования в современном C++. М, ДМК Пресс, 2020. – 452 с.

Рассматриваются реализации различных паттернов проектирования с использованием современных средств C++. Книга достаточно сложная, предполагается, что читатель хорошо владеет C++, имеет опыт обобщенного программирования.

9. Пирс Бенджамин. Типы в языках программирования. М., Лямбда-пресс, 2011. – 656 с.

Академическое изложение теории типов, довольно сложный математический аппарат. Книга скорее ориентирована на теорию, чем на практическое применение, но есть интересные темы о классификации типов и видах полиморфизма.

10. Эванс Эрик. Предметно-ориентированное проектирование (DDD): структуризация сложных программных систем. М., Вильямс, 2011. – 448 с.

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

11. Lambda Expressions in C++.

https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=vs-2019

Кратко и наглядно описан синтаксис лямбда-выражений.

12. Template Specialization (C++).

На страницу:
12 из 13