msiu_logo
Кафедра информационных систем и технологий
http://edu.msiu.ru

Обзор стандартной библиотеки языка C++

А.Г. Верещагин

2 марта 2014

Введение

Стандартная библиотека языка C++ (далее для краткости просто стандартная библиотека) — важная составляющая инфраструктуры языка, обеспечивающая универсальный набор классов и функций, применимых в широком спектре задач. Стандартная библиотека решает сложную задачу обеспечения переносимости и совместимости кода, позволяя использовать единый инструментарий в различных проектах и на различных платформах. Библиотека развивается вместе с языком в соответствии с современными тенденциями. Наработки, ранее доступные лишь в нестандартных расширениях, становятся частью языка. Стандарт C++11 привнёс не мало нового в библиотеку языка: была добавлена поддержка параллельного программирования, регулярных выражений, рациональных дробей и случайных чисел, улучшены поддержка интернационализации, механизмы управления ресурсами, совместимость с поздними стандартами языка C.

Для улучшения совместимости с языком C библиотека включает в себя большинство стандартных заголовочных файлов данного языка, доступных как по старому имени (с расширением .h), так и по новому (с префиксом c). Рекомендуется повсеместно использовать второй способ, если только не стоит задача обеспечить совместимость исходного кода с обоими языками.

Другой крупной частью библиотеки C++ является библиотека стандартных шаблонов (STL), содержащая в себе обобщённые реализации некоторых контейнеров (containers), алгоритмов (algorithms), функторов (functors) и умных указателей (smart pointers), в том числе итераторов (iterators). Стандарт языка не оперирует термином STL, так что деление библиотеки на STL и не-STL составляющие можно считать достаточно условным. Сам термин STL имеет историческое происхождение, но часто используется как синоним для обозначения стандартной библиотеки C++ целиком.

Все имена стандартной библиотеки содержатся внутри пространства имён std. Для доступа к ним необходимо использовать или оператор разрешения области видимости ::, или директиву using. Далее имена, определённые в стандартной библиотеке, будут приводиться без префикса std::.

Данный обзор содержит лишь неполный перечень основных компонентов библиотеки с их кратким описанием. За более полной информацией следует обращаться к специализированным источникам.

Контейнеры

Контейнеры — это специализированные классы, предназначенные для хранения однотипных объектов и обеспечения доступа к ним. Разные контейнеры обеспечивают различную эффективность тех или иных операций, так что необходимо разумно подходить к выбору наиболее подходящего к ситуации контейнерного класса. Контейнеры обладают рядом преимуществ перед классическими массивами C, решающими ту же задачу. Большинство контейнеров поддерживают динамическое изменение собственного размера, так что нет необходимости заранее беспокоиться о выделении достаточного объёма памяти и дальнейшем его перераспределении в случае необходимости. Информация о числе элементов в контейнере всегда доступна, её не нужно хранить отдельно, как это было в случае массивов. Для контейнеров доступен более широкий набор операций, тогда как для массивов возможно лишь чтение или запись элементов по заданному индексу. При грамотном подходе можно свести накладные расходы на использование контейнеров к минимуму, так что классические массивы имеет смысл использовать либо если нужно «выжать» максимум эффективности, либо если нужно добиться совместимости с кодом на C, приспособленным к массивам.

Контейнеры хранят копии переданных в них элементов. Но в контейнерах можно хранить и указатели. Следует иметь ввиду, что на объекты, которые могут быть сохранены в контейнере, налагается ряд разумных ограничений. Не все из них могут быть удовлетворены, но в таком случае не все контейнерные операции будут доступны. Простой пример: наличие операции сравнения у элементов позволяет поэлементно сравнивать и контейнеры. Типичными требованиями для элементов контейнера являются: наличие копирующего конструктора, оператора копирующего присваивания, деструктора, конструктора по умолчанию, операций == и <. Вместо копирования в C++11 контейнеры могут использовать семантику перемещения, если только не возникает задача скопировать контейнер целиком. Правда, это работает только для операций перемещений, помеченных спецификатором noexcpept.

