Последнее обновление:
| Главная |  Статьи |  Назад | 
Шаблон smart_ptr

Постановка задачи.

        Перед нами ставится следующая задача: создать шаблон, назовём его smart_ptr, с реализацией некоторых функций шаблона auto_ptr стандартной библиотеки C++ для автоматического удаления динамически созданных объектов при выходе из области видимости указателей (возможно нескольких) на них. Другими словами, допустим, у нас есть n динамически созданных (при помощи new) объектов одного типа, и имеется m указателей на них, при этом m>=n. Необходимо создать такой шаблон, который позволил бы реализовать автоматическое удаление объекта из памяти после удаления последнего указателя на него. Графически исходные данные можно представить следующим образом:

Организация связей между объектами и smart-указателями на них

Предлагаемое решение

        Предлагаемое решение не является единственным решением поставленной задачи. Кроме того, я не утверждаю, что предложенное решение является наилучшим, но, на мой взгляд, является более-менее логичным. Итак, что же за решение я предлагаю: создадим шаблон (в терминах C++) smart_ptr, содержащий внутри себя (в секции private) статический список (std::list), содержащий пары (std::pair), первый член которых – указатель на объект, а второй член – количество указателей на объект, которые были переданы шаблону smart_ptr.
 Создадим каркас класса шаблона smart_ptr:

template<class T> class smart_ptr { /**/ };

Для дальнейшей простоты написания (и восприятия, кстати, тоже) сделаем пару forward-объявлений:

• Определим std::pair<int ,T*> как pair_type:

typedef typename std::pair<int ,T*> pair_type;

• Определим std::list<pair_type> как ptr_list:

typedef typename std::list<pair_type> ptr_list;

Определим статический список ptr_list пар типа pair_type:

static ptr_list ptrs;

Кроме того, нам потребуется отдельно хранить указатель на объект в каждом конкретном экземпляре класса smart_ptr:

T *ptr;

Теперь private-секция класса будет выглядеть так:

template<class T> class smart_ptr
{
private:
    typedef typename std::pair<int ,T*>   pair_type;
   typedef typename std::list<pair_type> ptr_list;
static ptr_list ptrs;
    T *ptr;
/**/
}

Примечание: Хочу обратить Ваше внимание на наличие ключевого слова typename после typedef. Раньше стандарт C++ позволял не писать это ключевое слово, поэтому его можно не писать в «старых» компиляторах: например, Microsoft Visual C++.NET 2002 такое пропустит, а вот 2003 уже нет… Кому интересно для чего это нужно – читайте стандарт C++…

Реализация

        Итак, мы подошли к самому интересному разделу: реализации решения задачи. Зачастую, далеко не достаточно просто найти решение задачи, потому что решение может быть не реализуемым средствами языка. Наша задача – куда проще, поэтому и реализовать её достаточно просто, однако и тут есть свои «подводные камни».
        Всё решение будет в основном лежать в конструкторе и деструкторе объекта, однако, нам понадобится дополнительный интерфейс для доступа к хранимому объекту, а также манипуляции с ним.
        Конструкторы и деструктор, которые нам понадобятся:
• Конструктор по-умолчанию. В принципе, такой можно и не создавать, т. к. у объекта есть конкретная задача – хранение указателя на объект. В данном случае указатель инициализируется значением NULL.

smart_ptr(void);

• Конструктор с инициализацией хранимого указателя. Этот конструктор и будет «рабочей лошадкой».

smart_ptr(T*);

• Конструктор копий. Его необходимо реализовать по той причине, что нет смысла хранить один и тот же указатель в двух разных объектах smart_ptr. Кого интересует больше – почитайте про auto_ptr – тот же самый принцип. Обратите внимание, мы не гарантируем константности исходного объекта: ключевого слова const, как принято в конструкторах копий, нет:

smart_ptr(smart_ptr<T>&);

• Деструктор. Ну тут всё просто:

~smart_ptr(void);

        Помимо конструкторов и деструктора, как уже говорилось ранее, нам понадобится интерфейс доступа к объекту. Минимально-оптимальный набор будет следующим:

• Переустановка/сброс хранимого указателя:

void reset(T *_ptr = NULL) throw();

• Получение хранимого указателя:

T* get(void) throw ();

• Освобождение экземпляра от обязанностей:

T* release(void) throw ();

• Получить количество хранимых указателей на данный объект:

int counter(void) const throw ();

