← На главную

Шпаргалка по использованию умных указателей в C++

Благодаря наличию исключений, язык C++ позволяет разделить основную логику приложения и обработку ошибок, не мешая их в одну кучу. Что есть очень хорошо. Однако теперь по коду нельзя с уверенностью сказать, где может быть прервано его исполнение. Отсюда возникает опасность утечки ресурсов. Проблема эта решается при помощи деструкторов и идиомы RAII. Впрочем, придерживаться этой идиомы становится проблематично при использовании указателей. Особенно при использовании их не как членов класса, а просто как переменных в методах. На наше с вами счастье, в стандартной библиотеке языка есть умные указатели (smart pointers), придуманные именно для этого случая. Поскольку на C++ я пишу не регулярно, то иногда забываю некоторые нюансы использования умных указателей, в связи с чем решил вот набросать небольшую шпаргалку.

Важно! В старых книжках и статьях можно встретить упоминание auto_ptr. Этот тип умных указателей появился в C++, когда в языке еще не было move semantics. Из-за этого использование auto_ptr порой может приводить к трудным в обнаружении ошибкам. В стандарте C++17 auto_ptr был удален. Другими словами, все, что вы должны знать об auto_ptr – это то, что его не должно быть в современном коде. Вместо него всегда используйте unique_ptr.

unique_ptr

Шаблонный класс unique_ptr представляет собой уникальный указатель на объект. Указатель нельзя копировать, но можно передавать владение им с помощью std::move. При уничтожении указателя автоматически вызывается деструктор объекта, на который он указывает.

Создается unique_ptr так:

std::unique_ptr<SomeClass> unq(new SomeClass(/* ctor args */));

… но обычно используют шаблон make_unique, так короче:

auto unq = std::make_unique<SomeClass>(/* ctor args */);

Класс unique_ptr перегружает оператор ->, что позволяет обращаться к полям класса и вызывать его методы, словно мы работаем с обычным указателем:

unq->sayHello();

Как уже отмечалось, unique_ptr запрещено копировать:

// will not compile! auto cpy = unq;

Однако владение им можно передать при помощи std::move, например:

auto mov = std::move(unq); // unq is invalid now! mov->sayHello();

Плюс к этому, мы всегда можем получать из unique_ptr обычный указатель на объект:

SomeClass* ptr = mov.get(); ptr->sayHello();

… хотя это и является code smell. Кроме того, ничто не мешает создавать ссылки (reference) на unique_ptr:

auto& ref = mov; ref->sayHello();

То есть, в этом случае мы как бы не отнимаем владение объектом, а ненадолго одалживаем его, обращаясь к нему через все тот же умный указатель.

Интересно, что unique_ptr позволяет указать функцию, которую он будет вызывать вместо деструктора, так называемый custom deleter. Это позволяет использовать unique_ptr с ресурсами, возвращаемых из библиотек для языка C, и даже реализовать аналог defer из языка Go:

/* g++ custom-deleter.cpp -o custom-deleter */ #include <memory> #include <functional> #include <iostream> #include <stdio.h> template<typename T> using auto_cleanup = std::unique_ptr<T,std::function<void(T*)>>; static char dummy[] = ""; #define _DEFER_CAT_(a,b) a##b #define _DEFER_NAME_(a,b) _DEFER_CAT_(a,b) #define defer(...) \ auto _DEFER_NAME_(_defer_,__LINE__) = \ auto_cleanup<char>(dummy, [&](char*) { __VA_ARGS__; }); int main() { auto_cleanup<FILE> f( fopen("test.txt", "w"), [](FILE* f) { fclose(f); } ); defer( std::cout << "Bye #1" << std::endl ); defer( std::cout << "Bye #2" << std::endl ); fwrite("Hello!\n", 7, 1, f.get()); }

Заметьте, что в макросе defer нам пришлось передать в unique_ptr фиктивный указатель. Если бы мы передали nullptr, custom deleter не был бы вызван.

Важно! Если в умном указателе вы держите указать на массив объектов, то обязаны указать custom deleter, вызывающий для этого массива delete[] вместо delete. Если этого не сделать, будет освобожден только первый объект из массива, остальные же утекут.

shared_ptr и weak_ptr

Класс shared_ptr является указатем на объект, которым владеет сразу несколько объектов. Указатель можно как перемещать, так и копировать. Число существующих указателей отслеживается при помощи счетчика ссылок. Когда счетчик ссылок обнуляется, вызывается деструктор объекта. Сам по себе shared_ptr является thread-safe, но он не делает магическим образом thread-safe объект, на который ссылается. То есть, если доступ к объекту может осуществляться из нескольких потоков, вы должны не забыть предусмотреть в нем мьютексы или что-то такое.

Для создания shared_ptr обычно используется шаблон make_shared:

auto ptr = std::make_shared<SomeClass>(/* ctor args */);

В остальном работа с ним мало отличается от работы с unique_ptr, за тем исключением, что shared_ptr можно смело копировать.

Интересные грабли при использовании shared_ptr заключается в том, что с его помощью можно создать циклические ссылки. Например, есть два объекта. Первый ссылается при помощи shared_ptr на второй, а второй – на первый. Даже если ни на один из объектов нет других ссылок, счетчики ссылок никогда не обнулятся, и объекты никогда не будут уничтожены.

Эта проблема обходится при помощи weak_ptr, так называемого слабого указателя. Класс weak_ptr похож на shared_ptr, но не участвует в подсчете ссылок. Также у weak_ptr есть метод lock(), возвращающий временный shared_ptr на объект. Пример использования:

#include <memory> #include <iostream> class SomeClass { public: void sayHello() { std::cout << "Hello!" << std::endl; } ~SomeClass() { std::cout << "~SomeClass" << std::endl; } }; int main() { std::weak_ptr<SomeClass> wptr; { auto ptr = std::make_shared<SomeClass>(); wptr = ptr; if(auto tptr = wptr.lock()) { tptr->sayHello(); } else { std::cout << "lock() failed" << std::endl; } } if(auto tptr = wptr.lock()) { tptr->sayHello(); } else { std::cout << "lock() failed" << std::endl; } }

Программа выведет:

Hello! ~SomeClass lock() failed

Можно думать о weak_ptr как об указателе, позволяющим получить временное владение объектом. Само собой разумеется, если все постоянные указатели на объект перестанут существовать, и останутся только временные, полученные при помощи метода lock() класса weak_ptr, объект продолжит свое существование. Он будет уничтожен только тогда, когда на объект не останется вообще никаких указателей.

Умные указатели и наследование

Вопрос, о котором часто забывают – это кастование умных указателей вверх и вниз по иерархии классов. Для shared_ptr в стандартной библиотеке есть шаблоны static_pointer_cast, dynamic_pointer_cast и другие. Для unique_ptr таких же шаблонов почему-то не занесли, но их нетрудно найти на StackOverflow.

Пример кода:

#include <memory> #include <iostream> // https://stackoverflow.com/a/21174979/1565238 template<typename Derived, typename Base, typename Del> std::unique_ptr<Derived, Del> static_unique_ptr_cast( std::unique_ptr<Base, Del>&& p ) { auto d = static_cast<Derived *>(p.release()); return std::unique_ptr<Derived, Del>(d, std::move(p.get_deleter())); } template<typename Derived, typename Base, typename Del> std::unique_ptr<Derived, Del> dynamic_unique_ptr_cast( std::unique_ptr<Base, Del>&& p ) { if(Derived *result = dynamic_cast<Derived *>(p.get())) { p.release(); return std::unique_ptr<Derived, Del>(result, std::move(p.get_deleter())); } return std::unique_ptr<Derived, Del>(nullptr, p.get_deleter()); } class Base { public: Base(int num): num(num) {}; virtual void sayHello() { std::cout << "I'm Base #" << num << std::endl; } virtual ~Base() { std::cout << "~Base #" << num << std::endl; } protected: int num; }; class Derived: public Base { public: Derived(int num): Base(num) {} virtual void sayHello() { std::cout << "I'm Derived #" << num << std::endl; } virtual ~Derived() { std::cout << "~Derived #" << num << std::endl; } }; void testUnique() { std::cout << "=== testUnique begin ===" << std::endl; auto derived = std::make_unique<Derived>(1); derived->sayHello(); std::unique_ptr<Base> base = std::move(derived); base->sayHello(); auto derived2 = static_unique_ptr_cast<Derived>(std::move(base)); derived2->sayHello(); std::unique_ptr<Base> base2 = std::make_unique<Derived>(2); base2->sayHello(); std::cout << "=== testUnique end ===" << std::endl; } void testShared() { std::cout << "=== testShared begin ===" << std::endl; auto derived = std::make_shared<Derived>(1); derived->sayHello(); auto base = std::static_pointer_cast<Base>(derived); base->sayHello(); auto derived2 = std::static_pointer_cast<Derived>(base); derived2->sayHello(); std::shared_ptr<Base> base2 = std::make_shared<Derived>(2); base2->sayHello(); std::cout << "=== testShared end ===" << std::endl; } int main() { testUnique(); testShared(); }

Как видите, все оказалось не так уж и сложно.

Заключение

По моим представлениям, приведенной шпаргалки должно хватать в ~99% реальных задач. В оставшемся же 1% случаев вам поможет документация на cppreference.com. Впрочем, я не являюсь гуру C++, и вполне мог о чем-то забыть или чего-то не учесть. Если вы видите в приведенном тексте какие-либо косяки, не стесняйтесь сообщить мне об этом.