Контейнерные классы — это шаблоны. При использовании шаблонов необходимо конкретизировать их параметры, если компилятор не может получить информацию о типах из контекста. Так выглядит типичный пример объявления пустого вектора v, предназначенного для хранения целых чисел:

vector<int> v();

Далее, если не указано иного, про контейнерные классы будет предполагаться, что они параметризируются одним типом — типом хранимых в них объектов. Для контейнеров, динамически управляющих памятью, доступна конкретизация дополнительным параметром — так называемым распределителем памяти (allocator) — специализированным классом, отвечающим за распределение памяти, доступной контейнеру. На практике обычно хватает стандартной реализации, использующей, скорее всего, realloc.

Последовательные контейнеры

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

Класс arrayC++11

Наиболее близким к классическим массивам является контейнерный класс arrayC++11, являющийся, по сути, обёрткой над ними. Данный класс, как и классический массив, может хранить фиксированное число элементов, но поддерживает дополнительный набор методов, в том числе универсальный для всех контейнеров STL, и может быть использован при обобщённом программировании наряду с другими контейнерными классами. Преимущество данного контейнера — максимально быстрый доступ к своих элементам. Данный шаблонный класс конкретизируется двумя параметрами: хранимым типом данных и размером.

Класс valarray

valarray — это ещё один специализированный контейнерный класс, оптимизированный для эффективного использования векторных операций. Класс параметризируется типом хранимых элементов. Ко всем хранимым в контейнере элементам можно единовременно применять различные операции и функции, относительно которых они замкнуты; в случае пары контейнеров операции к элементам могут быть применены попарно.

Класс vector

Класс vector, пожалуй, можно назвать наиболее популярным STL-контейнером, пришедшим на замену классическим массивам. Данный класс, как и обычные массивы, предоставляет быстрый доступ к элементам. Элементы внутри вектора хранятся в памяти линейным образом, так что на эту область памяти всегда можноC++11 получить указатель, что делает векторы ещё более совместимыми с массивами. Преимущество вектора заключается в возможности динамически менять свой размер в процессе накопления или изъятия хранимых элементов. Вставка или удаление элементов в конце вектора в среднем так же эффективна. Вставка или удаление элементов в общем случае имеет линейную сложность.

В стандартной библиотеке определён специальный оптимизированный класс для векторного хранения логических значений vector<bool>. При работе с данным классом следует помнить о ряде характерных ограничений. Связанных с тем, что логические значения хранятся «упакованным» образом, так что нельзя, например, получить C-указатель на отдельный бит данных. На практике, там, где не удаётся использовать vector<bool>, не редко применяют vector<char> или аналоги. Для хранения битовой маски фиксированного размера также можно использовать bitset.

Класс deque

Класс deque похож на класс vector, но поддерживает не только эффективные операции вставки/удаления в конце контейнера, но, аналогично, эффективными являются операции вставки/удаления в начале контейнера. Доступ к элементам по прежнему является операцией константной сложности, но, вероятно, менее эффективной, чем в случае вектора. Так же класс deque не гарантирует линейного расположения элементов в памяти, так что для конвертирования в классический массив необходимо использование дополнительной памяти и времени.

Класс list

Класс list реализует структуру, известную под названием двунаправленный список. Преимущество списков — вставка и удаление элементов в произвольных позициях за константное время. Однако произвольный доступ к элементам имеет линейную сложность. Двунаправленный список позволяет осуществлять эффективную итерацию по своим элементам как в прямом порядке (от начала списка к концу), так и в обратном порядке (от конца списка к началу). Так же существует класс forward_listC++11, представляющий однонаправленный список. Итерация по такому списку возможна лишь в прямом направлении, однако такой список занимает меньше памяти, чем двунаправленный.

Класс string

Класс basic_string предназначен для хранения в линейной области памяти последовательностей символьных типов, именуемых «строками». В C строки представляют собой массивы символов, включая терминальный на конце. В C++ вместо родных строк C принято использовать соответствующие контейнерные классы. Но следует иметь ввиду, что неявных преобразований из строкового литерала в контейнерный класс не осуществляется.

