mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 13:14:41 +03:00
152 lines
12 KiB
Markdown
152 lines
12 KiB
Markdown
# Конструкторы `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<std::mutex> data_mutex_;
|
||
};
|
||
|
||
// как-то заставить пользователей этого класса создавать объекты только на куче
|
||
// и работать с std::unique_ptr<MyComponent2> или std::shared_ptr<MyComponent2>
|
||
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> {
|
||
// ???
|
||
}
|
||
|
||
// баним конструкторы копирования и перемещения, чтоб случайно не вытянуть
|
||
// данные объекта в экземпляр на стеке
|
||
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<MyComponent> {
|
||
return std::make_shared<MyComponent>(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<MyComponent> {
|
||
return std::shared_ptr<MyComponent>(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<MyComponent> {
|
||
return std::make_shared<MyComponent>(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/
|