Использование 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() );

 Подписаться на RSS

 #  #  #  #  #  #  #  #  #  #

Добавить комментарий