
Обратные вызовы в C++
Драйверы для работы с физическими устройствами формируют команды, посылают их через соответствующие протоколы и возвращают результаты. Реализацию этих драйверов мы рассматривать не будем, поскольку работа с hardware – это отдельная тема, для изучения которой требуется не одна книга. Для нас представляет интерес реализация имитируемого драйвера.
Очевидно, что имитируемый драйвер должен возвращать заранее заданные значения. Самое простое решение, лежащее на поверхности, заключается в том, чтобы хранить эти значения в глобальных или статических переменных и возвращать их в соответствующих методах. Однако в этом случае имитация будет очень примитивной: для всех датчиков будет возвращаться одно и то же значение. Можно хранить свое возвращаемое значение в каждом классе датчика, что больше похоже на работу в реальной системе, но это также не лишено недостатков: мы не можем моделировать изменения показателей в динамике. Лучшим решением было бы предоставить возможность пользователю возвратить значение в момент запроса, для чего нам, конечно же, понадобится обратный вызов. Обратный вызов будет использоваться по схеме «Запрос данных» (см. п. 1.2.1).
Итак, для реализации интерфейса имитируемого драйвера нам понадобятся дополнительные методы и определения (см. Листинг 89).
Листинг 89. Имитируемый драйвер (DriverImpl.h)class DriverSimulation : public IDriver
{
public:
enum ReadType { READ_SPOT = 0, READ_SMOOTH = 1, READ_DERIVATIVE = 2 }; // (1)
using OnReadValue = std::function
using OnOperable = std::function
void initialize() override;
void activate(SensorNumber number) override;
bool isOperable(SensorNumber number) override;
void setDefaultValue(SensorValue value); // (4)
void setDefaultOperable(bool isOperable); // (5)
void setReadValue(OnReadValue value); // (6)
void setOperable(OnOperable operable); // (7)
SensorValue readSpot(SensorNumber number) override; // (8)
SensorValue readSmooth(SensorNumber number) override; // (9)
SensorValue readDerivative(SensorNumber number) override; // (10)
static IDriver* create();
protected:
DriverSimulation();
private:
OnReadValue getValue_; // (11)
OnOperable getOperable_; // (12)
SensorValue defaultValue_ = 0; // (13)
bool defaultOperable_ = true; // (14)
};
В строке 1 объявляется перечисление для указания используемого метода чтения показателей. В строке 2 и 3 объявляются типы для обратных вызовов. Переменные соответствующих типов для хранения вызовов объявлены в строках 11 и 12. Настройка вызовов производится в методах 6 и 7. Кроме того, объявляются переменные для хранения значений по умолчанию (строки 13 и 14), эти переменные настраиваются в методах 4 и 5.
Реализацию чтения показателей продемонстрируем на примере получения текущего значения датчика (Листинг 90).
Листинг 90. Чтение текущего значения датчика в имитируемом драйвере (DriverImpl.cpp)SensorValue DriverSimulation::readSpot(SensorNumber number)
{
if (getValue_) // (1)
{
return getValue_(number, READ_SPOT); // (2)
}
else
{
return defaultValue_; // (3)
}
}
В строке 1 проверяется, настроен ли обратный вызов. Если настроен, то через него запрашивается значение для соответствующего датчика. Информацией вызова здесь является номер датчика и метод чтения показателей (строка 2). Если обратный вызов не настроен, то возвращается значение по умолчанию (строка 3).
6.2.4. Датчик
Обобщенный интерфейсный класс для работы с датчиком приведен в Листинг 91.
Листинг 91. Интерфейсный класс для роботы с датчиком (SensorInterface.h)namespace sensor
{
class ISensor
{
public:
virtual void setDriver(DriverPointer driverPointer) = 0; // (1)
virtual DriverPointer getDriver() = 0; // (2)
virtual double getValue() = 0; // (3)
virtual bool isOperable() = 0; // (4)
virtual ~ISensor() = default;
static SensorPointer createSensor(SensorType type, SensorNumber number, DriverPointer driverPointer); // (5)
};
}; //namespace sensor
В строке 1 объявлен метод для настройки драйвера, с которым будет работать датчик. Получить используемый драйвер можно с помощью метода 2. В строках 3 и 4 объявлены методы для получения текущего значения датчика и определения его работоспособности. В строке 5 объявлен метод для создания экземпляра класса соответствующего типа.
В соответствии с требованиями нам необходимо реализовать датчики, которые бы возвращали текущие, сглаженные и производные значения показателей. Для каждого способа реализован отдельный класс; диаграмма классов изображена на Рис. 29.

