Журнал LinuxFormat - перейти на главную

LXF147:tut7

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

Boost: Набор библиотек С++

Их использование существенно облегчает написание и чтение кода, считает Семен Есилевский.


Вступление

Ни для кого не секрет, что язык С++, оставаясь самым мощным компилируемым языком общего назначения, выглядит довольно неудобным по современным меркам. Синтаксис многих конструкций очень запутан, а простые вещи зачастую реализуются неоправданно сложно. Большинство этих проблем ликвидирует набор библиотек Boost (http://www.boost.org/), призванный, как следует из названия, кардинально повысить продуктивность программирования на С++. Boost имеет «полуофициальный» статус, поскольку многие его разработчики являются членами комитета стандартов С++, а некоторые из входящих в Boost библиотек уже включены в новый стандарт С++0х. Кроме того, лицензия Boost Software License позволяет свободно и бесплатно использовать Boost как в открытых, так и в коммерческих проектах.

Библиотеки Boost – чрезвычайно мощные, используют новейшие технологии программирования (такие как шаблонное метапрограммирование) и тщательно тестируются, однако назвать их дружественными к программисту трудно. Многие библиотеки имеют излишне запутанный синтаксис при явно недостаточной документации. Особенно это относится к самым мощным, но и самым сложным библиотекам, таким как генератор парсеров Spirit. В то же время в повседневной работе чаще всего нужны относительно небольшие, простые и практичные библиотеки из набора Boost. Некоторые из этих совсем не страшных и очень полезных библиотек рассмотрены в этой статье. Все они являются «заголовочными» [header-only], поэтому не нужно добавлять к программе что-либо на стадии компоновки.

Эта статья ориентирована на читателей, имеющих некоторый опыт программирования на С++ и хотя бы поверхностно знакомых со стандартной библиотекой и контейнерами STL.

Из чисел в строки и наоборот

  • Boost.lexical_cast

Рутинная задача преобразования числа в строку или строки в число решается стандартными средствами С++ на удивление неуклюже. Можно использовать функции Си atoi() и itoa(), но они работают со строками с стиле Си, а не с объектами типа string. Можно использовать класс stringstream:

#include <sstream>
...
double v = 3.14;
string str;
// Преобразовать число в строку
stringstream ss;
ss << v;
str = ss.str();
// Преобразовать строку в число
str = “100.3”;
ss.str(ss);
ss >> v;

Если нужно провести много преобразований, то использование объекта stringstream оправданно, но в обычном сценарии вводить промежуточную переменную для конвертации одного числа неудобно. Именно для таких случаев и создана шаблонная функция lexical_cast:

#include <boost/lexical_cast.hpp>
…
double v = 3.14;
string str;
// Преобразовать число в строку
str = boost::lexical_cast<string>(v);
// Преобразовать строку в число
str = “100.3”;
v = boost::lexical_cast<double>(str);

Как говорится, проще не бывает.

Заполнение контейнеров

  • Boost.assign

Стандартные контейнеры STL имеют один досадный недостаток: чтобы наполнить их элементами, всегда приходится либо писать цикл, либо вручную вызывать push_back:

vector<int> values;
// Заполняем вектор квадратами индексов
for(int i=1; i<=5; ++i){
 values.push_back(i*i);
}
// Список слов известной фразы Гамлета
list<string> words;
words.push_back(“to”);
words.push_back(“be”);
words.push_back(“or”);
words.push_back(“not”);
words.push_back(“to”);
words.push_back(“be”);

Ничего сложного, но избыточность этого кода видна невооруженным глазом. С помощью Boost.assign можно избавиться от этой проблемы, используя перегруженный оператор “,”:

#include <boost/assign.hpp>
...
values += 1,4,9,16,25;
words += ”to”,”be”,”or”,”not”,”to”,”be”;

С ассоциативными контейнерами удобно использовать перегруженный оператор “()” и функцию insert:

map<string,int> months;
insert( months )
( ”january”, 31 )( ”february”, 28 )
( ”march”, 31 )( ”april”, 30 )
( ”may”, 31 )( ”june”, 30 )
( ”july”, 31 )( ”august”, 31 )
( ”september”, 30 )( ”october”, 31 )
( ”november”, 30 )( ”december”, 31 );

Наконец, можно инициализировать любой контейнер сразу при его объявлении с помощью функции list_of:

vector<int> vals = list_of(1)(4)(9)(16)(25);

Для ассоциативных контейнеров предусмотрен особый вариант map_list_of:

map<int,int> next = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);