Класс basic_string, параметризированный типом данных char, называется string. Строковые классы в C++ похожи на векторы. Дополнительно поддерживается конкатенация строк с помощью перегруженной операции сложения. Поддерживается операции замены подстроки и получение подстроки. Начиная с C++11, строки хранятся в памяти непрерывным образом, включая терминальный символ, так что легко могут быть преобразованы в классические строки C и при необходимости.

Контейнеры-адаптеры

Контейнеры-адаптеры (container adaptor) — это обёртки над другими контейнерами, предоставляющие особые наборы операций. Оборачиваемый контейнер может быть задан как дополнительный параметр шаблона. Если параметр не задан, будет использоваться специфический для адаптера контейнер по умолчанию.

Класс stack

Класс stack предоставляет функционал по вставке/извлечению элементов в конце контейнера, а так же доступ к последнему элементу. В качестве контейнера по умолчанию используется класс deque.

Клас queue

Очередь поддерживает операции вставки в конец и извлечения элемента из начала, а так же операции доступа к первому и последнему элементу. В качестве контейнера по умолчанию используется deque.

Класс priority_queue

Очередь с приоритетами поддерживает операции, аналогичные классу stack, но вставка элементов предполагает их неявную сортировку, так что операция извлечения элемента извлекает всегда минимальный элемент коллекции. Вставка в уже упорядоченный массив, называемый кучей (heap), элемента имеет логарифмическую сложность. В особых случаях может потребоваться определить дополнительным параметром шаблона функцию сравнения двух элементов коллекции. Так, если использовать вместо операции «меньше» операцию «больше», можно извлекать из очереди с приоритетами не минимальные, а максимальные элементы. В качестве контейнера по умолчанию используется vector.

Ассоциативные контейнеры

В ассоциативных контейнерах доступ к элементу осуществляется не на основе его позиции, а на основе значения элемента. Ассоциативные контейнеры делятся на упорядоченные (ordered) и неупорядоченныеC++11 (unordered). В случае упорядоченных коллекций для хранимых элементов должен быть определён порядок (операция сравнения может быть задана отдельным параметром шаблона), сами же элементы хранятся упорядоченным образом, например, в виде красно-чёрных деревьев (red-black tree). В случае неупорядоченных контейнеров для элементов должна быть вычислима хеш-функция (может быть задана отдельным параметром шаблона), отображающая элемент в некоторое число в соответствии с его значением. Хорошо подобранные хеширующие функции способны обеспечить практически константное время доступа к элементам при столь же быстрой возможности вставки и удаления. Для большинства примитивных типов C и некоторых классов стандартной библиотеки уже определена перегруженная хеширующая функция hash. Упорядоченные контейнеры требуют логарифмической сложности для своих операций, но зато позволяют обойти элементы коллекции в соответствии с их порядком, что может быть полезным дополнительным свойством.

Классы set и multiset

Класс set представляет собой упорядоченный контейнер, соответствующий математическому понятию множества. В множестве хранятся только уникальные объекты. Множества поддерживают быструю операцию поиска (логарифмическое время). Вставка и удаление — в общем случае так же операции с логарифмической сложностью.

Класс multiset представляет собой контейнер, реализующий семантику мультимножества. В отличие от обычных множеств, в мультимножествах могут храниться несколько копий одинаковых объектов. В остальном мультимножества соответствуют множествам.

Классы unordered_setC++11 и unordered_multisetC++11

Класс unordered_set и unordered_multiset — неупорядоченные контейнеры, реализующие семантику множества и мультимножества соответственно. Поиск, вставка и удаление в среднем занимают константное время.

Классы map и multimap

Классы map и multimap реализуют классические ассоциативные массивы, отображающие множество ключей на множество значений (типы ключа и значения задаются как два независимых параметра шаблона). Таким образом, в отличие от обычных последовательных контейнеров, в качестве ключа в ассоциативных массивах выступает не число (индекс), а любой объект. Иногда ассоциативные массивы ещё называют словарями.

В качестве ключей в классах map и multimap можно использовать любые сравнимые объекты. Значения, доступные по ключам, сравнимыми быть не обязаны. К парам ключ-значение в классах map и multimap может быть получен доступ в порядке «возрастания» ключей. Операции вставки, удаления и поиска по ключу осуществляются за логарифмическое время.

Если класс map позволяет ассоциировать с каждым ключом лишь одно значение, в случае multimap по каждому ключу может быть доступно сразу несколько элементов.

Классы unordered_mapC++11 и unordered_multimapC++11

Классы unordered_map и unordered_multimap — это неупорядоченные контейнеры, реализующие семантику ассоциативных массивов аналогично классам map и multimap. Операции вставки, удаления и поиска по ключу занимают в среднем константное время.

Потоки ввода-вывода

В стандартной библиотеке языка C++ система ввода-вывода была существенно переработана. Были введены классы потоков и буферов, связанные в сложную иерархию. Все потоки унаследованы от общего предка basic_ios (который сам является потомком класса ios_base) и делятся на входные (basic_istream), выходные (basic_ostream) и ввода-вывода (basic_iostream). В случае, если в качестве единицы ввода-вывода используется тип char, то вышеозначенные шаблонные классы при конкретизации дают, соответственно, классы ios istream, ostream и iostream. Стандартные потоки ввода, вывода и ошибок доступны по именам cin, cout и cerr. cin — экземпляр класса istream, а cout и cerr — экземпляры класса ostream. Потоки ввода и/или вывода могут быть получены для любого файла с помощью классов basic_ifstream, basic_ofstream и basic_fstream в общем случае, или с помощью классов ifstream, ofstream и fstream конкретно для типа char. Аналогично можно работать со строками как с потоками. Для этого используются классы basic_istringstream, basic_ostringstream и basic_stringstream в общем случае и классы istringstream, ostingstream и stringstream для типа char.

Механизм перегрузки функций языка C++ позволяет единообразным образом работать со всем разнообразием доступных в языке потоков и типов данных, так что система ввода-вывода в C++ сложно устроена, но её просто использовать, по крайней мере, в типовых случаях. Вместе с входными потоками можно использовать оператор >> для ввода данных из потока. Для вывода данных в выходной поток используется оператор <<. С потоками ввода-вывода можно использовать оба оператора. В зависимости от типа второго операнда можно осуществлять корректный ввод-вывод чисел и логических значений, строк и любых других объектов, для которых перегружены соответствующие операторы.

Классы буферов используются классами потоков для осуществления низкоуровневых операций ввода-вывода. Общий класс всех буферов — basic_streambuf. Для работы с файлами предназначен класс basic_filebuf, а для работы со строками — basic_stringbuf. При конкретизации типом char получаются классы streambuf, filebuf и stringbuf. При разработке собственных потоков ввода-вывода работа начинается, как правило, с собственной реализации буферного класса, унаследованного от basic_streambuf или streambuf. Экземпляры классов потоков могут быть созданы поверх соответствующих буферов.

Возможна тонкая подстройка потоков ввода-вывода с помощью флагов форматирования, манипуляторов ввода-вывода и просто специализированных функций-членов классов потоков. Например, можно переключать формат ввода-вывода чисел для использования различных систем счисления. Флаги форматирования устанавливаются с помощью функции setf(), снимаются с помощью функции unsetf(). В отличие от флагов, манипуляторы ввода-вывода передаются прямо в поток с помощью операций << и >>. Например, «правильный» способ добавить признак перевода строки в поток — воспользоваться манипулятором endl. Наконец, важно знать про существование функции eof(), проверяющей, не достигнут ли конец потока. Без подобных проверок циклическое использование операторов << и >> может привести, в конце концов, к генерации исключения.

Умные указатели

Умным указателем (smart pointers) называется объект, чьё поведение подобно указателю. Для умных указателей перегружен оператор разыменования *, иногда возможен доступ к членам объекта с помощью оператора ->. Так же для некоторых видов умных указателей возможен аналог арифметики указателей.

Итераторы

