Pull to refresh

Находим более качественные решения при помощи boost

Reading time9 min
Views17K
Original author: Piotr Rut

Каждый C++-разработчик хотя бы слышал о Boost – это, пожалуй, наиболее распространенный набор внешних библиотек, используемый в мире C++. Истоки большинства стандартных библиотек восходят к Boost, поскольку многие разработчики Boost также входят в состав комитета по стандартам C++ и именно они определяют, в каком направлении будет развиваться язык – поэтому можете считать Boost своеобразным дорожным указателем. Возвращаясь к заголовку этой статьи - 'Boost' содержит много популярного функционала, вспомогательных библиотек, так, что, если вы столкнулись с какой-нибудь распространенной проблемой – первым делом обращайтесь к Boost, так как велики шансы, что там для вас найдется готовое решение.

Скажу еще несколько слов о синергии между Boost и стандартом C++. Большинство библиотек std – в частности, контейнеры, умные указатели, поддержка многопоточности, регулярные выражения, поддержка файловой системы, кортежи, варианты и многие другие – как правило, портированы из Boost. Этот тренд продолжится, но, поскольку в Boost такое множество разноцелевых библиотек, сейчас не для всех из них найдется место в стандарте, так как они слишком специализированные, зависят от контекста или просто не настолько популярны, чтобы переносить их в сам язык. В этой статье я постараюсь рассказать о некотором подобном функционале, сосредоточившись на тех возможностях, которые пока не входят в стандарт. Я покажу вам некоторые вещи, которые нахожу полезными – и вам, надеюсь, они тоже понравятся.

Контейнеры

Начнем с рассмотрения контейнеров, предлагаемых в Boost, но пока отсутствующих в stl (и которым даже не светит туда попасть). Стоит отметить, что со времен C++11 многие из контейнеров Boost уже портированы. Теперь у нас есть нечто вроде std::unordered_setstd::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.

 

Tags:
Hubs:
Total votes 23: ↑15 and ↓8+7
Comments5

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия