Использование Boost Pointer Container Library
Как часто Вам приходилось хранить в стандартных STL контейнерах указатели на объекты? Я думаю, достаточно. Но хранение обычных указателей приводит к тому, что приходится тратить дополнительные усилия на то, чтобы проверять, что память освобождается при уничтожении контейнера.
Обычное решение этой проблемы - умные указатели с подсчетом ссылок, обеспечивающие автоматическое освобождение памяти. Однако, достаточно часто встречаются случаи, когда их использование избыточно, например, когда объекты имеют всего одного владельца и подсчет ссылок, вообще говоря не нужен.
А судя по замерам, накладные расходы по производительности и памяти на подсчет ссылок достаточно велики: Boost smart pointer timings.
Что же делать в таких случаях? На помощь приходит очередная библиотека в составе Boost - Pointer Container Library. Она предоставляет основные контейнеры, схожие со стандартными, специально предназнченные для хранения указателей.
Использование
В примере мы будем использовать ptr_vector. Для него необходимо подключить один заголовочный файл:
#include <boost/ptr_container/ptr_vector.hpp>
Рассмотрим простейший пример использования, приведенный в документации библиотеки. Предположим, что у нас имеется классическая иерархия классов:
class animal
{
public:
virtual ~animal() {}
virtual void eat() = 0;
virtual int age() const = 0;
// ...
};
class mammal : public animal
{
// ...
};
class bird : public animal
{
// ...
};
Теперь, нам необходимо создать список животных. Для этого мы будем использовать контейнер ptr_vector:
boost::ptr_vector<animal> the_animals;
Сразу обращаем внимание: при объявлении контейнера * не указывается. Чтобы добавить объект в контейнер, вызываем метод push_back, аналогично стандартному контейнеру std::vector:
the_animals.push_back( new mammal("joe") );
the_animals.push_back( new bird("dodo") );
Теперь о доступе к элементам: контейнер предоставляет доступ к элементам не по указателям, а по ссылкам. То, есть необходимо писать следующим образом:
the_animals[0].eat(); // А в случае std::vector<animal*> это было бы vec[0]->eat();
boost::ptr_vector<animal>::iterator i = vec.begin();
i->eat(); // Опять же, для std::vector это было бы (*i)->eat(); Не знаю как Вас, но меня такая запись всегда раздражала.
Еще одно отличие: обработка нулевых указателей. Стандартный vector позволяет добавлять элементы - нулевые указатели. ptr_vector - не позволяет (по умолчанию). То есть, следующий код вызывает исключение:
the_animals.insert( the_animals.begin(), 0 ); // Исключение
Однако, если Вам необходимо, Вы можете разрешить хранение нулевых указателей. Делается это следующим образом:
boost::ptr_vector< boost::nullable<animal> > the_animals_nullable;
the_animals_nullable.insert( the_animals_nullable.begin(), 0 ); // Это уже корректно
Клонирование и перемещение данных
Крайне важное отличие таких контейнеров от стандартных в том, что они не могут быть скопированы напрямую! В чем причина: предполагается, что каждый указатель в любой момент времени находится в одном из таких контейнеров, который и освободит память при необходимости. Если же скопировать содержимое, то Вы получили бы указатель в двух контейнерах, что привело бы к последующим проблемам с их удалением.
Однако, не все так плохо. Вместо копирования можно осуществлять другие операции:
- Во-первых, контейнеры можно клонировать, вызывая клонирование всех входящих в них элементов.
- Во-вторых, данные можно извлечь из контейнера. И затем, например, переместить в другой.
Сначала о клонировании. Для того, чтобы содержимео контейнера могло быть клонировано, для всех его элементов должны быть определены две функции:
namespace boost
{
inline T* new_clone( const T& t )
{
// Здесь клонируем объект...
}
void delete_clone( const T* t )
{
// Здесь удаляем объект...
}
}
Если Ваш компилятор поддерживает Argument-Dependent Lookup, то их можно объявлять не в пространстве имен boost, а в том, где расположен обрабатываемый ими тип.
После того, как такие функции объявлены, можно вызывать метод clone контейнера:
the_animal_clones = the_animals.clone();
Теперь об извлечении и перемещении. Контейнеры предоставляют набор методов, позволяющих извлечь указатель по итератору и переместить набор элементов в другой контейнер:
boost::ptr_vector<animal>::auto_type the_animal = the_animals.release( the_animals.begin() ); // Извлекаем первый элемент
the_animal->eat();
При этом auto_type представляет из себя аналог std::auto_ptr.
Для перемещения существуют два вариант метода transfer
another_zoo.transfer( another_zoo.end(), // Добавляем последним элементом
zoo.begin(), // Первый элемент
zoo ); // Из этого контейнера
another_zoo.transfer( another_zoo.begin(), // Добавляем в конец
zoo.begin(), // От первого
zoo.end(), // До последнего
zoo ); // Из этого контейнера
Алгоритмы
К сожалению, с этими контейнерами нельзя использовать стандартные алгоритмы STL. Однако, некоторые основные реализованы в виде методов:
boost::ptr_vector<animal> zoo;
...
zoo.sort(); // Предполагаем, что описан 'bool operator<( const animal&, const animal& )'
zoo.sort( std::less<animal>() ); // То же самое, обращаем внимание на отсутствие *
zoo.sort( zoo.begin(), zoo.begin() + 5 ); // Сортируем участок
zoo.unique(); // Предполагаем, что описан 'bool operator==( const animal&, const animal& )'
zoo.unique( zoo.begin(), zoo.begin() + 5, my_comparison_predicate() );
zoo.erase_if( my_predicate() );
