Умные указатели в C++: boost::shared_ptr, boost::weak_ptr, boost::intrusive_ptr

Умные указатели (Smart pointers) - это, объекты, которые хранят указатели на динамическую память. Они действуют аналогично обычным указателям C++, за исключением того, что автоматически освобождают память в необходимый момент. Boost предоставляет шесть типов умных указателей:

  • scoped_ptr - некопируемый автоматический указатель на объект;
  • scoped_array - некопируемый автоматический указатель на массив;
  • shared_ptr - разделяемый указатель на объект;
  • shared_array - разделяемый указатель на массив;
  • weak_ptr - вариант разделяемого указателя, не увеличивающий счетчик ссылок;
  • intrusive_ptr - указатель со встроенным в объект счетчик ссылок

scoped_ptr и scoped_array

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


#include <boost/scoped_ptr.hpp> // Заголовочный файл, необходимый для работы

...

{ // Открываем новый блок
	boost::scoped_ptr<MyObject> spobj( new MyObject() ); // Создаем новый объект, и указатель ссылающийся на него

	spobj->myField ++; // Доступ к полю объекта
	spobj->myMethod(); // Вызов метода объекта
	
	MyObject & robj = *spobj; // Получаем ссылку на хранимый объект
	MyObject * pobj = spobj.get(); // Получаем указатель на хранимый объект
	
	if ( spobj ) { // Проверяем, что указатель ссылается на объект
		std::cout << "Указатель ссылается на объект по адресу " << spobj.get() << std::endl;
	}
} // А по завершению блока, переменная spobj уничтожается и автоматически удаляет созданный объект
Чем такой код лучше, чем использование обычного указателя на MyObject? Предположим, что метод myMethod() выкидывает исключение. Что происходит в этом случае, если мы используем обычный указатель? Управление уходит из блока и память остается занятой. В приведенном же выше коде память корректно освобождается, с вызовом деструктора объекта MyObject.

scoped_array полностью аналогичен scoped_ptr, c той лишь разницей, что работает с массивами. Аналогичный пример:


#include <boost/scoped_array.hpp>

...

{
	int size = ... // Размер массива
	boost::scoped_array&lt;MyObject&gt; sobjects( new MyObject[ size ] ); // Выделяем память под массив и создаем указатель

	sobjects[0].myField++; // Доступ к элементу массива
	sobjects[0].myMethod();

	if ( spobjects ) { // Проверяем, что указатель ссылается на массив
		std::cout << "Указатель ссылается на массив по адресу " << spobjects.get() << std::endl; // Распечатываем адрес массива
	}	
} // А здесь память освобождается

shared_ptr и shared_array

shared_ptr так же хранит указатель на объект в памяти. Отличие от scoped_ptr состоит в том, что shared_ptr добавляет к объекту счетчик ссылок, каждое копирование указателя увеличивает счетчик ссылок, а уничтожение уменьшает. Объект удаляется только тогда, когда значение счетчика ссылок достигает 0. Пример использования:


#include <boost/shared_ptr.hpp>

...

boost::shared_ptr<MyObject> spobj1; // Создаем неинициализированный указатель

{
	boost::shared_ptr<MyObject> spobj2( new MyObject() ); // Создаем новый объект и указатель на него

	spobj2->myField ++; // Доступ к полю объекта
	spobj2->myMethod(); // Вызов метода объекта
	
	MyObject & robj = *spobj2; // Получаем ссылку на хранимый объект
	MyObject * pobj = spobj2.get(); // Получаем указатель на хранимый объект

	if ( spobj2 ) { // Проверяем, что указатель ссылается на объект
		std::cout << "Указатель ссылается на объект по адресу " << spobj2.get() << std::endl;
	}

	spobj1 = spobj2; // Копируем указатель
} // Здесь уничтожается spobj2, но объект не удаляется, потому что на него еще ссылается spobj1

spobj1->myMethod(); // Еще раз вызываем метод того же объекта

spobj1.reset(); // Здесь указатель spobj1 сбрасывается, и объект удаляется, потому что никаких указателей на него уже не ссылается
	

shared_array полностью аналогичен shared_ptr, только работает с массивами. Пример кода приводиться не будет, там и так все понятно, если посмотреть на shared_ptr и scoped_array.

Ошибки при использовании shared_ptr

Здесь я опишу некоторые ошибки которые можно допустить при работе с разделяемыми указателями.

Создание нескольких указателей на один объект без копирования

Пожалуй, самая распространенная ошибка. Рассмотрим следующий код:



MyObject pobj = new MyObject(); // Создаем новый объект

