Шпаргалка по использованию умных указателей в C++
26 февраля 2018
Благодаря наличию исключений, язык 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 так:
… но обычно используют шаблон make_unique, так короче:
Класс unique_ptr перегружает оператор ->
, что позволяет обращаться к полям класса и вызывать его методы, словно мы работаем с обычным указателем:
Как уже отмечалось, unique_ptr запрещено копировать:
auto cpy = unq;
Однако владение им можно передать при помощи std::move, например:
// unq is invalid now!
mov->sayHello();
Плюс к этому, мы всегда можем получать из unique_ptr обычный указатель на объект:
ptr->sayHello();
… хотя это и является code smell. Кроме того, ничто не мешает создавать ссылки (reference) на unique_ptr:
ref->sayHello();
То есть, в этом случае мы как бы не отнимаем владение объектом, а ненадолго одалживаем его, обращаясь к нему через все тот же умный указатель.
Интересно, что unique_ptr позволяет указать функцию, которую он будет вызывать вместо деструктора, так называемый custom deleter. Это позволяет использовать unique_ptr с ресурсами, возвращаемых из библиотек для языка C, и даже реализовать аналог defer из языка Go:
#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:
В остальном работа с ним мало отличается от работы с unique_ptr, за тем исключением, что shared_ptr можно смело копировать.
Интересные грабли при использовании shared_ptr заключается в том, что с его помощью можно создать циклические ссылки. Например, есть два объекта. Первый ссылается при помощи shared_ptr на второй, а второй — на первый. Даже если ни на один из объектов нет других ссылок, счетчики ссылок никогда не обнулятся, и объекты никогда не будут уничтожены.
Эта проблема обходится при помощи weak_ptr, так называемого слабого указателя. Класс weak_ptr похож на shared_ptr, но не участвует в подсчете ссылок. Также у weak_ptr есть метод lock()
, возвращающий временный shared_ptr на объект. Пример использования:
#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;
}
}
Программа выведет:
~SomeClass
lock() failed
Можно думать о weak_ptr как об указателе, позволяющим получить временное владение объектом. Само собой разумеется, если все постоянные указатели на объект перестанут существовать, и останутся только временные, полученные при помощи метода lock()
класса weak_ptr, объект продолжит свое существование. Он будет уничтожен только тогда, когда на объект не останется вообще никаких указателей.
Умные указатели и наследование
Вопрос, о котором часто забывают — это кастование умных указателей вверх и вниз по иерархии классов. Для shared_ptr в стандартной библиотеке есть шаблоны static_pointer_cast, dynamic_pointer_cast и другие. Для unique_ptr таких же шаблонов почему-то не занесли, но их нетрудно найти на StackOverflow.
Пример кода:
#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++, и вполне мог о чем-то забыть или чего-то не учесть. Если вы видите в приведенном тексте какие-либо косяки, не стесняйтесь сообщить мне об этом.
Метки: C/C++.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.