# Конструкторы `std::shared_ptr` С появления C++11 прошло уже больше 10 лет, так что большинство C++-разработчиков уже все-таки знают про умные указатели. Отдаешь им владение сырым указателем и спишь спокойно — память будет освобождена. И все хорошо. И даже разницу между `std::unique_ptr` и `std::shared_ptr` они знают. Хотя, конечно, у меня был пару лет назад кандидат на собеседовании, который этой разницы не знал, потому что не пользовался STL... Некопируемый, уникально владеющий `std::unique_ptr` просто хранит указатель (и, возможно, функцию очистки — deleter) и в своем деструкторе этот `deleter` вызывает против сохраненного указателя. `std::shared_ptr` же хитрее, и для поддержания разделяемого владения между копиями ему нужен счетчик ссылок. Ну это все знают. Ничего интересного. Давайте просто пользоваться и не думать. Удивительно, но на практике в C++ довольно часто встречается ситуация, когда нам нужно описать некую сущность, которую ни в коем случае нельзя создавать на стеке. Обязательно она должна быть в куче. Простейший пример: нам нужен потокобезопасный объект, который будет внутри защищен мьютексом/атомарными переменными, и мы хотим, чтоб этот объект был свободно перемещаем из контейнера в контейнер, со стека в контейнер и обратно. А `std::mutex` и `std::atomic` конструкторов перемещения не имеют. И у нас два варианта действий в этом случае: ```C++ class MyComponent1 { ComponentData data_; // сделать неперемещаемое поле перемещаемым, добавив к нему // слой индирекции и отправив данные в кучу std::unique_ptr data_mutex_; }; // как-то заставить пользователей этого класса создавать объекты только на куче // и работать с std::unique_ptr или std::shared_ptr class MyComponent2 { ComponentData data_; std::mutex data_mutex_; }; ``` Второй вариант часто оказывается предпочтительным, поскольку обращений к мьютексу внутри `MyComponent2` обычно происходит больше, чем загрузок адреса самого объекта. Так что будем раскручивать этот вариант дальше. Ну и раз мы говорили о потокобезопасном объекте, разумно продолжить с тем, что управлять жизнью нашего объекта мы будем через `std::shared_ptr`. Стандартный прием для ограничения создания объектов где попало — сделать конструкторы приватными, а для создания предоставить отдельную функцию. ```C++ class MyComponent { public: static auto make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr { // ??? } // баним конструкторы копирования и перемещения, чтоб случайно не вытянуть // данные объекта в экземпляр на стеке MyComponent(const MyComponent&) = delete; MyComponent(MyComponent&&) = delete; // и этих друзей тоже баним, но это уже не обязательно MyComponent& operator = (const MyComponent&) = delete; MyComponent& operator = (MyComponent&&) = delete; private: MyComponent(Arg1, Arg2) { ... }; ... }; ``` Пойдем внутрь этой фабричной функции `make`. Обычно в этом месте выясняется, что опытный C++-разработчик на самом деле не очень опытный. Но это ему никак не мешает. Да и вообще редко кому мешает. Можно попробовать написать эту функцию так ```C++ auto MyComponent::make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr { return std::make_shared(std::move(arg1), std::move(arg2)); } ``` Но нас сразу же ждет [разочарование](https://godbolt.org/z/rvfPq6v1M) в полсотни строк ошибок — `std::make_shared` не может вызвать наш приватный конструктор! Не беда! И наш C++ разработчик, не сильно напрягаясь, [исправляет](https://godbolt.org/z/fq654TEaG) ошибку ```C++ auto MyComponent::make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr { return std::shared_ptr(new MyComponent(std::move(arg1), std::move(arg2))); } ``` Код компилируется, работает. Все свободны? Действительно, все работает. Но есть нюанс. Эти два варианта по-разному работают с памятью! Во многих случаях это не существенно. Но если подобным образом создается множество объектов, разница начинает быть заметной. `std::shared_ptr` считает живые слабые (`weak_ptr`) и сильные ссылки. Для этого ему нужно выделить небольшой блок памяти под пару (атомарных) `size_t` и, может быть, еще что-то. Этот блок зовется контрольным. При использовании `std::make_shared` контрольный блок выделяется рядом с создаваемым объектом. То есть выделяется один кусок памяти на как минимум `sizeof(MyComponent) + 2 * sizeof(size_t)`. Это поведение рекомендовано стандартом, но не обязательно. Тем не менее все известные имплементации следуют рекомендации. При вызове конструктора `std::shared_ptr` от сырого указателя объект уже как бы создан и контрольный блок рядом с ним не запихнуть. Поэтому будет выделено еще как минимум `2 * sizeof(size_t)` памяти. Где-то в другом месте. И тут в ход идут детали реализации аллокаторов, а также пляски с выравниваниями. И в действительности выделяется не `sizeof(MyComponent) + 2 * sizeof(size_t)`, а больше. И в случае прямого вызова конструктора от сырого указателя — значительно больше. Ну а также при расположении контрольного блока рядом с данными иногда начинает заметно играть локальность данных и выигрыш от попадания в кэш. Но это все если объект маленький. А если большой? А если большой, вы создавали его через `std::make_shared`, а потом плодили `std::weak_ptr`, у вас может начать происходить что-то очень похожее на утечку памяти. Хотя объекты исправно умирают и деструкторы вызываются. Вы же видели это в логе! Опять-таки: контрольный блок. Если у вас есть живые `weak_ptr`, привязанные к уже отмершим `std::shared_ptr`, контрольный блок продолжает жить. Ну чтоб вы могли вызвать `std::weak_ptr::expired()`, и он вам бы сказал `true`. Но если контрольный блок сидел в одном куске памяти с умершим объектом, а именно так и получается при создании через `std::make_shared`, кусок памяти из-под объекта операционной системе возвращаться не будет, пока не помрет сам контрольный блок! Вот вам и утечки. Также есть разница в том, какой именно `operator new` будет вызываться. `std::make_shared` всегда вызывает глобальный. И если вы перегрузили `new` для своего типа, поведение может быть не тем, что вы бы ожидали. ## Все, как обычно, плохо. Так что же делать если нам очень надо для своего компонента все-таки выполнить одну аллокацию и потенциально сэкономить? Есть ли решение? Конечно! В C++ всегда есть какое-нибудь страшное решение. Иногда даже без неопределенного поведения. И это даже наш случай. Есть `access token` техника, с помощью которой можно осуществить задуманное: Надо предоставить для `std::make_shared` **публичный** конструктор, но который можно вызвать, только имея экземпляр **приватного** типа (`access token`) ```C++ class MyComponent { private: // access token struct private_ctor_token { // только MyComponent cможет их создавать, явно обращаясь к конструктору по-умолчанию explicit private_ctor_token() = default; }; public: static auto make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr { return std::make_shared(private_ctor_token{}, std:: move(arg1), std::move(arg2)); } // этот конструктор приватный, хотя и в публичной секции -- его никто не сможет вызвать, // не имея доступа к приватному токену MyComponent(private_ctor_token, Arg1, Arg2) { ... }; ... }; ``` И [работает](https://godbolt.org/z/57vo1jE3c). Стоит обратить внимание, что конструктор токена должен быть помечен как `explicit`, иначе всю нашу систему безопасности с приватным типом легко обойти вот так: ```C++ int main() { MyComponent c({}, // создаем приватный токен, не называя его! // У нас нет доступа только к имени {}, {}); } ``` ## Полезные ссылки 1. https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared 2. https://habr.com/ru/post/509004/