Итераторы (iterators) — это умные указатели, отделяющие способ доступа к элементам коллекции (в том числе обхода) от типа самой коллекции. Для классических массивов C роль итераторов играют обычные указатели. Для одного и того же массива можно иметь несколько указателей, каждый из которых связан со своим элементом, причём данные указатели остаются корректны до тех пор, пока массив не будет перемещён в другую область памяти. На примере массива видно, во-первых, что для доступа к элементам полезно иметь отдельные объекты, нежели сам экземпляр контейнера; во-вторых, видно, что итераторы могут становиться недействительными в случае изменения контейнера.

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

Различные коллекции предоставляют различные интерфейсы для доступа к элементам, что усложняет написание для них обобщённых алгоритмов. Итераторы предлагают более универсальный механизм доступа к элементам, так что при обобщённом программировании применяются именно они. Тем не менее не существует единственного интерфейса для всех видов итераторов, так как это либо сильно ограничило бы предоставляемые итераторами возможности, либо привело к необходимости реализовывать некоторые методы в виде заглушек, например, генерирующих исключения, если какой-либо из методов не поддерживается итератором конкретного контейнера. Вместо этого стандарт вводит четыре и одну категорию итераторов (среди первых четырёх категорий одна расширяет другую; пятая категория вводится независимо). Различные алгоритмы предъявляют различные требования к категориям итераторов. Проверка соответствия переданного итератора необходимой категории осуществляется во время компиляции. Поддержка шаблонами C++ механизма неявных интерфейсов избавляет от необходимости итераторам наследоваться от общего предка.

Самый простой тип итератора — это итератор ввода (input iterators). Итераторы ввода поддерживают всего две основные операции: чтение текущего значения (операция *) и сдвиг к следующему элементу (операция ++). Следующая категория — это прямой итератор (forward iterators). Прямые итераторы — это итераторы ввода, для которых из утверждения a == b следует утверждение ++a == ++b. Для итераторов ввода это, вообще говоря, не обязательно. При расширении прямого итератора операцией декремента (сдвига к предыдущему элементу) получаются двунаправленные итераторы (bidirectional iterators). Наконец, итераторы, обеспечивающие сдвиг на любое количество элементов относительно текущего в обе стороны (то есть операции +n и -n для целых n) называются итераторами произвольного доступа (random access iterator). Пятая категория — это итераторы вывода (output iterators). Итераторы вывода подобны операторам ввода, но вместо операции чтения (операции *) реализуют операцию записи значения в заданную позицию (операцию *=). Данную операцию так же может поддерживать любой из перечисленных выше типов итераторов. Такие итераторы собирательно называются изменяющими итераторами (mutable iterators). Когда итераторы предоставляют только доступ на чтение, они называются константными итераторами (constant interators).

Каждый контейнер предоставляет свою собственную реализацию итератора. Так же в стандартной библиотеке определены несколько итераторов-адаптеров, превращающих одни итераторы в другие. Один из примеров таких итераторов — обратный итератор (reverse_iterator). Обратный итератор строится на основе двунаправленного итератора и меняет его ориентацию, так что операция инкремента осуществляет сдвиг к предыдущему элементу, а декремента — к следующему. Другой пример — итератор вставки (insert_iterator), перегружающий для оператора вывода операцию присваивания, так, что операция записи по итератору вызывает метод insert связанного с итератором контейнера.

Стандартная библиотека C++ содержит реализации итераторов-адаптеров для потоков ввода-вывода и их буферов. Итераторы потоков ввода-вывода (istream_iterator, ostream_iterator) используют операции >> и << соответственно для реализации операций сдвига, чтения и записи. Предполагается, что потоки содержат коллекции однотипных элементов. Итераторы буферов ввода-вывода (istreambuf_iterator, ostreambuf_iterator) используются для итерации по отдельным символам потока (например, символам типа char для класса streambuf).

Контейнеры предоставляют свои собственные методы для получения итераторов, указывающих на начало и конец контейнера. Начиная с C++11 возможно использование перегруженных внешних функций beginC++11 и endC++11. С помощью данных функций можно получать не только итераторы для стандартных контейнерных классов, но и для обычных массивов (итераторами будут обычные указатели). Применение данных функций позволяет реализовывать обобщённые алгоритмы, совместимые не только с контейнерными классами, но и с массивами.

Владеющие указатели

