Главная / Курсы / C++ по спирали / Глава 15. Указатели / Знакомство с умными указателями
# Глава 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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!