Примечание: Здесь и далее, я указываю на то, что мои методы ни при каких условиях не генерируют исключений. Это задаётся спецификацией исключения, ключевым словом throw() после объявления метода. Напоминаю, что если Ваш метод не меняет внутреннего состояния класса, используйте модификатор const после объявления функции. Помните, что это вовсе не для украшения: Ваш интерфейс могут использовать другие люди, которым нужны гарантии…

        И, конечно же, операторы:

• Оператор разыменовывания:

T& operator*() const throw ();

• Оператор доступа к членам объекта:

T* operator->() const throw ();

• Оператор присваивания:

smart_ptr<T> operator=(smart_ptr<T>&) throw();

• Оператор неравенства:

bool operator!=(smart_ptr<T> const&) const throw();

• Оператор равенства:

bool operator!=(smart_ptr<T> const&) const throw();

Примечание: Создавая новый класс, помните об операторах. Они нужны людям так же, как и гарантии…

        Итак, мы получили полное объявление класса:

template<class T> class smart_ptr
{
private:
    typedef typename std::pair<int,T*>    pair_type;
    typedef typename std::list<pair_type> ptr_list;
      static ptr_list ptrs;
                  T *ptr;
public:
      smart_ptr(void );
      smart_ptr(T*);
      smart_ptr(smart_ptr<T>&);
     ~smart_ptr(void );
public:
      void reset(T *_ptr = NULL) throw ();
      T*   get(void)             throw ();
      T*   release(void)         throw ();
      int  counter(voidconst  throw ();
public:
     T&            operator * () const throw ();
     T*            operator ->() const throw ();
     smart_ptr<T>  operator=  (smart_ptr<T>&      )       throw ();
     bool          operator!= (smart_ptr<T> const&) const throw ();
     bool          operator== (smart_ptr<T> const&) const throw ();
};

        Все статические элементы необходимо инициализировать вне объявления класса:

template<class T> typename smart_ptr<T>::ptr_list smart_ptr<T>::ptrs = smart_ptr<T>::ptr_list();

Примечание: Опять-таки обращаю Ваше внимание на ключевое слово typename.

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

Конструкторы

template<class T>
smart_ptr<T>::smart_ptr(void)
{ ptr = NULL; }

        Конструктор значений по умолчанию. Ну, тут всё просто: Внутреннему указателю присваивается значение NULL. Подробнее стоит рассмотреть код ниже:

template<class T>
smart_ptr<T>::smart_ptr(T* _ptr)
{
 if((ptr=_ptr)==NULL) { return ; }
 for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
  if((*i).second==_ptr) { ++((*i).first); return ; }
 pair_type new_pair;
 new_pair.first  = 1;
 new_pair.second = _ptr;
 ptrs.push_back(new_pair);
}

        Первым действием мы сохраняем значение указателя _ptr во внутреннем указателе ptr, и сразу же проверяем его значение: если оно NULL, то нет смысла продолжать – мы получили конструктор по умолчанию. Если же указатель не является NULL, то мы ищем его в списке сохранённых указателей. Если такой указатель уже имеется, то мы инкрементируем значение его счётчика.

Примечание: Если Вам необходимо только инкрементировать значение, т. е. не важно, что использовать: var++ или ++var (var – переменная любого типа, для которого определён префиксный и постфиксный инкремент), то предпочтительнее использовать ++var – это экономия памяти и более высокая скорость выполнения. Почему так? Посмотрите реализацию операторов постфиксного и префиксного инкремента. То же верно для декремента var-- и  --var.

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

template<class T>
smart_ptr<T>::smart_ptr(smart_ptr<T> &from)
{ ptr = from.ptr; from.ptr = NULL; }

        Мы сохраняем указатель из исходного объекта и обнуляем его, дабы исходный объект более не хранил его.

Деструктор

        Деструктор гораздо интереснее в реализации, нежели конструкторы, т. к. именно здесь мы принимаем решение, что делать с объектом в динамической памяти:

template<class T>
smart_ptr<T>::~smart_ptr(void)
{
 if(ptr!=NULL) for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
  if(i->second==ptr)
   if(--(i->first)==0) { ptrs.erase(i); delete ptr; break ; }
}

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

Интерфейс пользователя

        Переустановка указателя производится следующим образом:

template<class T>
void smart_ptr<T>::reset(T *_ptr) throw ()
{
 if(ptr==_ptr) return ;
 for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
  if(i->second==ptr)
   if(--(i->first)==0) { delete ptr; ptrs.erase(i); break ; }
 if((ptr=_ptr)==NULL) return ;
 for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
 if(i->second==_ptr) { ptr= _ptr; ++(i->first); return; }
 pair_type new_pair;
 new_pair.first  = 1;
 new_pair.second = ptr = _ptr;
 ptrs.push_back(new_pair);
}

        Сначала мы при необходимости удаляем старый объект (код, эквивалентный деструктору), а потом добавляем либо обновляем записи в списке сохранённых указателей (эквивалентно конструктору).
Получить указатель на объект мы можем вызовом метода:

template<class T>
T* smart_ptr<T>::get(void) throw ()
{ return ptr; }

Следующий метод освобождает объект smart_ptr от его обязанностей:

template<class T>
T* smart_ptr<T>::release(void) throw ()
{
 for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
  if(i->second==ptr)
   if(--(i->first)==0) { ptrs.erase(i); break ; }
 T* tmp = ptr;
 ptr = NULL;
 return tmp;
}

        Под освобождением от обязанностей мы понимаем обновление (или удаление) записей списка указателей для данного объекта без удаления объекта в динамической памяти. Это необходимо на тот случай, если мы «передумаем» автоматически удалить объект.
И, наконец, последний метод, позволяющий нам узнать количество указателей, хранимых для данного объекта:

template<class T>
int smart_ptr<T>::counter(void) const throw ()
{
 for(ptr_list::iterator i=ptrs.begin(); i!=ptrs.end(); ++i)
  if(i->second==ptr)
   return i->first;
 return 0;
}

        Скорее всего, нуль так и не будет никогда возвращён, т. к. при обнулении счётчика запись автоматически удаляется.

Операторы

        Ещё раз призываю Вас не лениться и пожалеть пользователей Вашего класса: пишите операторы для своих классов! Кроме того, ни один публикуемый класс не может считаться профессионально выполненным, если он не имеет операторов. Исключения из этого бывают, но очень редко. Как минимум, оператор присваивания бывает всегда.
Оператор разыменовывания:

template<class T>
T& smart_ptr<T>::operator* () const throw ()
{ return *ptr; }

        Говорить тут особо не приходится: разыменовываем внутренний указатель и возвращаем ссылку на сам объект. Оператор доступа к членам объекта:

template<class T>
T* smart_ptr<T>::operator-> () const throw ()
{ return ptr; }

        Тот, кто работал с итераторами STL знает, что это такое, а кто не работал - как Вы вообще программируете? Этот оператор позволяет работать с объектом smart_ptr как с указателем на хранимый им объект. Оператор присваивания:

template<class T>
smart_ptr<T> smart_ptr<T>::operator =(smart_ptr<T> &_ptr) throw ()
{
 reset(_ptr.ptr);
 _ptr.ptr = NULL;
 return *this ;
}

        Операторы равенства и неравенства:

template<class T>
bool smart_ptr<T>::operator==(smart_ptr<T> const &_ptr) const throw ()
{ return ptr== _ptr.ptr; }

template<class T>
bool smart_ptr<T>::operator!=(smart_ptr<T> const &_ptr) const throw ()
{ return ptr!= _ptr.ptr; }

        Два объекта smart_ptr равны тогда, когда их внутренние указатели равны, и наоборот, два объекта smart_ptr не равны тогда, когда их внутренние указатели не равны.

Результаты

        Вот мы и закончили рассмотрение реализации шаблона smart_ptr. Я повторюсь, но стоит напомнить о том, что данная реализация не является идеальной. По ходу написания статьи я нашёл несколько мест, где шаблон можно было бы отредактировать. Но это уже полёт фантазии. Как говорится: работает – не трогай. Исходные коды прилагаются, ссылки на них приведены ниже. Вашему вниманию предлагается также готовый проект для Microsoft Visual C++ .NET 2002.

--
Евгений Назаров
nevsoft@mail.ru
Последнее обновление:

        Исходные файлы к статье:

 Заголовочный файл шаблона smart_ptr в архиве ZIP:   smart_ptr.zip
 Готовый проект с использованием шаблона smart_ptr
 для Microsoft Visual C++ .NET 2002 в архиве ZIP:   smart_ptr_vc.zip

| Главная |  Статьи |  Назад | 
Hosted by uCoz