Введение в 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 и он был доступен в пространстве имен tr1std::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++ четко декларирует, что временные объекты уничтожаются, в случае появления исключения.

Note

К слову, 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.

Hint

Почему образован цикл? Давайте разберемся. При выходе из блока (в данном случае функции 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<>, но он в новый стандарт включен не был.