Владеющие указатели позволяют связать время жизни объекта с временем жизни указателя на него, что можно использовать для борьбы с утечками памяти и висячими ссылками. Существуют различные типы владеющих указателей. Стандартная библиотека C++ выделяет уникальные указатели (unique_ptrC++11), разделяемые указатели (shared_ptrC++11) и слабые указатели (weak_ptrC++11).

Уникальные указатели

Уникальный указатель хранит внутри себя указатель на область динамической памяти, которая должна быть освобождена в момент вызова деструктора уникального указателя. Такой подход избавляет от необходимости самостоятельно вызывать оператор delete для хранимого указателя, так как деструкторы объектов вызываются компилятором автоматически. Так, если уникальный указатель находится на стеке, деструктор будет автоматически вызван в момент выхода из соответствующего блока. Если уникальный указатель является членом объекта, его деструктор будет автоматически вызван в момент уничтожения владеющего объекта. Концепция уникального указателя подразумевает, что только один экземпляр объекта будет владеть указателем в каждый момент времени. Поэтому уникальные указатели не могут быть скопированы, но могут быть перемещены. При перемещении владение указателем передаётся от одного объекта к другому. Уникальный указатель, который был перемещён, теряет владение указателем, так что в момент его разрушения никакая память не будет освобождена.

Компилятор осуществляет выбор между перемещением и копированием на основе типа значения, к которому применяется операция. Иногда возникает желание перемещать те объекты, которые компилятор пытается по умолчанию копировать. Для преобразования объекта к виду правосторонние ссылки (для которой применяется семантика перемещения) можно использовать функцию move() стандартной библиотеки.

Разделяемые указатели

Концепция уникальных указателей подходит не для всех случаев владения указателями. Иногда одним и тем же указателем должны владеть несколько объектов. В этом случае применимы разделяемые указатели. Разделяемые указатели поддерживают технику подсчёта ссылок для определения момента, когда объект должен быть удалён. В момент, когда указатель впервые захватывается разделяемым указателем, счётчик ссылок устанавливается в единицу. При копировании разделяемого указателя счётчик ссылок увеличивается на единицу. При уничтожении разделяемого указателя счётчик ссылок уменьшается на единицу. Таким образом все копии разделяемого указателя ссылаются на один и тот же счётчик, значение которого равно числу копий. Объект удаляется в момент разрушения последней копии разделяемого указателя.

Разделяемые указатели так же поддерживают семантику перемещения и ведут себя аналогично уникальным указателям. При перемещении указателя владение передаётся от перемещаемого объекта к его перемещённому, значение счётчика ссылок при этом не изменяется.

Слабые указатели

Слабые указатели выражают не столько владение объектом, сколько возможность им завладеть, если объект ещё существует. Так как слабые указатели не владеют объектом, они не отвечают за его уничтожение и не участвуют в подсчёте ссылок. Но прямого доступа к объекту они тоже не дают. Слабые указатели получаются из разделяемых. Слабый указатель может быть преобразован обратно в разделяемый указатель с помощью метода lock(). Если объект, на который ссылается слабый указатель, ещё существует, доступ к нему может быть получен через созданный разделяемый указатель. В случае, если объект был удалён, будет возвращён «пустой» разделяемый указатель. Главное преимущество слабых указателей перед обычными заключается в том, они решают проблему висячих ссылок (dsngling pointers) — указателей, ссылающихся на удалённый объект. Для слабого указателя можно заранее проверить, был ли удалён адресуемый объект или нет, с помощью метода expired().

Слабые ссылки применяют для хранения указателей на кэшируемые объекты. Кэш имеет собственное время жизни, не зависящее от времени жизни указателя. С помощью слабых ссылок можно следить за временем жизни кэшируемых объектов и обновлять кэш в случае необходимости.

Ещё одно популярное применение слабых ссылок — устранение циклических зависимостей разделяемых указателей. Если в цепочке владения объектами возникает цикл, техника подсчёта ссылок перестаёт работать. Цепочку надо принудительно разорвать, сделав одну из ссылок слабой. При этом нужно соблюдать особую осторожность, так как введение слабых ссылок снова способно привести к утечкам памяти, так как слабые не отвечают за освобождение ресурсов.