{ // Начинаем новый блок
	boost::shared_ptr<MyObject> sp1( pobj ); // Создаем разделяемый указатель на объект

	...

	boost::shared_ptr<MyObject> sp2( pobj ); //Создаем второй разделыяемый указатель на объект

	...
} // И вот здесь наша программа падает
Почему же программа должна упасть в конце блока? Создание двух таких указателей приводит к созданию двух счетчиков ссылок!! И каждый из них обнуляется в конце блока, что приводит к двум попыткам удаления объекта. Первая срабатывает, а вот вторая валит программу.

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

boost::shared_ptr<MyObject> pobj( new MyObject() ); // Создаем новый объект и сразу же разделяемый указатель на него

{ // Начинаем новый блок
	boost::shared_ptr<MyObject> sp1 = pobj; // Создаем разделяемый указатель на объект

	...

	boost::shared_ptr<MyObject> sp2 = pobj; //Создаем второй разделыяемый указатель на объект

	...

	pobj.reset(); // Освобождаем исходный указатель
} // Здесь объект корректно удаляется

Создание анонимных указателей как аргументов при вызове функций многих аргументов

Рассмотрим следующий код:

void f(shared_ptr<int>, int); // Какая-то функция, принимает разделяемый указатель и число
int g(); // Другая функция, генерирует число

void ok()
{
    shared_ptr<int> p(new int(2));
    f(p, g());
}

void bad()
{
    f(shared_ptr<int>(new int(2)), g());
}

Чем отличаются реализации ok и bad? Они выглядят эквивалентными. Но: порядок вычисления аргументов функций не определен! Это значит, что, возможно, что сначала выполнится new int(2), а затем g(), который може выкинуть исключение. Что тогда произойдет? Конструктор boost::shared_ptr не будет даже вызван, и объект int(2) останется висеть в памяти. В случае многократного повторения ситуации или если объекты крпупнее чем int это може привести к значительным утечкам памяти.

weak_ptr

Предположим ситуацию, когда имеются два объекта ссылающиеся друг на друга, и при этом необходимо использовать для них подсчет ссылок через shared_ptr. Соответственно, друг на друга они тоже ссылаются через shared_ptr. Что произойдет в этой ситуации? После создания объекты будут ссылаться друг на друга и никогда не будут удалены! Получаем утечку памяти.

Как решить проблему?

Для таки случаев в boost был включен weak_ptr. Это указатель, который так же как и shared_ptr связан со счетчиком ссылок, однако его особенность состоит в том, что он не увеличивает счетчика ссылок. Т.е, если одну из связей между объектами реализовать через weak_ptr, то объекты будут корректно удалены.

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

weak_ptr имеет метод lock(), который возвращает новый shared_ptr на объект, и, соответственно, на время жизни этого shared_ptr увеличивает счетчик ссылок объекта.

Типичный пример использования:

#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

...

boost::shared_ptr<MyObject> osptr( new MyObject() );

...

boost::weak_ptr<MyObject> owptr( osptr ); // Создаем слабый указатель на объект, на который ссылается osptr

...

// Пусть в этот момент на объект имелось n ссылок

if ( boost::shared_ptr<MyObject> po = owptr.lock() ) { // После вызова метода lock уже n+1 ссылка
	po->myMethod(); // Вызываем метод объекта
} else {
	std::cout << "Ошибка, объект уже удалили" << std::endl;
} 

// Здесь уже опять n ссылок, временный разделяемый указатель уничтожился

intrusive_ptr

Последний тип умных указателей - intrusive_ptr. Этот тип указателя используется, когда уже имеется некоторый механизм для подсчета ссылок на объекты, и необходимо задействовать его.

Для того, чтобы указатель работал необходимо, чтобы были определены две функции: intrusive_ptr_add_ref и intrusive_ptr_release, принимающие в качестве аргумента указатель на объект. Определны они должны быть в пространстве имен boost, или, если компилятор поддерживает поиск пространств имен по типам аргументов, в пространстве имен используемого объекта.

Пример использования intrusive_ptr:

#include <boost/intrusive_ptr.hpp>

namespace boost {

void intrusive_ptr_add_ref( const MyObject * po ) {
	po->addRef(); // Какая-та внутренняя реализация
}

void intrusive_ptr_release( const MyObject * po ) {
	po->release(); // Внутренняя реализация освобождения объекта
}

}

...

{
	boost::intrusive_ptr<MyObject> po( new MyObject() );

	po->myField ++; // Доступ к полю объекта
	po->myMethod(); // Вызов метода объекта
	
	MyObject & robj = *po; // Получаем ссылку на хранимый объект
	MyObject * pobj = po.get(); // Получаем указатель на хранимый объект

	if ( po ) { // Проверяем, что указатель ссылается на объект
		std::cout << "Указатель ссылается на объект по адресу " << po.get() << std::endl;
	}
}

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

 #  #  #  #  #  #  #  #  #  #

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