В Boost.assign есть множество других полезных возможностей – например, заполнение с повторами и заполнение без избыточного копирования данных.

Имитация конструкции foreach

  • Boost.foreach

Предположим, у вас есть какой-то контейнер – например, список строк типа std::list<string>; и вы просто хотите вывести этот список на экран. «Штатное» решение выглядит так:

#include <boost/assign.hpp>
using namespace std;
list<string> lst;
...
// Выводим список
list<string>::iterator it;
for(it=lst.begin(); it!=lst.end(); it++){
 cout << *it << endl;
}

Вроде бы все хорошо, но для такой тривиальной операции приходится писать слишком много служебного кода. Итераторы – очень мощное средство, но в данном случае их применение – стрельба из пушки по воробьям. В нашем примере итератор нужен всего лишь чтобы получить значение текущего элемента контейнера. Во многих языках, таких как, например, C# или Java, есть конструкция foreach, предназначенная именно для легкого итерирования по любой последовательности или контейнеру. Макрос BOOST_FOREACH предназначен для ее эмуляции в С++:

// Выводим список
BOOST_FOREACH(string s, lst){
 cout << s << endl;
}

Согласитесь, выглядит намного понятнее и проще. Если нужно не просто читать элементы списка, но и модифицировать их, то достаточно объявить переменную цикла как ссылку:

// Список чисел
list<double> v = list_of(1)(2)(3)(4)(5);
// Превращаем его в список квадратов чисел
BOOST_FOREACH(double& d, v){
 d = d*d;
}

Макрос BOOST_FOREACH реализован так, что никогда не выделяет память динамически, а получающийся код по эффективности не уступает написанному вручную с помощью итераторов. Работает он с любыми контейнерами STL, обычными массивами, строками в стиле Си или объектами string. Можно также передать std::pair, содержащий пару любых итераторов, и макрос «пробежит» диапазон между ними. В теле цикла можно использовать обычные операторы return, continue и break:

std::deque<int> deque_int( /*...*/ );
int i = 0;
BOOST_FOREACH( i, deque_int )
{
 if( i == 0 ) return;
 if( i == 1 ) continue;
 if( i == 2 ) break;
}

Циклы могут быть вложенными на любую глубину. Можно итерировать и в обратном порядке, используя аналогичный макрос BOOST_REVERSE_FOREACH.

Начав использовать BOOST_FOREACH, отказаться от него очень сложно: количество служебного кода зачастую уменьшается в разы, а его читаемость существенно возрастает.

Функции обратного вызова

  • Boost.function и Boost.bind

Функции обратного вызова [callbacks] – очень распространенная идиома программирования. Особенно часто они используются при программировании GUI-приложений для отклика на события. Как правило, их реализуют с помощью указателей на функции. Одна беда – синтаксис указателей на функции в С++ просто устрашающий! Для иллюстрации создадим заведомо бесполезный класс, выполняющий сложение и вычитание:

class Math {
 public:
  double do_add(double a, double b){ return a+b; }
  double do_sub(double a, double b){ return a-b; }
};

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

int main(int argc, char* argv[]){
 Math m;
 double (Math::*ptr_add)(double, double) = &Math::do_add;
 double (Math::*ptr_sub)(double, double) = &Math::do_sub;
 cout << ”Сумма: ” << (m.*ptr_add)(200.0,100.0) << endl;
 cout << ”Разность: ” << (m.*ptr_sub)(200.0,100.0) << endl;
}

И как вам такой кошмарный синтаксис? ptr_add не привязан к конкретному экземпляру класса Math, и при вызове приходится явно писать m.*ptr_add. Выглядит так, как будто у объекта есть метод ptr_add; но его нет, и смысл конструкции совершенно иной. Предположим, теперь мы хотим создать функцию, которой будут передаваться два числа и указатель на метод, который к ним надо применить (т. е. собственно классический вариант обратного вызова). Понять, как это записать, очень сложно, и я даже не буду приводить этот код. Одним словом, синтаксис ужасен, а в более сложных случаях он становится вообще практически нечитабельным. На помощь приходит тандем Boost.function и Boost.bind. С их помощью наш пример становится значительно проще для понимания:

#include <boost/bind.hpp>
#include <boost/function.hpp>
double do_operation(boost::function<double(double, double)> func ,double a, double b){
 return func(a,b);
}
int main(int argc, char* argv[]){
 Math m;
 boost::function<double(double, double)> =
 boost::bind(&Math::do_add,&m,_1,_2);
 boost::function<double(double, double)> ptr_sub =
 boost::bind(&Math::do_sub,&m,_1,_2);
 cout << ”Сумма: ” << ptr_add(200.0,100.0) << endl;
 cout << ”Разность: ” << ptr_sub(200.0,100.0) << endl;
}

Конструкция boost::function<double(double, double)> func читается естественным образом – понятно, что func – это функция с сигнатурой double(double, double). Этот же тип имеют и переменные ptr_add и ptr_sub – сразу ясно, что функция do_operation готова с ними работать.

Смысл конструкции boost::bind(&Math::do_add,&m,_1,_2) понять немного сложнее. bind связывает функцию и ее аргументы в единый «вызываемый» объект, обращаться с которым можно как с обычной функцией. В нашем случае мы связываем метод Math::do_add с экземпляром нашего класса m и «магическими» переменными _1 и _2. При вызове созданного объекта _1 автоматически заменяется первым фактическим аргументом, _2 – вторым и т. д. Это так называемые заполнители [placeholders] для аргументов. Наконец, мы просто вызываем ptr_add и ptr_sub как обычные функции, поскольку они уже связаны с нужным экземпляром класса Math.

Научившись работать с Boost.function и Boost.bind, можно навсегда забыть о кошмарном синтаксисе указателей на функции.

Сигналы и слоты

  • Boost.signals2

Если вы работали с библиотекой Qt, то знаете, насколько удобной во многих случаях является концепция сигналов и слотов. Сигналы – это, по сути, обобщение идеи функций обратного вызова. Сигнал может быть соединен со множеством слотов, и все они будут вызываться при активации сигнала. Соединение со слотом создается вручную, но разрывается автоматически, когда разрушается вызывающий либо обрабатывающий сигнал объект. Это очень важное свойство – оно позволяет не заботиться о том, что случайно будет вызван слот несуществующего объекта и вся программа «рухнет». Обычные функции обратного вызова никаких гарантий на этот случай не дают.

В Boost есть две очень похожие библиотеки, реализующие сигналы – signals и signals2. Последняя удобнее и современнее, поскольку является полностью заголовочной и позволяет посылать сигналы между разными нитями в многопоточной программе.

Для примера напишем программу, печатающую результат четырех арифметических действий с числами:

#include <boost/signals2.hpp>
void mul(float x, float y) { cout << x * y << endl; }
void div(float x, float y) { cout << x / y << endl; }
void add(float x, float y) { cout << x + y << endl; }
void sub(float x, float y) { cout << x - y << endl; }
int main(int argc, char* argv[]){
 boost::signals2::signal<void (float, float)> sig;
 sig.connect(&add);
 sig.connect(&sub);
 sig.connect(&mul);
 sig.connect(&div);
 sig(10,5);
}

Мы создали сигнал с нужной нашим функциям сигнатурой void (float, float), соединили его со всеми четырьмя функциями методом connect() и активировали, передав параметры 10 и 5. В результате вызываются все функции поочередно.

Сигналы можно соединять не только с функциями, но и с любыми вызываемыми объектами (для которых определен оператор “()”), в том числе с теми, которые создает boost::bind. Например, в нашем примере с функциями обратного вызова можно было бы написать

boost::signals2::signal<double (double, double)> sig;
sig.connect( boost::bind(&Math::do_sub,&m,_1,_2) );
cout << *sig(200,100) << endl;

При этом вызывается нужный метод, но значение возвращается в виде указателя, и его нужно разыменовать.

Если вызывается несколько сигналов, которые возвращают значения, то результат такого вызова неоднозначен. Что при этом будет возвращено, решает пользователь с помощью так-называемых «комбинаторов» [combiners] – специальных объектов, которые аккумулируют значения, возвращенные всеми слотами, и обрабатывают их. Однако это уже «высший пилотаж». Если же игнорировать возвращаемые значения слотов, то пользоваться сигналами чрезвычайно просто.