Идиома «Получение ресурса есть инициализация»

Получение ресурса есть инициализация (Resurce Acquisition Is Initialization — RAII) — это важная идиома программирования, безопасного относительно исключений. Смысл идиомы заключается в создании неразрывной связи между ресурсом и его объектом-владельцем, так что ресурс захватывается непосредственно в момент инициализации объекта и освобождается в момент его уничтожения. Объекты-владельцы должны иметь автоматическое время жизни, либо сами должны принадлежать другим объектам-владельцам. Так как компилятор управляет временем жизни автоматических объектов и самостоятельно вызывает деструкторы, программист может не бояться «забыть» освободить какой-либо ресурс. Более того, компилятор гарантирует вызов деструкторов и в случае возникновения исключений, что делает идиому гораздо более значимой. Если захват ресурса отделён от инициализации объекта-владельца, то при возникновении исключительной ситуации на промежутке между этими двумя событиями может произойти утечка ресурса, если только программист дополнительно не позаботился об обработке исключения и предотвращении утечек. Следование идиоме RAII облегчает работу с ресурсами, борьбу с утечками и предлагает универсальных подход для решения подобных проблем.

В C++11 введены владеющие указатели для упрощения работы с динамической памятью, но это ещё не есть реализация подхода RAII. Рассмотрим следующую строку кода:

f(shared_ptr<int>(new int(42)), g());

Компилятор обладает определённой свободой выбора в отношении порядка вычисления приведённого выше выражения. В частности, вызов функции g() может произойти между процессом захвата ресурса (new int(42)) и инициализацией объекта-владельца (shared_pte<int>(...)). Если при этом g() сгенерирует исключение, ресурс будет утерян. RAII-подход заключается в том, чтобы сделать инициализацию и захват ресурса максимально близкими, так что вынесение инициализации умного указателя в отдельное предложение должно помочь:

shared_ptr<int> p(new int(42));
f(p, g());

Теперь между захватом ресурса и инициализацией объекта-владельца не должно произойти никаких исключительных ситуаций и всё должно быть хорошо. Правда, визуально всё равно можем отличить код инициализации от кода захвата ресурса. В общем случае, если объект-владелец реализован недостаточно чисто, он может сам сгенерировать исключение в процессе конструирования, и при этом не освободить указатель, переданный в качестве аргумента. Это несколько искусственный пример, но имеющий право на жизнь. Другая «неприятная» ситуация — переполнение стека, которое теоретически может произойти между этапами захвата ресурса и инициализации. В этом случае так же произойдёт утечка ресурса. К сожалению, в случае переполнения стека поведение программы не определено, так что ресурсы, выделенные до переполнения стека, будут утеряны в любом случае.

Наиболее правильным с точки зрения идиомы RAII будет следующий код, использующий стандартную функцию make_shared()C++11:

f(make_shared<int>(42), g());

Функция make_shared()C++11 создаёт разделяемый указатель, совмещая захват ресурса с инициализацией умного указателя. Инициализация и захват ресурса стали близки настолько, насколько это возможно. Данный вариант не сильно лучше предыдущего, но выглядит более число. К тому же такая конструкция может быть реализована оптимальным образом, так что выделение памяти под хранимый объект и под служебную часть умного указателя будет совмещено в одну операцию. К сожалению, аналогичная функция для уникального указателя (make_unique()C++14) не попала в стандарт C++11, но её следует ожидать в следующей версии стандарта: C++14.

Вызываемые объекты

Функциональными объектами или функторами (functors) называются объекты с перегруженным оператором (), так что такие объекты синтаксически можно использовать подобно функциям или указателям на функции. Стоит заметить, что введённые в C++11 лямбда-выражения порождают не что иное, как анонимные функторы. При реализации обобщённых алгоритмов важно иметь способ «прозрачно» оперировать со всем разнообразием вызываемых объектов (invocable objects). Определённый в C++11 шаблонный класс function может быть использован для хранения любого вызываемого объекта соответствующей сигнатуры, являющейся параметром шаблонного класса. Так же при возникновении задачи определения переменной, пригодной для хранения вызываемого объекта, иногда может быть полезен спецификатор auto, вычисляющий тип переменной в момент её инициализации.

