# Глава 15.7 Знакомство с умными указателями
Работать с указателями _неудобно и опасно,_ и в предыдущих главах вы прочувствовали это. Управлять через указатели памятью — тоже _неудобно и опасно._ Чрезвычайно легко совершить ошибку:
- Выделить память и обратиться за её пределы. Это UB.
- Выделить память и забыть её освободить. Это утечка памяти.
- Освободить память, а потом обратиться по адресу этой памяти. Это UB, которое скорее всего закончится повреждением памяти.
- Освободить выделенную память дважды. Это UB и наверняка повреждение памяти.
Нужно постоянно держать в голове, что операции выделения и освобождения памяти — парные. Например, смешивание `new T[n]` и обычного `delete` вместо `delete[]` — чрезвычайно распространенная ошибка.
Указатели часто называют **сырыми указателями** (raw pointers) из-за низкоуровневого доступа к адресам памяти и необходимости самостоятельно этой памятью управлять. В современном C++ их нужно всеми силами [избегать.](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#rr-raii) Альтернатива — **умные указатели** (smart pointers), снимающие с разработчика ответственность за освобождение памяти.
## Мотивация
Над сырым указателем напрашивается [RAII](/courses/cpp/chapters/cpp_chapter_0055/#block-raii)-обёртка, в которой:
- Конструктор инициализирует поле-указатель на объект.
- Деструктор разрушает этот объект и освобождает выделенную под него память.
Эту задумку можно представить как шаблонный класс:
```cpp
template <typename T>
class Pointer
{
public:
Pointer(T * p)
{
// Инициализирует ptr
}
~Pointer()
{
// Вызывает delete ptr либо delete[] ptr
}
T * get()
{
// Возвращает сырой указатель
}
private:
T * ptr;
};
```
К счастью, в стандартной библиотеке найдётся нечто похожее: три RAII-класса умных указателей, реализующих разные модели владения.
## Концепция владения
В C++ концепция владения (ownership) определяет, какой объект несёт ответственность за жизненный цикл ресурса. Владелец — это объект, который должен освободить ресурс, когда он больше не нужен. Например, вызвать `delete` или `delete[]` для освобождения памяти. В соответствии с RAII, когда владелец выходит из области видимости или генерируется исключение, ресурс освобождается.
Умные указатели реализуют три вида владения:
- _Эксклюзивное._ У ресурса только один владелец.
- _Совместное_ (разделяемое). У ресурса может быть много владельцев. При этом ведётся подсчёт ссылок. Владелец, оставшийся последним, обнуляет счётчик ссылок и уничтожает ресурс.
- _Слабое_ (наблюдение без владения). Объект не считается владельцем ресурса и не удерживает его от удаления, но имеет к нему доступ.
## Какие бывают умные указатели
Начиная с C++11 доступно три варианта указателей:
- `std::unique_ptr` для _эксклюзивного_ владения объектом. В каждый момент времени на объект может указывать только один `std::unique_ptr`.
- `std::shared_ptr` для _совместного_ владения объектом. Он ведёт подсчёт ссылок и удаляет объект, когда у него не остается владельцев.
- `std::weak_ptr` для наблюдения за объектом, на который указывает `std::shared_ptr`. Он не увеличивает счётчик владения и применяется для предотвращения циклических зависимостей. Они возникают, когда объекты ссылаются друг на друга через `std::shared_ptr`, из-за чего никогда не удаляются.
В старом коде вы также можете встретить класс `std::auto_ptr`, но в C++17 он был удалён из Стандарта. Ему на замену пришёл `std::unique_ptr`.
Сценарии применения трёх классов умных указателей различаются, но _суть_ у них общая: автоматическое удаление объекта в динамической памяти при выходе из области видимости или при исключениях. Бонусом умные указатели повышают читабельность кода: благодаря «говорящим» названиям сразу понятно, какая у объекта модель владения.
По производительности и потреблению памяти `std::shared_ptr` уступает сырым указателям, потому что ведёт подсчёт ссылок. Зато `std::unique_ptr` в этом плане практически от них не отличается.
## Класс std::unique_ptr и функция std::make_unique()
Шаблонный класс [std::unique_ptr](https://en.cppreference.com/w/cpp/memory/unique_ptr.html) реализует модель эксклюзивного владения: в каждый момент времени на объект ссылается только один `std::unique_ptr`. Это означает, что:
- Указатель `std::unique_ptr` нельзя копировать.
- Зато его можно перемещать — передавать владение объектом другому `std::unique_ptr`.
### Инициализация std::unique_ptr
По умолчанию `std::unique_ptr` инициализируется значением `nullptr`:
```cpp {.example_for_playground .example_for_playground_001}
std::unique_ptr<int> p;
std::println("{}", p == nullptr);
```
```
true
```
Чтобы присвоить указатель адрес объекта, нужно выделить для него память через `new`:
```cpp {.example_for_playground .example_for_playground_002}
std::unique_ptr<int> p{new int(16)};
std::println("{}", *p);
```
```
16
```
Здесь мы создали в динамической памяти переменную типа `int`, присвоили ей значение 16 и создали на неё умный указатель.
В общем виде создание умного указателя на объект выглядит так:
```cpp
std::unique_ptr<T> p{new T{}};
```
А это — создание указателя на сишный массив:
```cpp {.example_for_playground .example_for_playground_003}
std::unique_ptr<int[]> p_arr(new int[8]{});
p_arr[1] = 5;
std::println("{} {}", p_arr[0], p_arr[1]);
```
Тип массива передаётся параметром шаблона класса `std::unique_ptr`. Поэтому при удалении объекта умный указатель корректно вызовет `delete[]`, а не `delete`.
К главе про динамическое выделение памяти мы [написали](/courses/cpp/chapters/cpp_chapter_0154/#block-semver) простой класс `SemVer`. Создадим умный указатель на его объект. Заведём вложенный блок кода, чтобы убедиться, что объект разрушается при выходе из области видимости:
```cpp {.example_for_playground .example_for_playground_004}
int main()
{
{
std::unique_ptr<SemVer> p_ver{new SemVer(5, 0, 11)};
p_ver->print();
}
std::println("Exiting main");
}
```
```
Parameterized constructor: 5.0.11
Version: 5.0.11
Destructor: 5.0.11
Exiting main
```
Когда внутри функции создаётся `std::unique_ptr`, сам он размещается на стеке. А объект, которым владеет умный указатель, аллоцируется в динамической памяти:
```
Стек Куча
std::unique_ptr<T> Объект типа T
┌────────────────┐ ┌─────────────┐
│ 0x... │────────────>│ Значение │
└────────────────┘ └─────────────┘
```
Вы пишете библиотеку для сохранения скриншотов. У вас есть сырой массив пикселей. Вы обернули его в `std::unique_ptr`, чтобы гарантировать очистку памяти, выделенной через `new[]`. Но в объявлении умного указателя допущена ошибка. Исправьте её. {.task_text}
```cpp {.task_source #cpp_chapter_0157_task_0010}
void capture_screenshot()
{
// Представьте, что вместо вызова new здесь вызов функции
// для получения пикселей экрана
Pixel * pixels = new Pixel[3];
// Найдите и исправьте ошибку в объявлении unique_ptr
std::unique_ptr<Pixel> pixels_ptr(pixels);
// ...Код для сохранения скриншота
}
```
Переменная `pixels` — это массив. {.task_hint}
```cpp {.task_answer}
void capture_screenshot()
{
Pixel * pixels = new Pixel[3];
// Создаём умный указатель на массив []
std::unique_ptr<Pixel[]> pixels_ptr(pixels);
}
```
### Функция std::make_unique()
Явный вызов `new` в конструкторе `std::unique_ptr` выглядит странновато. Мы же хотим отказаться от ручного выделения памяти, правда? Значит, логичнее скрыть `new` за удобным фасадом. И начиная с C++14 таким фасадом выступает шаблонная функция [std::make_unique()](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique).
Она выделяет память, конструирует объект и возвращает на него умный указатель:
```cpp
std::unique_ptr<T> p = std::make_unique<T>{};
```
В этом объявлении дважды фигурирует тип `T`. Если имя типа длинное, то это неудобно, и к тому же противоречит принципу [DRY](https://ru.wikipedia.org/wiki/Don%E2%80%99t_repeat_yourself) (Don't repeat yourself, не повторяйся). Так что многие разработчики при создании указателя через `std::make_unique()` явному указанию типа предпочитают ключевое слово `auto`. Компилятор выведет тип автоматически:
```cpp
auto p = std::make_unique<T>{}; // Тип p - std::unique_ptr<T>
```
Перепишем создание указателя на объект `SemVer` с помощью `std::make_unique()`:
```cpp {.example_for_playground .example_for_playground_005}
// Было:
// std::unique_ptr<SemVer> p_ver{new SemVer(5, 0, 11)};
// Стало:
auto p_ver = std::make_unique<SemVer>(5, 0, 11);
```
Так выглядит заведение умного указателя на сишный массив из 5-ти элементов:
```cpp {.example_for_playground .example_for_playground_006}
auto arr_ptr = std::make_unique<int[]>(5);
arr_ptr[1] = 5;
std::println("{} {}", arr_ptr[0], arr_ptr[1]);
```
У вызова `std::make_unique<T>()` вместо `new T` есть ещё одно достоинство: он гарантирует, что создание объекта и его передача во владение `std::unique_ptr` выполняются _как неделимая транзакция._ А значит, невозможна ситуация, при которой:
- объект уже создан, но ещё не передан умному указателю,
- генерируется исключение,
- появляется утечка памяти, потому что у объекта нет владельца, который его удалит.
В каких случаях это происходит? Допустим, мы вызвали некую функцию `copy()` и передали ей два временных объекта — указатели на экземпляры класса `File`:
```cpp
const bool success = copy(std::unique_ptr<File>(new File("A")),
std::unique_ptr<File>(new File("B")));
```
В C++ порядок вычисления аргументов функции **не определён.** Попросту нет единого правила, в какой последовательности вычислять передаваемые в функцию выражения. Компилятор сам решает, делать это слева направо, справа налево или в более хитром порядке для оптимизаций.
До C++17 Вполне возможна последовательность:
1. `new File("A")`
2. `new File("B")`. Если здесь вылетит исключение, то произойдёт утечка.
3. Конструктор `std::unique_ptr<File>(new File("A"))`.
4. Конструктор `std::unique_ptr<File>(new File("A"))`.
То есть до C++17 создание объектов `File()` в куче и создание `std::unique_ptr` могли быть разделены. Но начиная с C++17 вступили в силу [новые правила](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0145r3.pdf) уточнения порядка вычислений. Теперь Стандарт гарантирует, что выражения, инициализирующие разные аргументы функции, не могут перемешиваться. То есть создание первого `std::unique_ptr<File>(new File("A"))` (включая `new` и конструктор) должно быть полностью завершено до того, как начнётся вычисление другого аргумента. При этом порядок вычислений аргументов функции остается неопределённым.
Функция `std::make_unique()` защищает от подобных утечек, если проект компилируется со Стандартом до C++17. Эта гарантия безопасности называется _exception safety._
Перепишите объявление указателя `pixels_ptr` с использованием `std::make_unique()`. {.task_text}
```cpp {.task_source #cpp_chapter_0157_task_0020}
void capture_screenshot()
{
std::unique_ptr<Pixel[]> pixels_ptr(new Pixel[3]);
// ...
}
```
Синтаксис получения умного указателя `std::unique_ptr` на массив `T[]` из `n` элементов: `std::make_unique<T[]>(n)`. {.task_hint}
```cpp {.task_answer}
void capture_screenshot()
{
auto pixels_ptr = std::make_unique<Pixel[]>(3);
// ...
}
```
### Запрет на копирование std::unique_ptr
Умный указатель `std::unique_ptr` нельзя копировать, и это логично. Если бы копирование было разрешено, то два умных указателя считали бы себя единственными владельцами одной и той же области памяти. Для её освобождения каждый бы вызвал `delete`, что означало бы двойное освобождение памяти.
Объект копируется в двух случаях: при вызове конструктора копирования или оператора присваивания `=`. Оба помечены в классе `std::unique_ptr` удалёнными, чтобы их нельзя было использовать:
```cpp
namespace std
{
template<class T, class Deleter>
class unique_ptr
{
// Конструктор копирования запрещён
unique_ptr(const unique_ptr &) = delete;
// Оператор присваивания запрещён
unique_ptr& operator=(const unique_ptr &) = delete;
// ...
};
}
```
**Конструктор копирования** принимает параметр — константную ссылку на объект своего же типа. Попытка вызвать его для `std::unique_ptr` завершается ошибкой компиляции:
```cpp {.example_for_playground .example_for_playground_007}
std::unique_ptr<SemVer> p1 = std::make_unique<SemVer>(5, 0, 11);
// Вызываем конструктор копирования
std::unique_ptr<SemVer> p2 = p1;
```
```
main.cpp:35:29: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<SemVer>'
35 | std::unique_ptr<SemVer> p2 = p1;
| ^ ~~
```
Не дайте знаку равенства `=` себя запутать! Когда мы разбирали [копирующую инициализацию,](/courses/cpp/chapters/cpp_chapter_0121/#block-copy-initialization-difference) то пояснили, что инициализация _нового_ объекта с использованием другого объекта — это именно инициализация, а не присваивание. Вызывается конструктора копирования, а не оператор присваивания.
Вызов функции, которая принимает параметр по значению, тоже приводит к вызову конструктора копирования. Убедитесь в этом, взглянув на ошибку компиляции:
```cpp {.example_for_playground .example_for_playground_008}
void read_version(std::unique_ptr<SemVer> p_ver)
{
p_ver->print();
}
int main()
{
std::unique_ptr<SemVer> p = std::make_unique<SemVer>(5, 0, 11);
read_version(p); // Упс
}
```
```
main.cpp:41:14: error: call to implicitly-deleted copy constructor of 'std::unique_ptr<SemVer>'
41 | read_version(p);
| ^
```
Присваивание _ранее созданному_ объекту значения через `=` приводит к вызову **оператора присваивания.** Вместо инициализации нового объекта значение присваивается уже существующему. Вызов оператора присваивания для `std::unique_ptr` приводит к соответствующей ошибке компиляции:
```cpp {.example_for_playground .example_for_playground_009}
std::unique_ptr<SemVer> p1 = std::make_unique<SemVer>(5, 0, 11);
std::unique_ptr<SemVer> p2 = nullptr;
// Вызываем оператор присваивания
p2 = p1;
```
```
main.cpp:37:4: error: object of type 'std::unique_ptr<SemVer>' cannot be assigned because its copy assignment operator is implicitly deleted
37 | p2 = p1;
| ^ ^ ~~
```
### Перемещение std::unique_ptr
Экземпляры класса `std::unique_ptr` нельзя копировать, зато можно перемещать! Это единственный способ передать владение объектом от одного `std::unique_ptr` другому. **Перемещение** сводится к копированию адреса объекта из одного указателя в другой и обнулению старого. Деструкторы при этом не вызываются, память не аллоцируется и не освобождается. Перемещение — дешёвая операция.
Для перемещения используется функция [std::move()](https://en.cppreference.com/w/cpp/utility/move.html):
```cpp {.example_for_playground .example_for_playground_010}
std::unique_ptr<SemVer> p1 = std::make_unique<SemVer>(5, 0, 11);
std::unique_ptr<SemVer> p2 = std::move(p1);
p2->print();
std::println("p1 == nullptr ? {}", p1 == nullptr);
```
```
...
Version: 5.0.11
p1 == nullptr ? true
...
```
Важно помнить, что `std::unique_ptr`, из которого был перемещен объект, обнуляется. Попытка его разыменования приводит к UB.
```
┌─────── До перемещения из p1 ────────────────────────────────┐
│ │
│ std::unique_ptr<Semver> p1 Объект типа SemVer │
│ ┌─────────────────────────┐ ┌─────────────────┐ │
│ │ 0x... │────────────>│ Значение │ │
│ └─────────────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────── После перемещения p2 = std::move(p1) ────────────────┐
│ │
│ │
│ std::unique_ptr<Semver> p1 Объект типа SemVer │
│ ┌─────────────────────────┐ ┌─────────────────┐ │
│ │ nullptr │───>x ┌─>│ Значение │ │
│ └─────────────────────────┘ │ └─────────────────┘ │
│ │ │
│ std::unique_ptr<Semver> p2 │ │
│ ┌─────────────────────────┐ │ │
│ │ 0x... │──────────┘ │
│ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
Вы пишете систему управления задачами. У вас есть структура `Task` и класс `TaskManager`. Реализуйте его методы: {.task_text}
- `add_task()` добавляет указатель на задачу в очередь `tasks`. Если указатель нулевой, то метод ничего не делает.
- `get_task()` возвращает первую задачу в очереди. Если очередь пуста, то возвращается нулевой указатель.
- `task_count()` возвращает количество задач в очереди.
```cpp {.task_source #cpp_chapter_0157_task_0030}
struct Task
{
std::string title;
std::string description;
std::size_t id;
};
class TaskManager
{
public:
void add_task(std::unique_ptr<Task> task);
std::unique_ptr<Task> get_task();
std::size_t task_count();
private:
std::queue<std::unique_ptr<Task>> tasks;
};
// Ваш код
```
Не забудьте использовать `std::move()` для передачи владения умным указателем. На cppreference вы можете посмотреть список методов [std::queue](https://en.cppreference.com/cpp/container/queue). {.task_hint}
```cpp {.task_answer}
struct Task
{
std::string title;
std::string description;
std::size_t id;
};
class TaskManager
{
public:
void add_task(std::unique_ptr<Task> task);
std::unique_ptr<Task> get_task();
std::size_t task_count();
private:
std::queue<std::unique_ptr<Task>> tasks;
};
void TaskManager::add_task(std::unique_ptr<Task> task)
{
if (task != nullptr)
tasks.push(std::move(task));
}
std::unique_ptr<Task> TaskManager::get_task()
{
if (tasks.empty())
return nullptr;
std::unique_ptr<Task> task = std::move(tasks.front());
tasks.pop();
return task;
}
std::size_t TaskManager::task_count()
{
return tasks.size();
}
```
## Класс std::shared_ptr и функция std::make_shared()
Если `std::unique_ptr` организует эксклюзивное владение объектом, то [std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr.html) — совместное. Несколько `std::shared_ptr` могут указывать на один и тот же объект. Для этого внутри класса ведётся подсчёт ссылок. Узнать значение счётчика можно вызовом метода `use_count()`:
```cpp {.example_for_playground .example_for_playground_011}
std::shared_ptr<SemVer> p1(new SemVer(5, 0, 11));
std::shared_ptr<SemVer> p2 = p1; // Копирование разрешено
std::println("Use count: {}", p1.use_count());
```
```
...
Use count: 2
...
```
Когда на объект начинает указывать новый `std::shared_ptr`, счётчик увеличивается. Это происходит при копировании. А когда у одного из владельцев ресурса вызывается деструктор, счётчик уменьшается. При обнулении счётчика объект уничтожается.
```
std::shared_ptr<Semver> p1 Объект типа SemVer
┌─────────────────────────┐ ┌─────────────────┐
│ 0x... │────────────>│ Значение │
│ │ ┌─>│ │
└─────────────────────────┘ │ └─────────────────┘
│
std::shared_ptr<Semver> p2 │
┌─────────────────────────┐ │
│ 0x... │──────────┘
└─────────────────────────┘
```
### Как устроен std::shared_ptr
У класса `std::shared_ptr` есть два приватных поля. Это указатели на:
- Сам объект.
- Управляющий блок (control block). В нём хранится счётчик ссылок.
Изменение счётчика ссылок — атомарная операция, безопасно выполняющаяся в многопоточном коде. Из-за атомарных изменений счётчика `std::shared_ptr` работает чуть медленнее, чем `std::unique_ptr`. Разумеется, потокобезопасность не распространяется на сам объект под управлением `std::shared_ptr`. Для работы с ним из нескольких потоков нужно защищать его самостоятельно. Для этого есть примитивы синхронизации, которые мы обсудим в главах про конкурентное выполнение кода.
Объект `std::shared_ptr` занимает больше памяти, чем `std::unique_ptr`: он хранит два указателя, а не один, и аллоцирует дополнительное место под управляющий блок. Так выглядит схема, при которой на 1 объект типа `T` ссылаются 2 указателя `std::shared_ptr`. У них общий управляющий блок:
```
std::shared_ptr<T> p1
┌─────────────────────┐
│ T * ptr ──┼─────────────────────┐
├─────────────────────┤ │
│ ControlBlock * cb ──┼────────┐ │
└─────────────────────┘ │ │
│ │
std::shared_ptr<T> p2 │ │
┌─────────────────────┐ │ │
│ T * ptr ──┼─────────────────────┤
├─────────────────────┤ │ │
│ ControlBlock * cb ──┼────────┤ │
└─────────────────────┘ │ │
▼ ▼
ControlBlock Объект типа T
┌──────────────────┐ ┌───────────────┐
│ strong_refs = 2 │ │ Данные │
├──────────────────┤ └───────────────┘
│ Другие поля │
└──────────────────┘
```
### Функция std::make_shared()
Когда вы связываете `std::shared_ptr` с объектом с помощью вызова `new`, происходит две отдельные аллокации: под сам объект и под управляющий блок.
```cpp {.example_for_playground .example_for_playground_012}
std::shared_ptr<SemVer> p(new SemVer(5, 0, 11));
p->print();
```
Если `new` заменить на функцию `std::make_shared()`, то под объект и управляющий блок аллоцируется единая область памяти. Это работает быстрее, чем два выделения разных блоков памяти. Также это более выгодно с точки зрения локальности данных и дрюжелюбности к кешу процессора.
Как и в случае с `std::make_unique()`, функция `std::make_shared()` даёт гарантию безопасности от утечек памяти при исключениях (exception safety). Обе функции называют _фабричными._ **Фабричная функция** инкапсулирует процесс создания объекта и возвращает его экземпляр.
## Класс std::weak_ptr
Порой в коде появляются циклические зависимости, когда два `std::shared_ptr` «смотрят» друг на друга, их счётчики не обнуляются, и поэтому указатели не могут быть удалены. Такие «мёртвые петли» неизбежны между узлами двусвязных списков, деревьев и графов:
```
std::shared_ptr
┌─────────┐ strong_refs = 1 ┌─────────┐
│ Node А │───────────────────► Node B │
│ ◄───────────────────┤ │
└─────────┘ std::shared_ptr └─────────┘
strong_refs = 1
▲ ▲
│ │
└───────────────────────────┘
Циклическая зависимость
```
На иллюстрации петля возникла между соседними узлами списка. Но петля может быть более длинной. И если будет утерян указатель на подобную петлю, то произойдёт утечка памяти.
Чтобы избавиться от циклической зависимости, тип одного из указателей заменяется на `std::weak_ptr`. Класс [std::weak_ptr](https://en.cppreference.com/w/cpp/memory/weak_ptr.html) используется в связке с `std::shared_ptr` и не увеличивает его счётчик ссылок:
```
std::shared_ptr
┌─────────┐ strong_refs = 1 ┌─────────┐
│ Node А │───────────────────► Node B │
│ ◄─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤ │
└─────────┘ std::weak_ptr └─────────┘
▲ ▲
│ │
└ ─ ─ ─ ─ ─ ─ X ─ ─ ─ ─ ─ ─ ┘
Циклическая зависимость
разорвана
```
Класс `std::weak_ptr` не даёт прямого доступа к объекту. К нему не применимы операторы разыменования `*` или обращения к члену класса `->`.
Вместо этого есть метод `lock()`, который проверяет, жив объект или уже удалён. Если объект удалён, метод возвращает обнулённый `std::shared_ptr`. Если объект ещё существует, то метод возвращает указывающий на него `std::shared_ptr`.
```cpp {.example_for_playground .example_for_playground_013}
auto p_owner = std::make_shared<int>(1024);
// Вызываем конструктор weak_ptr от shared_ptr
std::weak_ptr<int> p_weak = p_owner;
// ...Где-то в другом месте по коду
if (auto p_shared = p_weak.lock(); p_shared == nullptr)
std::println("Object is already destroyed");
else
std::println("Object's data: {}", *p_shared);
```
Вы пишете движок для игры. В ней есть тяжёлые объекты: текстуры `Texture`, которые загружаются по имени. Чтобы многократно не подгружать одну и ту же текстуру, вы завели кеш `TextureCache`. {.task_text}
Реализуйте метод класса `get_texture()`. Он работает с кешем, хранящим указатели `std::weak_ptr` на текстуры: {.task_text}
- Если текстуры нет в кеше, метод создаёт `std::shared_ptr` на новую текстуру, кеширует слабый указатель на неё и возвращает `std::shared_ptr`.
- Если текстура уже есть в кеше и её `std::weak_ptr` не нулевой, метод возвращает `std::shared_ptr`, полученный из `std::weak_ptr`.
```cpp {.task_source #cpp_chapter_0157_task_0040}
struct Texture
{
std::string name;
std::vector<unsigned char> data;
};
class TextureCache
{
public:
std::shared_ptr<Texture> get_texture(const std::string & name);
private:
std::unordered_map<std::string, std::weak_ptr<Texture>> cache;
};
// Реализация метода get_texture()
```
Воспользуйтесь методом [find()](https://en.cppreference.com/cpp/container/unordered_map/find) класса `std::unordered_map`, чтобы определить, находится ли текстура в кеше. Если текстура найдена, получите из её слабого указателя `std::shared_ptr` вызовом метода `lock()`. Если полученный указатель не нулевой, верните его. В остальных случаях текстуры либо вообще не было в кеше, либо она уже удалена. Нужно создать её через `std::make_shared()`, положить в кеш и вернуть из метода. {.task_hint}
```cpp {.task_answer}
struct Texture
{
std::string name;
std::vector<unsigned char> data;
};
class TextureCache
{
public:
std::shared_ptr<Texture> get_texture(const std::string & name);
private:
std::unordered_map<std::string, std::weak_ptr<Texture>> cache;
};
std::shared_ptr<Texture> TextureCache::get_texture(const std::string & name)
{
if (auto it = cache.find(name); it != cache.end())
{
// Пытаемся получить shared_ptr из weak_ptr
if (auto shared = it->second.lock(); shared != nullptr)
return shared;
}
// Текстуры нет или она уже удалена: создаём её
auto texture = std::make_shared<Texture>(name);
// Кешируем: сохраняем как weak_ptr
cache[name] = texture;
return texture;
}
```
## Советы по использованию умных указателей
- Всегда предпочитайте умные указатели сырым.
- Если вы работаете с сишной библиотекой, не позволяйте сырым указателям расползтись по коду вашего проекта: изолируйте их и оборачивайте в умные указатели. Если потребуется, пишите RAII-классы - обёртки.
- Для передачи умного указателя в сишную функцию используйте метод [get()](https://en.cppreference.com/cpp/memory/unique_ptr/get). Он есть у классов `std::unique_ptr` и `std::shared_ptr` и возвращает сырой указатель на объект.
----------
## Резюме
- Обычные указатели иногда называют сырыми указателями, потому что они предоставляют низкоуровневый доступ к памяти с необходимостью вручную её освобождать.
- Умные указатели — это RAII-обёртки над сырыми указателями для автоматического контроля времени жизни объекта.
- `std::unique_ptr` реализует модель эксклюзивного владения объектом, а `std::shared_ptr` — совместного.
- Указатель `std::unique_ptr` нельзя копировать, но можно перемещать функцией `std::move()`.
- Фабричные функции `std::make_unique()` и `std::make_shared()` инициализируют указатели соответствующих типов. Они гарантируют безопасность от утечек памяти при исключениях (exception safety) и соответствуют принципу DRY.
- `std::make_shared()` выделяет память под сам объект и под счётчик ссылок единым блоком. Если использовать `new`, произойдёт две отдельных аллокации в разных местах памяти, что менее эффективно.
- Если два и более объекта ссылаются друг на друга через `std::shared_ptr`, возникает циклическая зависимость, и эти объекты не могут быть удалены.
- `std::weak_ptr` предотвращает циклические зависимости. Он нужен для _наблюдения_ за указателем, которым _владеет_ `std::shared_ptr`.
Следующие главы находятся в разработке
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!