Введение в C++11: умные указатели
Продолжу доброю традицию и расскажу сегодня об умных указателях, также известных как Smart Pointers. Умные указатели очень актуальны в мире C++ и новый стандарт не обошел их стороной.
Smart pointer — это объект, работать с которым можно как с обычным указателем, но при этом, в отличии от последнего, он предоставляет некоторый дополнительный функционал (например, автоматическое освобождение закрепленной за указателем области памяти).
Умные указатели призваны для борьбы с утечками памяти, которые сложно избежать в больших проектах. Они особенно удобны в местах, где возникают исключения, так как при последних происходит процесс раскрутки стека и уничтожаются локальные объекты. В случае обычного указателя — уничтожится переменная-указатель, при этом ресурс останется не освобожденным. В случае умного указателя — вызовется деструктор, который и освободит выделенный ресурс.
В новом стандарте появились следующие умные указатели:
unique_ptr
, shared_ptr
иweak_ptr
. Все они объявлены в заголовочном файле <memory>
.unique_ptr
Этот указатель пришел на смену старому и проблематичному
auto_ptr
. Основная проблема последнего заключается в правах владения. Объект этого класса теряет права владения ресурсом при копировании (присваивании, использовании в конструкторе копий, передаче в функцию по значению).std::auto_ptr<int> x_ptr(new int(42)); std::auto_ptr<int> y_ptr; // вот это нехороший и неявный момент // права владения ресурсов уходят в y_ptr и x_ptr начинает // указывать на null pointer y_ptr = x_ptr; // segmentation fault std::cout << *x_ptr << std::endl;
Это очень неудобно, при работе с контейнером из умных указателей. Банальное
std::vector<std::auto_ptr<int> > vec; // ... std::auto_ptr<int> tmp = vec[0];
сделает элемент вектора невалидным. Именно поэтому данный класс не пользовался популярностью среди разработчиков.
В отличии от
auto_ptr
, unique_ptr
запрещает копирование.std::unique_ptr<int> x_ptr(new int(42)); std::unique_ptr<int> y_ptr; // ошибка при компиляции y_ptr = x_ptr; // ошибка при компиляции std::unique_ptr<int> z_ptr(x_ptr);
Изменение прав владения ресурсом осуществляется с помощью вспомогательной функции
std::move
(которая является частью механизма перемещения).std::unique_ptr<int> x_ptr(new int(42)); std::unique_ptr<int> y_ptr; // также, как и в случае с ``auto_ptr``, права владения переходят // к y_ptr, а x_ptr начинает указывать на null pointer y_ptr = std::move(x_ptr);
Как
auto_ptr
, так и unique_ptr
обладают методами reset()
, который сбрасывает права владения, и get()
, который возвращает сырой (классический) указатель.std::unique_ptr<Foo> ptr = std::unique_ptr<Foo>(new Foo); // получаем классический указатель Foo *foo = ptr.get(); foo->bar(); // сбрасываем права владения ptr.reset();
Как видно,
unique_ptr
недалеко ушел от своего предшественника в плане удобства использования, но, во всяком случае, он обезопасил от неявных смен прав владений ресурсом.shared_ptr
Это самый популярный и самый широкоиспользуемый умный указатель. Он начал своё развитие как часть библиотеки boost. Данный указатель был столь успешным, что его включили в C++ Technical Report 1 и он был доступен в пространстве имен
tr1
—std::tr1::shared_ptr<>
.
В отличии от рассмотренных выше указателей,
shared_ptr
реализует подсчет ссылок на ресурс. Ресурс освободится тогда, когда счетчик ссылок на него будет равен 0. Как видно, система реализует одно из основных правил сборщика мусора.std::shared_ptr<int> x_ptr(new int(42)); std::shared_ptr<int> y_ptr(new int(13)); // после выполнения данной строчки, ресурс // на который указывал ранее y_ptr (int(13)) освободится, // а на int(42) будут ссылаться оба указателя y_ptr = x_ptr; std::cout << *x_ptr << "\t" << *y_ptr << std::endl; // int(42) освободится лишь при уничтожении последнего ссылающегося // на него указателя
Также как и
unique_ptr
, и auto_ptr
, данный класс предоставляет методы get()
иreset()
.auto ptr = std::make_shared<Foo>(); Foo *foo = ptr.get(); foo->bar(); ptr.reset();
При работе с умным указателем, следует опасаться их создания на лету. Например, следующий код может привести к утечки памяти.
someFunction(std::shared_ptr<Foo>(new Foo), getRandomKey());
Почему? Да потому, что стандарт C++ не определяет порядок вычисления аргументов. Может случиться так, что сначала выполнится
new Foo
, затем getRandomKey()
и лишь затем конструктор shared_ptr
. Если же функция getRandomKey()
бросит исключение, до конструктора shared_ptr
дело не дойдет, хотя ресурс (объект Foo) был уже выделен.
В случае с
shared_ptr
есть выход — использовать фабричную функциюstd::make_shared<>
, которая создает объект заданного типа и возвращаетshared_ptr
указывающий на него.someFunction(std::make_shared<Foo>(), getRandomKey());
Почему и как это работает? Очень просто. Как я уже сказал выше, make_shared возвращает
shared_ptr
. Этот результат является временным объектом, а стандарт C++ четко декларирует, что временные объекты уничтожаются, в случае появления исключения.
К слову,
new Foo
тоже возвращает временный объект. Однако, временным является указатель на выделенный ресурс, и в случае исключения — уничтожится указатель, при этом ресурс останется выделенным.weak_ptr
Этот указатель также, как и
shared_ptr
начал свое рождение в проекте boost, затем был включен в C++ Technical Report 1 и, наконец, пришел в новый стандарт.
Данный класс позволяет разрушить циклическую зависимость, которая, несомненно, может образоваться при использовании
shared_ptr
. Предположим, есть следующая ситуация (переменные-члены не инкапсулированы для упрощения кода)class Bar; class Foo { public: Foo() { std::cout << "Foo()" << std::endl; } ~Foo() { std::cout << "~Foo()" << std::endl; } std::shared_ptr<Bar> bar; }; class Bar { public: Bar() { std::cout << "Bar()" << std::endl; } ~Bar() { std::cout << "~Bar()" << std::endl; } std::shared_ptr<Foo> foo; }; int main() { auto foo = std::make_shared<Foo>(); foo->bar = std::make_shared<Bar>(); foo->bar->foo = foo; return 0; }
Как видно, объект
foo
ссылается на bar
и наоборот. Образован цикл, из-за которого не вызовутся деструкторы объектов. Для того чтобы разорвать этот цикл, достаточно в классеBar
заменить shared_ptr
на weak_ptr
.
Почему образован цикл? Давайте разберемся. При выходе из блока (в данном случае функции
main()
) уничтожаются локальные объекты. Локальным объектом является foo
. При уничтожении foo
счетчик ссылок на его ресурс уменьшится на единицу. Однако, ресурс освобожден не будет, так как на него есть ссылка со стороны ресурса bar
. А наbar
есть ссылка со стороны того же ресурса foo
.weak_ptr
не позволяет работать с ресурсом напрямую, но зато обладает методомlock()
, который генерирует shared_ptr()
.std::shared_ptr<Foo> ptr = std::make_shared<Foo>(); std::weak_ptr<Foo> w(ptr); if (std::shared_ptr<Foo> foo = w.lock()) { foo->doSomething(); }
Вместо заключения
Умные указатели — очень удобная и полезная вещь, но я рассмотрел их поверхностно, лишь их концептуальные части. За полным списком их возможностей следует обращаться к документации.
Стоит отметить, что рассмотренные мною умные указатели (кроме
unique_ptr
) не предназначен для владения массивами. Это связано с тем, что деструктор вызывает именноdelete
, а не delete[]
(что требуется для массивов).
Для
unique_ptr
мы имеем дело с предопределенной специализацией для массивов. Для ее использования необходимо указать []
возле параметра шаблона. Выглядит это так.std::unique_ptr<Foo[]> arr(new Foo[2]); arr[0].doSomething();
Кроме этого, в boost есть специальный класс
shared_array<>
, но он в новый стандарт включен не был.
-----------------------
Комментариев нет:
Отправить комментарий