В C++11 наряду с лямбда-выражениями появился ещё один интересный тип вызываемых объектов, называемый связывающие выражения (bind expressions). Ниже приведён простой пример связывающего выражения:

auto plus_two = bind(plus, _1, 2);
cout << plus_two(2); // => 4

Здесь plus — это стандартный функтор, возвращающий результат сложения двух своих аргументов, а bindC++11функция связывания, связывающая в данном конкретном случае второй аргумент функтора plus со значением 2. Первый аргумент функции остаётся свободным, так что в результате будет получен вызываемый объект plus_two — функтор с одним аргументов, возвращающий значение аргумента, увеличенное на 2. Данный функтор имеет тип, совместимый с классом function<int(int)>. Связывание может применяться к любым вызываемым объектам. Это достаточно мощный инструмент обобщённого программирования, приближающий C++ к функциональной парадигме.

Алгоритмы

Стандартная библиотека C++ содержит множество обобщённых алгоритмов для работы с контейнерами и функторов «на все случаи жизни». Большинство алгоритмов принимают в качестве аргументов итераторы, так что тип используемого контейнера не имеет значения, важно лишь, чтобы предоставляемый контейнером итератор подходил под требуемую алгоритмом категорию. Среди популярных алгоритмов можно выделить алгоритм итерации по всем элементам контейнера for_each, алгоритм подсчёта числа элементов, удовлетворяющих условию count_if, алгоритмы поиска find и find_if, search и search_n, алгоритм копирования элементов из одного контейнера в другой copy, алгоритмы заполнения контейнера fill и generate, алгоритм преобразования элементов контейнера по заданному правилу transform, алгоритмы для удаления и замещения элементов remove и replace, алгоритм обращения порядка элементов в коллекции reverse, алгоритм случайного перемешивания элементов в коллекции random_shuffle, алгоритм сортировки sort, алгоритм бинарного поиска binary_search, алгоритмы поиска максимума и минимума max и min и др.

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

cout << accumulate( // применить операцию к элементам коллекции
  istream_iterator<int>(cin), // коллекция считывается с клавиатуры
  istream_iterator<int>(), // считывается до самого конца ввода
  1, // начальное значение — 1
  multiplies) // применить к элементам операцию произведения
<< endl; // закончить вывод результата признаком перевода строки

Классы исключений

При генерации исключений в C++ для хранения информации об исключении может использоваться любой объект, в том числе примитивный тип данных (например, просто код ошибки). В стандартной библиотеки языка определено семейство классов для описания различных ошибок, которые как используются самой библиотекой, так и могут быть использованы для описания пользовательских исключительных ситуаций.

Класс exception — это абстрактный базовый класс для всех других классов исключений стандартной библиотеки. В данном классе определена виртуальная функция what(), возвращающая C-строку, содержащую описание произошедшей исключительной ситуации. Строка с описанием может быть передана в конструктор в большинстве производных классов, описанных ниже.

Класс logic_error — это класс для логических ошибок, то есть ошибок, которые никогда не должны были произойти, но произошли по вине программиста, но не были обнаружены в процессе компиляции. Производные классы: invalid_argument (в функцию был передан некорректный аргумент), domain_error (выход за область определения функции), length_error (превышение ограничения допустимого размера), out_of_range (выход за допустимые границы диапазона).

Класс runtime_error описывает непредсказуемые ошибки, которые связаны с внешними событиями, возникающими в процессе работы программы. Производные классы: range_error (выход за пределы допустимого диапазона вещественной арифметики), overflow_error (переполнение при использовании целочисленной арифметики), underflow_error (потеря значимости, т.е. «переполнение наоборот» при использовании целочисленной арифметики), system_errorC++11 (ошибка взаимодействия с операционной системой).

Существует ещё целый ряд классов для описания служебных исключительных ситуаций, такие как bad_alloc (неудача выделения памяти) или bad_cast (ошибка приведения типов) и другие.