Однажды со мной консультировался начинающий разработчик. Не помню точно, о чем шла речь (да это и не важно), но вопрос был в стиле «есть проблема – как ее решить?». Первой моей мыслью, которую я и озвучил, было – «сделай обратный вызов». Следующий, вполне ожидаемый, вопрос был «а как его реализовать?». Почти не думая, я ответил первое, что пришло в голову – «используй указатель на функцию». «Хорошо», сказал разработчик, «я почитаю про эти указатели». Через какое-то время он снова пришел с вопросом – «ну, что такое указатель на функцию, я понял, но как внутри функции узнать, какому классу предназначается вызов?» Так, слово за слово, вопрос за вопросом, и я вдруг начинаю осознавать, что вопросы совсем не такие уж простые, как вначале могло показаться, и что одно понятие тянет за собой другое, что есть множество альтернатив при выборе способа реализации, и что так сразу и не скажешь, какой из них лучше подходит именно для вот этого случая… Так и родилась идея книги, которую вы сейчас держите перед глазами.
Формат представления информации в виде книги имеет одно неоспоримое преимущество: здесь отсутствуют ограничения по объему. Появляется возможность изложить весь материал обстоятельно, подробно, в деталях, охватывая множество аспектов и нюансов. Это выгодно отличает книгу от других форматов, таких, как статьи, лекции, презентации и т. п. В них всегда приходится идти на компромиссы, выделяя главное и отбрасывая детали, которые, на первый взгляд, кажутся несущественными, но их наличие существенно облегчает освоение материала и избавляет читателя от необходимости самостоятельно искать ответы на вопросы, которые неизбежно возникают при изучении незнакомых предметов.
Сами по себе обратные вызовы является узкоспециализированной темой, однако при этом они охватывают ряд смежных концепций как в сфере использования языка программирования, так и в сфере архитектурно-проектных решений. В связи с этим, изучение обратных вызовов значительно повышает компетенции специалиста и обогащает его арсенал приемов и способов решения нетривиальных задач.
В первую очередь книга предназначена для разработчиков среднего (middle) уровня, т. е. тех, кто уже достаточно хорошо знает язык C++, но хотел бы расширить и углубить свои знания в области проектирования и дизайна. Безусловно, не лишней она будет и для начинающих, но нужно быть готовым к тому, что для изучения материала придется приложить значительные усилия: рассматриваемые концепции являются достаточно сложными и предполагают хорошее знание синтаксиса C++, а также некоторый опыт в программировании. Надеюсь, опытные разработчики также найдут книгу полезной как в плане систематизации знаний, так и в плане новых идей и методов, которые можно использовать в практике разработки.
Структурно книга состоит из разделов, глав и параграфов. В первом разделе излагаются теоретические основы, даются определения и термины. Во втором разделе рассматриваются способы реализации обратных вызовов в языке C++. В третьем разделе проводится сравнительный анализ реализаций, вырабатываются рекомендации для выбора в конкретных случаях. В четвертом разделе рассматривается использование шаблонов – пожалуй, наиболее интересной концепции C++, активно развивающейся в новых стандартах. И в заключение, чтобы изложенный материал не показался совсем уж абстрактным и оторванным от жизни, в пятом разделе демонстрируется практическое использование обратных вызовов на примере проектирования программного компонента.
В книге иллюстрируется, как используются те или иные конструкции C++, но не раскрывается их сущность – предполагается, что читатель об этом осведомлен. Поэтому для успешного понимания материала необходимо ориентироваться в следующих темах:
• базовый синтаксис C++;
• классы и наследование, перегрузка операторов;
• лямбда-выражения и захват переменных;
• контейнеры стандартной библиотеки;
• семантика шаблонов C++;
• шаблоны с переменным числом параметров, частичная специализация шаблонов.
Теоретические положения проиллюстрированы многочисленными примерами, оформленными в виде листингов. После каждого листинга (за исключением совсем уж тривиальных случаев) идет пояснение, которое облегчает понимание кода. Примеры создавались, ориентируясь на стандарт C++ 17; некоторые из них используют специфические особенности указанного стандарта и не будут компилироваться в более ранних версиях. Исходные тексты всех примеров можно найти в https://github.com/tkachenko-vitaliy/Callbacks, там же указан адрес электронной почты для связи с автором.
Во втором издании исправлены некоторые опечатки, а также переработана глава 5.5, в которой представлены улучшенные технические решения, основываясь на новых возможностях стандарта C++ 17.
На этом вступительную часть можно считать оконченной, приступим теперь непосредственно к изучению обратных вызовов.
Представьте следующую ситуацию. Вам нужно совершить платеж в банке. Вы идете в банк, берете талон, дожидаетесь, пока вас пригласят, и совершаете платеж. Но ведь столько времени придется потратить, в банке всегда такие очереди… Есть вариант получше: попросить свою маму (или бабушку) зайти в банк и занять очередь. Когда очередь подойдет, мама (или бабушка) позвонит, и вам остается только прийти и сделать платеж. Если же вы в этот день сильно заняты, тогда можно оставить телефон друга, и он сделает платеж вместо вас.
Итак, результат один и тот же, но последовательность действий различная. В первом случае вы сами идете в банк, отстаиваете очередь и совершаете платеж, т. е. выполняете все необходимые операции. Во втором случае вы сидите и ожидаете, когда вам позвонят, т. е. сделают вызов, и делаете только одно действие, а именно – совершаете платеж. Либо это делает ваш друг, если маме (или бабушке) дали его, а не ваши контакты. Можно утверждать, что ваша мама (или бабушка) инициировала, а вы выполнили обратный вызов.
Перейдем теперь на язык программирования и дадим формальное определение.
Обратный вызов – это паттерн, в котором какой-либо исполняемый код как аргумент передается в другой код, при этом ожидается, что через сохраненный аргумент исполняемый код будет запущен в требуемый момент времени.
Возвращаясь к неформальному примеру: здесь выполнение платежа можно считать исполняемым кодом, номер телефона – аргументом, телефонный звонок – запуском кода на выполнение.
Графически описанную концепцию можно проиллюстрировать следующим образом (Рис. 1). В программе существует код, выполняющий какие-либо операции, или исполняемый код. Когда программа запускается, исполняемый код как аргумент передается в другой код, или вызывающий код. Вызывающий код сохраняет переданный аргумент и начинает работу. В нужный момент времени, используя сохраненный аргумент, вызывающий код запускает исполняемый код, т. е. осуществляет обратный вызов.
Рис. 1. Концепция обратных вызовов
Различие между прямым и обратным вызовом проиллюстрировано на Рис. 2. В первом случае поток управления запускает вызывающий код, из которого вызывается исполняемый код, и далее управление возвращается в точку вызова. Во втором случае поток управления идет мимо исполняемого кода и настраивает аргумент в вызывающем коде, а вызов исполняемого кода осуществляет уже вызывающий код, т. е. поток управления идет в обратном направлении. Таким образом, мы имеем обратный вызов.
Рис. 2. Прямой и обратный вызов
Все многообразие задач, решаемых с помощью обратных вызовов, можно разделить на следующие группы.
Представим, что мы разрабатываем программное обеспечение для микроконтроллера управления технологическими процессами. Контроллеру требуется периодически получать показания датчиков, таких как температура, влажность, давление и т. д. Как это реализовать?
Самое простое решение – код для опроса датчиков непосредственно реализовать в ПО контроллера. Но здесь возникает множество вопросов. А если в системе понадобится использовать другую модель датчика, код опроса которого должен быть другим? А если нам нужно использовать различные датчики для различных режимов? А как быть, когда мы вообще не знаем, какие датчики будут использоваться?
Эффективный способ решения указанных проблем – разработка драйвера, т. е. модуля, поддерживающего единый интерфейс вызовов для различных реализаций. Однако одно дело подать идею, а вот реализовать – тут все гораздо сложнее: интерфейс должен быть универсальным и покрывать все возможные требования; необходимо разработать механизм для загрузки нужной реализации интерфейса; требуется каким-то образом связывать интерфейс и реализацию – в итоге нам понадобится сервис поддержки драйверов. Для операционной системы это вполне оправдано, однако для микроконтроллера с его очень ограниченными ресурсами внедрение такого сервиса чревато потерей производительности как из-за большого объема кода, так и из-за дополнительного расхода памяти.
Можно предложить не такое универсальное, зато более простое и менее ресурсоемкое решение с помощью обратных вызовов(Рис. 3). Код опроса упаковывается в отдельный компонент. Перед началом работы происходит настройка, т. е. указанный код как аргумент сохраняется в рабочем коде контроллера. В нужный момент рабочий код делает обратный вызов, выполняет соответствующую функцию и получает требуемое значение. Если необходимо, в процессе работы можно изменять хранимый аргумент, изменяя, таким образом, код опроса датчиков.
Рис. 3. Опрос датчиков с помощью обратного вызова
Представим, что мы разрабатываем супербыстрый алгоритм сортировки, оптимизированный для работы на нашем многопроцессорном суперкомпьютере. Было потрачено массу усилий, реализовано много кода, и, наконец, алгоритм почти готов. Но вот незадача: мы не знаем заранее, что именно нам нужно сортировать. Сортировка чисел – это самый простой случай, а что делать, если понадобится сортировать, допустим, структуры, содержащие записи из базы данных? Пусть в структуре содержатся сведения о сотрудниках – фамилия, имя, отчество. Как реализовать сортировки по отдельным полям, по совокупности полей? Неужели придется дублировать код для каждого случая?
Простое и эффективное решение указанной проблемы представлено на Рис. 4. Код для сравнения полей упаковывается в отдельный компонент. Когда запускается алгоритм, этот компонент передается как аргумент. В требуемый момент времени алгоритм через указанный аргумент вызовет код сравнения, передавая элементы данных как параметры. Таким образом, можно реализовать различные правила сравнения и передавать их алгоритму без изменения рабочего кода.
Рис. 4. Результат вычисления с помощью обратного вызова
Представим, что мы разрабатываем модуль сетевого обмена. Как пользователю узнать, какие протоколы поддерживаются?
Самое простое решение – получить количество поддерживаемых протоколов, а затем запрашивать их имена по порядковому номеру. Данный способ легко реализуем, если внутри модуля имена протоколов хранятся в массиве. А если имена нужно хранить в списке? Тогда задача усложняется: нужно сделать перебор элементов списка, чтобы получить нужное значение по порядковому номеру. А если имена должны храниться в виде двоичного дерева?
Бесплатно
Установите приложение, чтобы читать эту книгу бесплатно
На этой странице вы можете прочитать онлайн книгу «Обратные вызовы в C++», автора Виталия Евгеньевича Ткаченко. Данная книга имеет возрастное ограничение 12+, относится к жанру «Программирование». Произведение затрагивает такие темы, как «разработка программного обеспечения», «языки программирования». Книга «Обратные вызовы в C++» была написана в 2020 и издана в 2021 году. Приятного чтения!
О проекте
О подписке