Рис. 29. Диаграмма классов, реализующих управление датчиками
Как видно из диаграммы, при вызове метода для получения значения датчик обращается к драйверу, вызывая соответствующие методы. В зависимости от настроенного драйвера будут возвращаться реальные или имитируемые значения.
6.2.5. Контейнер
Контейнер предназначен для хранения экземпляров классов для управления датчиками. Объявление класса приведено в Листинг 92.
Листинг 92. Объявление контейнера (SensorContainer.h)namespace sensor
{
class ISensor;
class SensorContainer
{
public:
void addSensor(SensorNumber number, SensorPointer sensor); // (1)
void deleteSensor(SensorNumber number); // (2)
SensorPointer checkSensorExist(SensorNumber number); // (3)
SensorPointer findSensor(SensorNumber number); // (4)
template
void forEachSensor(CallbackIterate&& callback) // (5)
{
for (auto item : container_) // (6)
{
callback(item.first, item.second);
}
}
private:
std::map
};
};
Хранилище объектов реализовано в виде двоичного дерева (строка 7). Ключом здесь выступает номер датчика, содержимым является указатель на класс управления датчиком. Методы для добавления и удаления указателей объявлены в строках 1 и 2.
Метод в строке 3 возвращает указатель на объект класса, если последний с заданным номером содержится в хранилище, в противном случае возвращается нулевой указатель. Метод в строке 4 возвращает указатель на объект класса для соответствующего номера; если объект отсутствует, то генерируется исключение.
Метод 5 предназначен для итерации по всем хранимым объектам. Здесь используется обратный синхронный вызов (см. п. 1.4.1) по схеме «перебор элементов» (см. п. 1.2.3). Реализация осуществляет перебор всех элементов хранилища, для каждого элемента выполняется соответствующий вызов. Метод реализован в виде шаблона, что позволяет его использование для различных типов объектов. Входным параметром метода выступает объект вызова, объявленный как ссылка на r-value. Такое объявление позволяет передавать выражения или временные копии объектов.
6.2.6. Асинхронные запросы
Для реализации асинхронных запросов объявляется очередь, в которую помещаются все поступающие запросы. Обработка очереди происходит в отдельном потоке. Поток извлекает очередной запрос и для него выполняет обратный вызов. Объявление класса для выполнения асинхронных вызовов приведено в Листинг 93.
Листинг 93. Класс для выполнения асинхронных вызовов (CommandQueue.h)class CommandQueue
{
public:
void start(); // (1)
void stop(); // (2)
void addCommand(SensorNumber number, SensorPointer pointer, SensorValueCallback callback); // (3)
private:
struct Command // (4)
{
SensorNumber number;
SensorPointer pointer;
SensorValueCallback callback;
};
std::queue
std::condition_variable conditional_; // (6)
std::mutex mutex_; // (7)
std::thread queueThread_; // (8)
bool exit_; // (9)
void readCommand(); // (10)
};
В строке 4 объявлена структура, в которой будут храниться данные для выполнения вызова: номер датчика, указатель на класс датчика и объект вызова. В строке 5 объявлен контейнер, который будет хранить указанные структуры. В строках 6 и 7 объявлены переменные для синхронизации операций записи/чтения очереди, в строке 8 объявлен класс для запуска потока обработки очереди, в строке 9 объявлен индикатор для завершения работы потока.
В строке 1 объявлен метод, который запускает поток обработки очереди, в строке 2 объявлен метод для остановки этого потока. Метод, объявленный в строке 3, добавляет переданные данные в очередь путем создания экземпляра структуры 4 и размещения ее в контейнере 5.
Обработка очереди реализована в методе, объявленном в строке 10. Поток обработки очереди вызывает этот метод, который, в свою очередь, ожидает поступления записей и обрабатывает их. Реализация приведена в Листинг 95.
Листинг 94. Обработка очереди запросов (CommandQueue.cpp)void CommandQueue::readCommand()
{
while (!exit_) // (1)
{
std::unique_lock
conditional_.wait(lock, [this]() {return commandQueue_.size() > 0 || exit_ == true; }); // (3)
while (commandQueue_.size() > 0 && exit_ == false) // (4)
{
Command cmd = commandQueue_.front(); // (5)
commandQueue_.pop(); // (6)
lock.unlock(); // (7)
cmd.callback(cmd.number, cmd.pointer->getValue()); // (8)
lock.lock(); // (9)
}
}
}
Пока не установлен индикатор завершения (устанавливается в методе stop), выполняется цикл 1. Вначале блокируется мьютекс 2 (это необходимо для корректной работы условной переменной), затем осуществляется ожидание условной переменной 3. Когда метод addCommand сформировал новую запись и добавил ее в контейнер, он инициирует срабатывание условной переменной, и поток выполнения переходит к циклу 4 (мьютекс при этом оказывается заблокирован). Этот цикл работает, пока очередь не опустеет либо будет установлен индикатор выхода.
В строке 5 из контейнера извлекается очередная запись, в строке 6 эта запись удаляется из контейнера. В строке 7 снимается блокировка мьютекса, что позволяет добавлять в контейнер новые записи, пока идет обработка очередной команды. В строке 8 осуществляется обратный вызов, в строке 9 мьютекс блокируется вновь, и далее повторяется цикл 4.
6.2.7. Наблюдатель
Объявление класса наблюдателя приведено в Листинг 95.
Листинг 95. Наблюдатель – класс для отслеживания пороговых значений (Observer.h)class Observer
{
public:
void start(); // (1)
void stop(); // (2)
void addAlert(SensorNumber number, SensorPointer pointer, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс); // (3)
void deleteAlert(SensorNumber number); // (4)
private:
struct Alert // (5)
{
Alert() {}
Alert(SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, SensorPointer sensor, CheckAlertTimeout checkTimeout):
callback(callback), alertValue(alertValue), alertRule(alertRule), sensor(sensor), checkTimeout(checkTimeout), currentTimeout(0)
{
}
SensorAlertCallback callback;
SensorValue alertValue;
AlertRule alertRule;
SensorPointer sensor;
CheckAlertTimeout checkTimeout;
CheckAlertTimeout currentTimeout;
};
std::map
std::thread pollThread_; // (7)
bool exit_; // (8)
std::mutex mutex_; // (9)
void poll(); // (10)
};
В строке 1 объявлен метод для запуска процесса отслеживания пороговых значений, в строке 2 – метод для останова. Метод в строке 3 добавляет датчик для отслеживания, метод 4 – удаляет.
В строке 5 объявлена структура, в которой хранятся данные, необходимые для отслеживания показаний датчика. В строке 6 объявлен контейнер для хранения указанных структур; метод addAlert добавляет запись в контейнер, метод deleteAlert удаляет ее. В строке 7 объявлен класс для запуска потока для отслеживания, в строке 8 объявлен индикатор выхода, в строке 9 объявлен мьютекс для синхронизации.
Отслеживание показаний реализовано в методе, объявленном в строке 10. Поток отслеживания вызывает этот метод, который циклически опрашивает назначенные датчики и в случае превышения пороговых значений осуществляет обратный вызов. Реализация приведена в Листинг 96.
Листинг 96. Отслеживание пороговых значенийvoid Observer::poll()
{
using namespace std::chrono_literals;
while (!exit_) // (1)
{
std::this_thread::sleep_for(1s); // (2)
std::lock_guard
for (auto& item : containerAlert) // (4)
{
Alert& alert = item.second;
alert.currentTimeout++; // (5)
if (alert.checkTimeout != 0 && alert.currentTimeout >= alert.checkTimeout) // (6)
{
bool triggerAlert = false;
if (alert.alertRule == AlertRule::More) // (7)
{
triggerAlert = alert.sensor->getValue() > alert.alertValue;
}
else // (8)
{
triggerAlert = alert.sensor->getValue() < alert.alertValue;
}
if (triggerAlert) // (9)
{
alert.checkTimeout = alert.callback(item.first, alert.alertValue); // (10)
}
alert.currentTimeout = 0; // (11)
}
}
}
}
В строке 1 объявлен цикл опроса, который выполняется, пока не выставлен индикатор завершения (выставляется в методе stop). В строке 2 поток засыпает на 1 секунду, т. е. интервал опроса равен 1 секунде. В строке 3 блокируется мьютекс, чтобы избежать коллизий добавления/удаления элементов в контейнере.
В строке 4 осуществляется опрос элементов, хранящихся в контейнере. Текущее время опроса в строке 5 увеличивается на единицу. Если уведомление разрешено, о чем говорит ненулевое значение timeout, и время последнего опроса превысило назначенное время (строка 6), то тогда проверяется, имелось ли превышение пороговых значений в соответствии с назначенными правилами (строки 6, 7). Если превышение зафиксировано (строка 9), то осуществляется обратный вызов (строка 10). Этот вызов возвращает следующий интервал опроса, после чего текущее время сбрасывается (строка 11).
6.2.8. Интерфейсный класс
Класс, объявляющий интерфейс для взаимодействия с приложением, представлен в Листинг 97.
Листинг 97. Интерфейсный класс (ControlInterface.h)namespace sensor
{
class ISensorControl
{
public:
virtual ~ ISensorControl () = default;
virtual void initialize() = 0; // (1)
virtual void shutDown() = 0; // (2)
virtual void assignDriver(DriverPointer driver) = 0; // (3)
virtual DriverPointer getAssignedDriver() = 0; // (4)
virtual DriverPointer getSensorDriver(SensorNumber number) = 0; // (5)
virtual void addSensor(SensorType type, SensorNumber number) = 0; // (6)
virtual void deleteSensor(SensorNumber number) = 0; // (7)
virtual bool isSensorExist(SensorNumber number) = 0; // (8)
virtual bool isSensorOperable(SensorNumber number) = 0; // (9)
virtual SensorValue getSensorValue(SensorNumber number) = 0; // (10)
virtual void querySensorValue(SensorNumber number, SensorValueCallback callback) = 0; // (11)
virtual void readSensorValues(SensorValueCallback callback) = 0; // (12)
virtual SensorValue getMinValue(SensorNumber first, SensorNumber last) = 0; // (13)
virtual SensorValue getMaxValue(SensorNumber first, SensorNumber last) = 0; // (14)
virtual void setAlert(SensorNumber number, SensorAlertCallback callback, SensorValue alertValue, AlertRule alertRule, CheckAlertTimeout checkTimeoutSeс = 1) = 0; // (15)
virtual void resetAlert(SensorNumber number) = 0; // (16)
static ISensorControl* createControl(); // (17)
};
};
В строке 1 и 2 объявлены методы для запуска и останова. В строках 3 и 4 объявлены методы для назначения и получения драйвера. Этот драйвер должен быть создан и назначен в самом начале работы, поскольку он будет передаваться новым создаваемым датчикам. Узнать назначенный драйвер для соответствующего датчика можно в методе 5.
В строках 6 и 7 объявлены методы для добавления и удаления датчика. В методе 8 можно проверить, существует ли датчик с переданным номером, в методе 9 можно проверить, является ли датчик работоспособным.
В строке 10 объявлен метод для чтения текущего показания датчика. В методе 11 осуществляется асинхронный запрос показания датчика, значение будет возвращаться через передаваемый обратный вызов. В строке 12 осуществляется опрос показаний всех работоспособных датчиков, значения также возвращаются через обратный вызов. С помощью методов, объявленных в строках 13 и 14, можно получить минимальное и максимальное значение для набора датчиков с номерами из указанного диапазона.
В строке 15 назначается отслеживание пороговых значений, в строке 16 отслеживание выключается. С помощью метода, объявленного в строке 17, можно создать экземпляр соответствующего интерфейсного класса.
Класс, реализующий интерфейс, приведен в Листинг 98.
Листинг 98 Класс, реализующий интерфейс (SensorControl.h)namespace sensor
{
class ISensor;
class IDriver;
class CommandQueue;
class AlertControl;
class SensorContainer;
class SensorControl: public ISensorControl
{
public:
SensorControl();
~SensorControl();
void initialize() override;
/* Other Interface methods – they are not displayed here*/
private:
SensorContainer* sensorContainer_; // (1)
CommandQueue* commandQueue_; // (2)
AlertControl* alertControl_; // (3)
bool isInitialized_; // (4)
DriverPointer driver_; // (5)
void checkInitialize(); // (6)
void checkDriver(); // (7)
};
}; //namespace sensor
В строке 1 объявлен контейнер для хранения датчиков, в строке 2 – класс для выполнения асинхронных запросов, в строке 3 – класс для отслеживания пороговых значений. Соответствующие указатели создаются в конструкторе и уничтожаются в деструкторе. Индикатор 4 указывает, была ли выполнена инициализация.
В строке 6 объявлен вспомогательный метод, который проверяет, была ли выполнена инициализация (если нет, выбрасывает исключение). В строке 7 аналогичный метод проверяет, был ли установлен драйвер.
Рассмотрим, как здесь используются обратные вызовы. Для начала самый простой случай – чтение показаний работоспособных датчиков (Листинг 99).
Листинг 99. Обратные вызовы в классе, реализующем интерфейс (SensorControl.cpp)void SensorControl::readSensorValues(SensorValueCallback callback)
{
checkInitialize(); // (1)
sensorContainer_->forEachSensor([callback](SensorNumber number, SensorPointer sensor) // (2)
{
if (sensor->isOperable()) // (3)
{
callback(number, sensor->getValue()); // (4)
}
}
);
}
В строке 1 производится проверка, инициализирован ли класс. Если класс не проинициализирован, то функция выбросит исключение.
В строке 2 происходит перебор элементов контейнера, в качестве обратного вызова используется лямбда-выражение. Контейнер будет вызывать лямбда-выражение, в которое он будет передавать номер датчика и указатель на экземпляр класса. В теле выражения проверяется, является ли датчик работоспособным (строка 3), и если да, то выполняется соответствующий обратный вызов (строка 4).
Рассмотрим теперь поиск максимального и минимального значения для заданного диапазона номеров датчиков. Вначале разработаем вспомогательный класс, который будет последовательно принимать на вход показания датчиков и искать среди них максимальное и минимальное значение (Листинг 100).
Листинг 100. Класс для анализа минимального и максимального значения (SensorControl.cpp)class FindMinMaxValue
{
public:
enum MinMaxSign { MIN_VALUE = 0, MAX_VALUE = 1 }; // (1)
FindMinMaxValue(SensorNumber first, SensorNumber last, MinMaxSign sign) : // (2)
sign_(sign), first_(first), last_(last), count_(0)
{
if (sign == MIN_VALUE)
{
result_ = std::numeric_limits
}
else
{
result_ = std::numeric_limits
}
arrayFunMinMax_[MIN_VALUE] = &FindMinMaxValue::CompareMin; // (5)
arrayFunMinMax_[MAX_VALUE] = &FindMinMaxValue::CompareMax; // (6)
}
void operator()(SensorNumber number, SensorPointer sensor) // (7)
{
if ( sensor->isOperable() && (number >= first_ && number <= last_) ) // (8)
{
(this->*arrayFunMinMax_[sign_])(sensor->getValue()); // (9)
count_++; // (10)
}
}
SensorValue result() { return result_; } // (11)
size_t count() { return count_; } // (12)
private:
SensorNumber first; // (13)
SensorNumber last; // (14)
MinMaxSign sign; // (15)
SensorValue result; // (16)
size_t count; // (17)
using FunMinMax = void (FindMinMaxValue::*)(SensorValue value); // (18)
void CompareMin(SensorValue value) // (19)
{
if (result_ > value)
{
result_ = value;
}
}
void CompareMax(SensorValue value) // (20)
{
if (result_ < value)
{
result_ = value;
}
}
FunMinMax arrayFunMinMax_[2]; // (21)
};
В строке 2 объявлен конструктор, который принимает на вход следующие параметры: минимальное значение диапазона номеров; максимальное значение диапазона номеров; параметр, указывающий, что необходим поиск минимального либо максимального значения. В конструкторе инициализируются переменные класса: минимальное значение диапазона (объявлено в строке 13); максимальное значение диапазона (объявлено в 14); параметр для поиска (объявлено в 15); итоговый результат (объявлено в 16); количество датчиков, которые участвовали в поиске (объявлено в 17). В зависимости от переданного параметра начальный результат инициализируется соответственно максимальным либо минимальным значением (строки 3 и 4). Кроме того, инициализируется массив указателей на функцию (строки 5 и 6, объявление в 21). Данные функции предназначены для сравнения и запоминания максимального либо минимального значений (объявлены в 19 и 20).
Анализ очередного значения происходит в перегруженном операторе 7. На вход подаются номер датчика и указатель на датчик. Если датчик работоспособный и его номер попадает в заданный диапазон номеров (строка 8), то в зависимости от параметра поиска через указатель вызывается соответствующая функция для анализа (строка 9), а также увеличивается счетчик просмотренных датчиков (строка 10). Функции 11 и 12 возвращают итоговые результаты.