Типобезопасные обратные вызовы в C++

ОГЛАВЛЕНИЕ

В данной статье представлен класс, который добавляет типобезопасные обратные вызовы C++ в проекты.

•    Скачать исходники - 32.39 Кб
•    Скачать документацию - 33.08 Кб

Обсуждаемый класс добавляет типобезопасные обратные вызовы C++ в проекты. Его свойства заключаются в следующем:
•    Любую функцию в любом классе можно вызвать откуда угодно в любом другом классе.
•    Можно передать от 0 до 5 аргументов любого типа функции обратного вызова и задать любой тип возвращаемой переменной.
•    Обратный вызов можно передать в качестве аргумента любой функции.
•    Оптимизирован для высокой скорости.
•    Размер кода менее 1 Кб, не нужны дополнительные библиотеки.
•    Не зависит от платформы: работает на Windows, Linux, Mac, и т.д.
•    Был испытан в Visual Studio 6.0, 7.0, 7.1 и 8.0 (= Visual Studio 6, вплоть до .NET 2005).
•    Новое в версии 3.0 (октябрь 2007): класс также поддерживает обратные вызовы статических функций и функций внутри виртуально производных классов.

Введение

В C++ простого адреса функции недостаточно для определения обратного вызова, как в старом C. В C++ каждый экземпляр класса хранит переменные класса в своей собственной области памяти. Указатель this указывает на эту область переменных. При каждом вызове любой функции C++ указатель this невидимо передается функции и в дополнение к аргументам функции. Microsoft Visual Studio 6 использует регистр процессора ECX для передачи указателя this, тогда как нормальные аргументы функции проталкиваются в стек. Чтобы использовать обратные вызовы, скопируйте файлы Callback.h и PreProcessor.h в проект и #include "Callback.h".

Зачем использовать обратные вызовы?

Допустим, написан планировщик, реагирующий на определенные события путем выполнения соответствующих действий. События могут быть любыми, например, прибытие данных, ввод данных пользователем или таймеры, а действия являются выполнением любого кода. Планировщик содержит в дополнительном потоке бесконечный цикл, ждущий события. В Windows можно использовать функцию API WaitForMultipleObjects(), возвращающую индекс сигнализированного события. Планировщик выглядит следующим образом:

Пример кода, сопровождающий данную статью, использует обратные вызовы для сортировки списка разными функциями сортировки обратного вызова. Заметьте, что благодаря сигналам и слотам не нужно писать собственную функцию RegisterCallback()! Более подробно читайте в части 2 данной серии статей.

Определение обратных вызовов

В callback.h есть два класса: cCall и cCallGen. Происходящее внутри этих классов весьма сложно, поэтому здесь объясняется только применение. cCall определяется как:

cCall <_ReturnType, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5>

Можно использовать функции обратного вызова, принимающие 0, 1, 2, 3, 4 или 5 аргументов. Если нужно больше 5 аргументов, классы обратного вызова с легкостью расширяются. Однако рекомендуется передавать более 5 аргументов в виде структуры, чтобы код легче читался.

Допустим, надо осуществить обратный вызов следующей функции:

int cMyApplication::Calculate(float Factor, bool Flag, char* Name)
{
    ......
}

Для этой функции можно создать соответствующий обратный вызов:

cCall <int, float, bool, char*> i_CallbackCalculate;

Первый аргумент шаблона (int) всегда используется для определения типа возвращаемой переменной. Следующие аргументы шаблона (float, bool, char*) являются типами аргументов, передаваемых функции обратного вызова.

Чтобы определить обратный вызов для следующей функции без аргументов, возвращающей void,

void cMyApplication::Print()
{
    ......
}

надо написать:

cCall <void> i_CallbackPrint;

Присваивание обратных вызовов

Обратные вызовы могут присваиваться друг другу с помощью operator=, но из-за типобезопасности этих обратных вызовов следующее приводит к ошибке компилятора:

i_CallbackCalculate = i_CallbackPrint;  // ошибка
i_CallbackCalculate типа int function(float, bool, char*) нельзя присвоить i_CallbackPrint типа void function(void).

Генерация обратных вызовов

i_CallbackCalculate и i_CallbackPrint нельзя было использовать до сих пор, потому что они еще не были инициализированы, то есть им не была присвоена никакая функция. Если вызвать i_CallbackCalculate.Valid(), результатом будет false, а значит, нельзя использовать обратный вызов. Если попытаться (i_CallbackCalculate.Execute(...)), программа аварийно завершится посредством утверждения. Чтобы сгенерировать инициализированные обратные вызовы для вышеназванных функций, надо использовать cCallGen. cCallGen (генератор обратного вызова) унаследован от cCall и применяется всего один раз для инициализации. cCallGen определяется как cCallGen <_Class, _ReturnType, _Arg1, _Arg2, _Arg3, _Arg4, _Arg5>.

cCallGen <cMyApplication, int, float, bool, char*> i_CallGenCalculate(this, Calculate);
// и
cCallGen <cMyApplication, void> i_CallGenPrint(this, Print);

Видно, что cCallGen принимает на один аргумент шаблона больше, чем cCall. Первый тип (cMyApplication) – класс, содержащий функцию обратного вызова; остальное точно такое же, как и у cCall. Параметрами, передаваемыми конструктору, являются указатель this класса, содержащего функцию обратного вызова, и сама функция обратного вызова (Calculate, Print). Теперь ясно, зачем нужны два класса обратного вызова. cCallGen может использоваться только внутри класса, содержащего функцию обратного вызова (здесь cMyApplication), потому что ему требуется указатель this. cCall можно создать в любом классе, и позже ему можно присвоить совместимый обратный вызов, переданный откуда угодно:

i_CallbackCalculate = i_CallGenCalculate;
i_CallbackPrint     = i_CallGenPrint;

Такое присваивание возможно, потому что cCallGen унаследован от cCall. Теперь можно выполнить обратный вызов, написав:

int Result = i_CallbackCalculate.Execute(3.448, true, "Hello world");
// и
i_CallbackPrint.Execute();

Если функции обратного вызова статические, используется cCallGenS вместо cCallGen.

Пример использования

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

Список становится очень гибким, потому что вызывающий оператор может влиять на поведение сортировки, не манипулируя кодом в cList. Это сущность C++. Можно сделать то же самое путем наследования от cList и переписывания функции Compare. Однако обратные вызовы позволяют динамически менять поведение списка при выполнении путем вызова SetCallback(...) всегда, когда нужно.

Обзор интерфейса

cCall    cCallGen (для обратных вызовов членов и виртуальных обратных вызовов)    cCallGenS (для статических обратных вызовов)

Конструктор:

cCall <tRet [,tArg1,,,tArg5]> MyCall()    Конструктор:
cCallGen <cClass, tRet [,tArg1,,,tArg5]> MyCallGen(Instance, Function)    Конструктор:
cCallGenS <tRet [,tArg1,,,tArg5]> MyCallGen(Function)
cCall& operator=(cCall &ExtCall)
cCall& operator=(cCallGen &ExtCallGen)    -    -
bool Valid()    -    -
tRet Execute([tArg1, tArg2, tArg3, tArg4, tArg5])    -    -

Чтобы увидеть, как все это работает вместе, скачайте простой пример кода.

Ошибки

Выдаются ошибки компилятора при попытке присвоить обратный вызов несовместимому обратному вызову (с разными аргументами или типом возвращаемой переменной). Выдаются ошибки времени исполнения (утверждение) при передаче неверного количества аргументов в i_Callback.Execute(...) или при попытке исполнить неинициализированный обратный вызов.

Boost

Preprocessor.h был извлечен из Boost::function. Boost - огромная платформенно-независимая библиотека C++ (35 Мб), использующая массу хитростей, чтобы заставить компилятор делать по идее невозможные вещи. Так как все компиляторы ведут себя по-разному и имеют отличные ограничения, Boost содержит тысячи операторов выбора, директив #define и typedef, чтобы обеспечить свою работу во всех компиляторах. В Boost есть много интересных вещей, но имеются недостатки:

•    Boost огромна (35 Мб).
•    Замедляет процесс компиляции.
•    Иногда вызывает фатальный сбой компьютера, требующий перезагрузки.
•    Почти невозможно понять очень загадочный код в 100 заголовочных файлах, используемых для Boost::function.

Разработанный класс обратного вызова намного удобней, чем Boost::function.

QT

QT – еще одна библиотека, о которой надо знать. QT – огромная платформенно-независимая библиотека графического пользовательского интерфейса(GUI). Однако QT интересна, даже если программировать GUI не нужно, потому что она предлагает платформенно-независимые функции для доступа к файлам, таймеры, и т.д. QT обходит ограничения в компиляторе и даже расширяет сам язык C++ путем встраивания дополнительного препроцессора в компилятор. Это обеспечивает новый функционал, такой как более легкая поддержка нескольких языков, информация о типах в процессе исполнения, защищенные указатели, более разборчивый код и сигналы и слоты, являющиеся улучшенными обратными вызовами (смотрите часть 2 этой серии статей).

Увы, у QT также есть минусы:
•    Она стоит $1500 (за исключением разработки свободного программного обеспечения для Linux).
•    Она огромная (20 Мб исходников).
•    Она замедляет процесс компиляции.
•    Программы, написанные с помощью QT, вынуждены предоставлять несколько дополнительных динамических библиотек с полным размером 6 Мб.
•    Сигналы и слоты нельзя использовать для возврата значения вызывающей функции.
Поэтому, если вас не устраивают большие библиотеки типа Boost или QT, этот крохотный класс обратного вызова от ElmueSoft точно не помешает.

Во второй части мы обсудим типобезопасные сигналы и слоты C++ (события и делегаты). Прочитайте вторую часть, основанную на данной статье и реализующую очень удобную систему сигнал / слот (событие / делегат) на C++, не требуя никакой большой библиотеки!
•    Сигнал (событие), возбужденный где угодно в любом классе кода, может быть получен слотами (делегатами) в любом классе кода.
•    Можно подключить столько сигналов к слоту и столько слотов к сигналу, сколько нужно.
•    Можно передать от 0 до 5 аргументов любого типа сигналу, и можно объединить несколько возвращаемых значений из слотов во всего одно возвращаемое значение, возвращаемое вызывающей функции.
•    Если класс уничтожается, все подключенные сигналы или слоты автоматически отключаются.
•    Сигналы защищены от повторного вхождения.
•    Оптимизирован для высокой скорости.
•    Размер скомпилированного кода менее 1 Кб, не нужны никакие дополнительные библиотеки.
•    Гораздо удобней, чем Boost::signals.
•    Доступны некоторые дополнительные возможности.
•    Платформенно-независимый: работает на Windows, Linux, Mac, и т.д...
•    Был испытан в Visual Studio 6.0, 7.0, 7.1 и 8.0 (= Visual Studio 6 вплоть до .NET 2005)