Каждый C++-разработчик хотя бы слышал о Boost – это, пожалуй, наиболее распространенный набор внешних библиотек, используемый в мире C++. Истоки большинства стандартных библиотек восходят к Boost, поскольку многие разработчики Boost также входят в состав комитета по стандартам C++ и именно они определяют, в каком направлении будет развиваться язык – поэтому можете считать Boost своеобразным дорожным указателем. Возвращаясь к заголовку этой статьи - 'Boost' содержит много популярного функционала, вспомогательных библиотек, так, что, если вы столкнулись с какой-нибудь распространенной проблемой – первым делом обращайтесь к Boost, так как велики шансы, что там для вас найдется готовое решение.
Скажу еще несколько слов о синергии между Boost и стандартом C++. Большинство библиотек std – в частности, контейнеры, умные указатели, поддержка многопоточности, регулярные выражения, поддержка файловой системы, кортежи, варианты и многие другие – как правило, портированы из Boost. Этот тренд продолжится, но, поскольку в Boost такое множество разноцелевых библиотек, сейчас не для всех из них найдется место в стандарте, так как они слишком специализированные, зависят от контекста или просто не настолько популярны, чтобы переносить их в сам язык. В этой статье я постараюсь рассказать о некотором подобном функционале, сосредоточившись на тех возможностях, которые пока не входят в стандарт. Я покажу вам некоторые вещи, которые нахожу полезными – и вам, надеюсь, они тоже понравятся.
Контейнеры
Начнем с рассмотрения контейнеров, предлагаемых в Boost, но пока отсутствующих в stl
(и которым даже не светит туда попасть). Стоит отметить, что со времен C++11 многие из контейнеров Boost уже портированы. Теперь у нас есть нечто вроде std::unordered_set
, std::unordered_map
с их неуникальными версиями, которые реализованы на таблицах хеширования, std::array
, обертке для массива std::forward_list
на чистом С. Название достаточно прозрачное, а в C++20 мы получили std::span
, который фактически является классом для представления памяти. Первые реализации всех этих новых типов контейнеров появились в Boost и широко использовались до того, как добрались до 'stl'. Теперь давайте рассмотрим еще некоторые очень полезные, но не настолько универсальные контейнеры, оставшиеся в Boost
.
Векторные типы
Вероятно, из всех типов контейнеров в мире C++ чаще всего используется std::vector
. Он предлагает непрерывную динамическую память, в которой может храниться неопределенное количество объектов. Но у такого подхода есть некоторые недостатки: например, добавление новых данных в хранилище будет приводить к тому, что блоки памяти станут выделяться заново. При многочисленных операциях выделения это может серьезно ударить по производительности. Теперь представьте, что мы будем хранить некоторое определенное количество объектов, достаточно небольшое. В таком случае «плата» за выделение и повторное выделение памяти кажется расточительной – ведь мы уже с большей или меньшей уверенностью сможем спрогнозировать, сколько памяти нам понадобится. На такой случай в качестве решения может пригодиться boost::container::small_vector
, и вот как вы можете инициализировать такой контейнер:
boost::container::small_vector<int, 5=""> boostSmallVector;
boost::container::small_vector
– отличный выбор на такой случай. Второй аргумент шаблона указывает, сколько объектов может храниться в массиве на стеке, без какого-либо динамического выделения. Похожая структура достижима при помощи std::array
, но во втором случае возникает два крупных недостатка. Первый заключается в том, что boost::container::small_vector
на самом деле позволяет добавить больше элементов, чем вы указали. В таком случае он выделяет блок динамической памяти и копирует все элементы туда. Такого варианта следует избегать или считать, что он допустим изредка, поскольку статический массив является членом класса, и эта память будет расходоваться зря. Другое преимущество boost::container::small_vector
над std::array
в том, что у него векторный интерфейс, и он позиционируется как динамический контейнер. Благодаря этому, вы можете с легкостью выяснять, сколько именно объектов на самом деле было инициализировано, и в итоге не получаете какого-либо неопределенного поведения или аварийного завершения, попытавшись управлять ими самостоятельно. boost::container::small_vector обычно может заменить std::vector
в имеющемся коде, для этого нужно просто изменить тип переменной. Другой контейнер, похожий на boost::container::small_vector
– это boost::container::static_vector
, и определяется он примерно так же:
boost::container::static_vector<int, 5=""> boostStaticVector;
Вся разница в том, что он никогда не выделяет динамическую память. Когда вы добавляете элемент сверх его проходимости, он выбрасывает исключение. Поэтому им следует пользоваться, когда элементов не может быть больше, чем вы указали, а также существует потребность в вектороподобном интерфейсе.
Кольцевой буфер
Еще один пример вспомогательного контейнера из Boost
– это boost::circular_buffer
. Всякий раз, когда вы хотите создать какой-нибудь простой фреймворк для логирования, или, может быть, ваше приложение собирает какую-нибудь статистику, и в нем нужно хранить фиксированное количество самых новых записей, то кольцевой буфер – то, что нужно. Boost дает нам простое, но разностороннее решение, boost::circular_buffer
. Он совместим с интерфейсом контейнеров STL, поэтому считайте его предпочтительнее вашего собственного класса, ведь с таким буфером дальнейшая интеграция с существующим кодом будет гораздо проще.
boost::circular_buffer<int> circular_buffer(3);
circular_buffer.push_back(1);
circular_buffer.push_back(2);
circular_buffer.push_back(3);
std::cout << circular_buffer[0] << ' ' << circular_buffer[1] << ' ' << circular_buffer[2] << '\n';
circular_buffer.push_back(4);
std::cout << circular_buffer[0] << ' ' << circular_buffer[1] << ' ' << circular_buffer[2] << '\n';
В вышеприведенном примере мы добавляем в кольцевой буфер три элемента и выводим их на экран. Вывод должен выглядеть так: 1 2 3. После того, как мы добавим значение 4, но буфер уже заполнен, поэтому 1 удаляется из него, и мы видим 2 3 4.
Битовый массив
Есть и еще один очень интересный, хотя и немного странный контейнер: boost::bimap
. Он придется кстати, когда требуется искать словарь не только по его ключам, но и по значениям. Чтобы вообразить, что такое битовый массив, и как он работает, можете представить себе два классических словаря, так, что в обоих хранятся одни и те же объекты, но ключи одного – это значения другого, и наоборот. Эта взаимосвязь отлично проиллюстрирована в официальной документации.
Теперь поближе к практике. Левая и правая стороны boost::bimap
– это пара множеств с типами, указываемыми пользователем. Благодаря этому мы можем создать очень специализированный контейнер, алгоритмическая сложность у которого с обеих сторон разная. Так, например, если вам нужна уникальная неупорядоченная хеш-таблица слева и упорядоченная неуникальная древовидная структура справа, то можно выбрать unordered_set_of
и multiset_of
соответственно. Теперь давайте попытаемся создать такой контейнер.
#include <boost bimap.hpp="">
#include <boost bimap="" multiset_of.hpp="">
#include <boost bimap="" unordered_set_of.hpp="">
#include <string>
#include <iostream>
int main()
{
using BoostBimap = boost::bimap<boost::bimaps::unordered_set_of<std::string>, boost::bimaps::multiset_of<int>>;
BoostBimap exampleBimap;
exampleBimap.insert({"a", 1});
exampleBimap.insert({"b", 2});
exampleBimap.insert({"c", 3});
exampleBimap.insert({"d", 1});
auto range = exampleBimap.right.equal_range(1);
for (auto it = range.first; it != range.second; ++it)
{
std::cout << it->second;
}
}
Вставляем четыре пары из строк и целых чисел. Целые числа хранятся в мультимножестве в правой части карты битов, поэтому попытаемся добавить значение 1 дважды. Вывод этого фрагмента кода будет ad
, так как и a
, и d
спарены с 1
.
boost::bimap
– отличный способ хранить зависимые пары переменных, доступ к которым должен предоставляться с обеих сторон. В данном случае мы можем определять оптимальные структуры данных для нашего случая использования, а битовый массив отвечает за их реализацию. Это один из самых изощренных и широко доступных контейнеров, который может быть очень мощным, если его использовать рационально.
Токенизатор
Разделение строк – это задача, о которую время от времени спотыкается любой разработчик, а boost::tokenizer
предоставляет нам сложное и эффективное решение для этой цели. Можно несколькими способами определить критерии разделения, начиная от простых разделителей символов до метасимволов, смещений и пр. Теперь давайте заглянем в следующий код.
#include <boost tokenizer.hpp="">
#include <string>
#include <iostream>
int main()
{
using Tokenizer = boost::tokenizer<boost::char_separator<char>>;
std::string testString = "String for tokenizer test, C++";
boost::char_separator<char> separator(", ", "+", boost::drop_empty_tokens);
Tokenizer tokenizer(testString, separator);
for (auto tokenIt = tokenizer.begin(); tokenIt != tokenizer.end(); ++tokenIt)
std::cout << *tokenIt << '_';
}
В вышеприведенном примере мы создаем объект-разделитель, который будет определять поведение токенизатора. Первый аргумент ", " использует в качестве разделительных знаков запятые и пробелы, а из окончательного вывода их удаляет. Немного иная ситуация складывается со вторым аргументом "+". Он также выделяет разделительный знак, но мы оставим те, что указаны здесь. Последнее, что мы в нем задали – отбросить пустые токены, поскольку в этом примере они нам не нужны. Окончательный вывод должен выглядеть как String_for_tokenizer_test_C_+_+_
, все токены разделены нижними подчеркиваниями и, как мы видим, запятые и пробелы удалены.
Теперь давайте изменим этот код для другого очень распространенного случая, то есть, для токенизации формата csv. Это проще простого, так как boost::tokenizer
это уже поддерживает. Все, что от нас требуется – заменить boost::char_separator
на boost::escaped_list_separator
. В нем поля по умолчанию разделяются запятыми, причем, различаются случаи, в которых разделяются сами поля, и случаи, когда разделяются части полей.
#include <boost tokenizer.hpp="">
#include <string>
#include <iostream>
int main()
{
using Tokenizer = boost::tokenizer<boost::escaped_list_separator<char>>;
std::string testString = "Name,\"Surname\",Age,\"Street,\"Number\",Postal Code,City\"";
Tokenizer tokenizer{testString};
for (auto tokenIt = tokenizer.begin(); tokenIt != tokenizer.end(); ++tokenIt)
std::cout << *tokenIt << '_';
}
Вывод вышеприведенной программы выглядит так: Name_Surname_Age_Street,Number,Postal Code,City_
Часть, содержащая адрес, была интерпретирована как одно поле, как и планировалось, причем, с помощью обычного boost::char_separator
мы бы этого не сделали.
Сетевая поддержка Boost Asio
Boost Asio
(что означает «асинхронный ввод/вывод») – это библиотека, в которой нам предоставляется фреймворк для асинхронной обработки задач. Она часто используется, когда у вас на руках есть функции, на выполнение которых требуется много времени – обычно это функции, обращающиеся к внешним ресурсам. Но в этом разделе мы не будем говорить об очередях задач, асинхронной обработке, таймерах и тому подобном. Мы сосредоточимся на одном из внешних сервисов, который напрямую поддерживается Boost Asio
– речь о сетевом взаимодействии. Большое преимущество этого фреймворка в том, что он позволяет писать кроссплатформенные сетевые функции, поэтому вам больше не требуется писать иную реализацию для каждой из ваших целевых систем. У него есть собственная сокетная реализация с поддержкой протоколов транспортного уровня, в частности, TCP
, UDP
, ICMP
, а также шифрования SSL/TLS
. Теперь давайте напишем пример, который выполнит безопасное рукопожатие по TCP.
#include <iostream>
#include <boost asio.hpp="">
#include <boost asio="" ssl.hpp="">
int main(int argc, char* argv[])
{
boost::asio::io_context io_context;
boost::asio::ssl::context ssl_context(boost::asio::ssl::context::tls);
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(io_context, ssl_context);
boost::asio::ip::tcp::resolver resolver(io_context);
auto endpoint = resolver.resolve("google.com", "443");
boost::asio::connect(socket.next_layer(), endpoint);
socket.async_handshake(boost::asio::ssl::stream_base::client, [&] (boost::system::error_code error_code)
{
std::cout << "Handshake completed, error code (success = 0) " << error_code << std::endl;
});
}
Итак, когда у вас есть установленное соединение, можно вызвать boost::asio::write
и boost::asio::read
для коммуникации с сервером. Если у вас уже есть опыт работы со, скажем, сокетами POSIX
, то вы быстро уловите, как делаются дела в Boost Asio
.
Заключение
Целью этой статьи было представить некоторые библиотеки и возможности Boost, которые мне когда-либо пригодились. На самом деле, в Boost есть уже очень много вещей, а в каждом релизе добавляются новые. Например, в версии 1.73, новейшей на момент написания статьи, появилась сериализация JSON
и легковесный фреймворк для обработки ошибок, он LEAF
. Рекомендую вам самостоятельно следить за нововведениями, поскольку они могут сэкономить вам массу времени на разработку функциональности, которая уже может присутствовать в Boost
.