Гетерогенные контейнеры

  • Boost.variant и Boost.any

Один из хрестоматийных вопросов, постоянно задаваемых на тематических форумах – как создать гетерогенный контейнер в С++? С++ – строго типизированный язык, и в массивах или контейнерах STL можно хранить значения только какого-то одного определенного типа. В то же время часто возникает необходимость создать гетерогенный контейнер, содержащий, скажем, одновременно числа и строки. Сделать это силами стандартной библиотеки можно, но решение будет громоздким и небезопасным, т. к. придется использовать «тяжелую артиллерию» вроде указателей типа *void.

В Boost есть стредства, позволяющие создать удобные и безопасные гетерогенные контейнеры со строгой проверкой типов. Начнем со случая, когда мы четко знаем, что будем хранить либо строки, либо целые числа, либо числа с плавающей точкой:

#include <boost/variant.hpp>
using namespace std;
…
typedef boost::variant<string, double, int> data_t;
vector<data_t> data;
data.push_back(123);
data.push_back(3.14);
data.push_back(“Hello!”);

Мы перечисляем все нужные типы как параметры шаблонного типа variant, после чего можем использовать его как тип для нашего контейнера. В контейнер теперь можно добавлять данные всех перечисленных типов. Чтобы прочитать данные, нужно либо точно знать тип текущего элемента (что бывает редко), либо действовать методом проб и ошибок. Например, так можно вывести все данные из нашего контейнера вместе с их типом:

BOOST_FOREACH(data_t& item, data){
 int* ptr1 = boost::get<int>(&item);
 if(ptr) cout << ”Это целое число:” << *ptr1 << endl;
 double* ptr2 = boost::get<double>(&item);
 if(ptr2) cout << ”Это число c плавающей точкой:” << *ptr2 << endl;
 string* ptr3 = boost::get<string>(&item);
 if(ptr3) cout << ”Это строка:” << *ptr3 << endl;
}

Шаблонная функция boost::get<> пытается получить значение заданного типа из переменной типа variant (передается ее адрес). Если это удается, она возвращает указатель нужного типа на это значение, а если не удается, то NULL. Перебирая все варианты, можно определить и тип элемента, и его значение. Есть и другой вариант этой функции, который принимает не адрес, а саму переменную, и возвращает не указатель, а само значение. Если тип не совпадает, то генерируется исключение типа bad_get:

try {
 int val = boost::get<int>(item);
} catch(const boost::bad_get&){
 cout << ”Это не целое число!” << endl;
}

Другой сценарий использования гетерогенного контейнера возникает, когда нужно хранить значения действительно любого типа либо когда вариантов типов очень много. В таком случае на помощь приходит Boost.any:

boost::any data; // Можно хранить что угодно!
...
// Пытаемся извлечь строку
try{
 cout << any_cast<string>(data) << endl;
} catch(const boost::bad_any_cast&) {
 cout << ”Это не строка!” << endl;
}

Функция any_cast ведет себя точно так же, как и boost::get, и тоже существует в двух вариантах, возвращающих указатель либо само значение.

Выводы

Библиотеки Boost вполне оправдывают свое название – они позволяют повысить продуктивность программирования на С++ и создают удобства, невиданные в рамках базового языка и стандартной библиотеки. Особенно хорошо с этой задачей справляются «маленькие» библиотеки вроде Boost.foreach и Boost.assign – простые, легкие в освоении и имеющие удобный синтаксис. Они заполняют пробелы стандартной библиотеки и исправляют недостатки синтаксиса самого языка. Ярким примером являются Boost.bind и Boost.function, позволяющие забыть об указателях на функции, их ограничениях и ужасном синтаксисе. Однако не все рассмотренные в этой статье библиотеки являются «маленькими». Например, Boost.signals2 имеет свои «темные углы», а синтаксис комбинаторов способен повергнуть новичка в уныние. В этом особенность Boost – отход от простых моделей использования часто приводит пользователя в плохо документированные «дебри». Тем не менее, Boost – обязательная часть арсенала современного программиста на С++, который хочет работать действительно эффективно, а не бороться постоянно с синтаксическими тонкостями и излишней низкоуровневостью этого языка.


Персональные инструменты
купить
подписаться
Яндекс.Метрика