mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-09 13:14:18 +03:00
Add shared_from_this
This commit is contained in:
@@ -70,17 +70,18 @@
|
||||
6. Стандартная библиотека
|
||||
1. [NULL-терминированные строки](standard_lib/null_terminated_string.md)
|
||||
2. [Конструирование std::shared_ptr](standard_lib/shared_ptr_constructor.md)
|
||||
3. [потоки ввода/вывода](standard_lib/iostreams.md)
|
||||
4. [std::aligned_storage](standard_lib/aligned_storage.md)
|
||||
5. [функции стантарной библиотеки как параметры](standard_lib/function_pass_and_address_restriction.md)
|
||||
6. [std::ranges::views](standard_lib/ranges_views_lazy.md)
|
||||
7. [`operator[] ` ассоциативных контейнеров](standard_lib/map_subscript.md)
|
||||
8. [std::enable_if/std::void_t](standard_lib/enable_if_void_t.md)
|
||||
9. [Конструкторы контейнеров](standard_lib/stl_constructors.md)
|
||||
10. [std::uniform_int_distribution](standard_lib/uniform_int_distribution.md)
|
||||
11. [std::ranges::transform | filter](standard_lib/transform_filter_ranges.md)
|
||||
12. [vector::reserve и vector::resize](standard_lib/vector_resize_reserve.md)
|
||||
13. [std::function](standard_lib/std_function_const.md)
|
||||
3. [shared_from_this](standard_lib/shared_from_this.md)
|
||||
4. [потоки ввода/вывода](standard_lib/iostreams.md)
|
||||
5. [std::aligned_storage](standard_lib/aligned_storage.md)
|
||||
6. [функции стантарной библиотеки как параметры](standard_lib/function_pass_and_address_restriction.md)
|
||||
7. [std::ranges::views](standard_lib/ranges_views_lazy.md)
|
||||
8. [`operator[] ` ассоциативных контейнеров](standard_lib/map_subscript.md)
|
||||
9. [std::enable_if/std::void_t](standard_lib/enable_if_void_t.md)
|
||||
10. [Конструкторы контейнеров](standard_lib/stl_constructors.md)
|
||||
11. [std::uniform_int_distribution](standard_lib/uniform_int_distribution.md)
|
||||
12. [std::ranges::transform | filter](standard_lib/transform_filter_ranges.md)
|
||||
13. [vector::reserve и vector::resize](standard_lib/vector_resize_reserve.md)
|
||||
14. [std::function](standard_lib/std_function_const.md)
|
||||
7. Исполнение программы
|
||||
1. [Бесконечные циклы](runtime/endless_loop.md)
|
||||
2. [Рекурсия](runtime/recursion.md)
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
# std::shared_from_this
|
||||
|
||||
Обсуждая особенности [`std::make_shared`](shared_ptr_constructor.md), я упоминул, что иногда крайне необходимо убедиться, что объекты вашего класса всегда создаются только в куче и управляются с помощью умного указателя. Вот сейчас будет еще один такой случай.
|
||||
|
||||
Вы разрабатываете графический интерфейс и, как это было принято лет 20 назад, решили что все доллжно быть объектно-ориентированно и красиво. Компонентики. Виджеты. Всех мы будем по требованию создавать, управлять умными shared и weak указателями, чтоб не очень сильно задумываться о владении — очень стандартный подход, между прочим. Rust библиотеки для GUI, например, часто [критикуют](https://www.warp.dev/blog/why-is-building-a-ui-in-rust-so-hard) за чудовищную сложность именно из-за владений.
|
||||
|
||||
И так у вас появились некоторые базовые типы:
|
||||
|
||||
```C++
|
||||
enum class EventType {
|
||||
Clicked,
|
||||
Created,
|
||||
// и другие
|
||||
};
|
||||
|
||||
class Widget {
|
||||
public:
|
||||
virtual ~Widget() = default;
|
||||
};
|
||||
|
||||
class EventListener {
|
||||
public:
|
||||
// Уведомить Listener, что такой-то Widget породил некоторое событие
|
||||
void notify(EventType, std::weak_ptr<Widget> event_source);
|
||||
};
|
||||
```
|
||||
|
||||
Хорошо. Дальше давайте заведем кнопку! Куда же без кнопки в хорошем UI?!
|
||||
|
||||
```C++
|
||||
class Button: public Widget {
|
||||
public:
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener}
|
||||
{
|
||||
// (1) хотелось бы уведомить listener, что кнопка создана!
|
||||
listener_->notify(EventType::Created, this); // Это неправильно...
|
||||
}
|
||||
|
||||
void click() {
|
||||
// (2) хотелось бы уведомить listerner, что на кнопочку нажали!
|
||||
listener_->notify(EventType::Clicked, this); // И это разумеется неправильно
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
};
|
||||
```
|
||||
|
||||
К нашему большому счастью, этот [код не компилируется](https://godbolt.org/z/v77aG9rhz)
|
||||
|
||||
```
|
||||
<source>:26:47: error: cannot convert 'Button*' to 'std::weak_ptr<Widget>'
|
||||
26 | listener_->notify(EventType::Created, this); // Это неправильно...
|
||||
| ^~~~
|
||||
| |
|
||||
| Button*
|
||||
```
|
||||
|
||||
Ах, ну да, типы разные... Опытного разработчика на C++ это, конечно, заставило бы задуматься. А вот неопытного... Он может просто выполнить «преобразование» типов!
|
||||
|
||||
```C++
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener}
|
||||
{
|
||||
// (1) хотелось бы уведомить listener, что кнопка создана!
|
||||
listener_->notify(EventType::Created, std::shared_ptr<Button>(this)); // Это ОЧЕНЬ неправильно...
|
||||
}
|
||||
|
||||
void click() {
|
||||
// (2) хотелось бы уведомить listerner, что на кнопочку нажали!
|
||||
listener_->notify(EventType::Clicked, std::shared_ptr<Button>(this)); // И это разумеется ТОЖЕ неправильно
|
||||
}
|
||||
```
|
||||
И взорвется
|
||||
```C++
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = std::make_shared<Button>(listener);
|
||||
}
|
||||
```
|
||||
```
|
||||
Program returned: 139
|
||||
free(): invalid pointer
|
||||
Program terminated with signal: SIGSEGV
|
||||
```
|
||||
|
||||
`std::shared_ptr<Button>(this)` создает новый shared_ptr, который ничего знаеть не знает о существовании другого умного указателя, управляющего объектом. Что разумеется приводит к попытке повторного освобождения памяти: сначала одним указателем, затем другим. Что иногда даже может работать успешно... Неопределенное поведение все-таки!
|
||||
|
||||
Хорошо, давайте чинить. Забудем пока про конструктор. Попробуем хотя бы починить метод `click`.
|
||||
|
||||
Для этого необходимо сообщить кнопке о том, что она управляется умным указателем. Например, вручную добавить в нее `weak_ptr<Button>` поле и заполнить его после конструирования.
|
||||
Да, это должна быть слабая ссылка, чтобы не создавать цикл из shared_ptr и сопутствующую ему утечку памяти
|
||||
|
||||
|
||||
|
||||
```C++
|
||||
class Button: public Widget {
|
||||
public:
|
||||
// Мы не можем передать weak_ptr<Button> в конструктор! Ведь мы же еще кнопку не создали!
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener} {}
|
||||
|
||||
// Придется сделать что-то такое мерзкое
|
||||
void set_self(std::weak_ptr<Button> self) {
|
||||
self_ = self;
|
||||
}
|
||||
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, self_);
|
||||
}
|
||||
|
||||
void
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
std::weak_ptr<Button> self_;
|
||||
};
|
||||
```
|
||||
|
||||
И пользоваться бы этим добром пришлось так
|
||||
```C++
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = std::make_shared<Button>(listener);
|
||||
button->set_self(button);
|
||||
button->click();
|
||||
}
|
||||
```
|
||||
|
||||
Некрасиво, но работает...
|
||||
|
||||
|
||||
Но С++11 предлагает нам решенине получше! `std::enable_shared_from_this`
|
||||
Который позволит нам автоматически добавить такое же self-поле и позаботится о его правильное заполнении
|
||||
при конструировании `shared_ptr`.
|
||||
|
||||
```C++
|
||||
class Button: public Widget, public std::enable_shared_from_this<Button> {
|
||||
public:
|
||||
// Мы не можем передать weak_ptr<Button> в конструктор! Ведь мы же еще кнопку не создали!
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener} {}
|
||||
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, this->weak_from_this());
|
||||
// есть также shared_from_this
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
};
|
||||
|
||||
...
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = std::make_shared<Button>(listener);
|
||||
button->click();
|
||||
}
|
||||
```
|
||||
[Оно не падает](https://godbolt.org/z/EvxsaKcvz). Красота!
|
||||
|
||||
Но что-то все еще не то. Полная красота не достигнута... Ну разумеется!
|
||||
Ведь в вашем приложении будут не только кнопки. Но и комбо-боксы, текстовые поля и прочее...
|
||||
И что к каждому классу поотдельности `public std::enable_shared_from_this<ClassName>` пририсовывать?
|
||||
|
||||
Нет. Разумнее прицепить его к базовому классу и забыть. Давайте так сделаем.
|
||||
|
||||
```C++
|
||||
class Widget: public std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
virtual ~Widget() = default;
|
||||
};
|
||||
|
||||
// Для отладки, добавим печать в notify
|
||||
class EventListener {
|
||||
public:
|
||||
void notify(EventType, std::weak_ptr<Widget> event_source) {
|
||||
std::cout << "event received\n";
|
||||
if (auto w = event_source.lock()) {
|
||||
std::cout << "widget valid\n";
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Все [работает](https://godbolt.org/z/8fGr5vcPr) как надо
|
||||
```
|
||||
Program returned: 0
|
||||
event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Прекрасно. Кстати, нам же не очень-то бы хотелось выпячивать этот метод `shared_from_this()` ведь он же для внутренних нужд... Может, сделать `protected` наследование?
|
||||
|
||||
```C++
|
||||
class Widget: protected std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
virtual ~Widget() = default;
|
||||
};
|
||||
```
|
||||
И... уже [неправильно](
|
||||
https://godbolt.org/z/876T4Y8rn), но продолжает компилироваться!
|
||||
```
|
||||
Program returned: 0
|
||||
event received
|
||||
```
|
||||
Получили nullptr и рады... Да, ни в коем случае нельзя его наследовать ни приватно, ни защищенно. Так и в документации написано. Настройте себе правило для линтера, пожалуйста.
|
||||
|
||||
Кстати, если у вас будет несколько абстрактных интерфейсов, каждый с `enable_shared_from_this` и вы попытаетесь унаследовать сразу хоть пару из них... Вы получите...
|
||||
|
||||
```C++
|
||||
|
||||
class Widget: public std::enable_shared_from_this<Widget> {
|
||||
public:
|
||||
virtual ~Widget() = default;
|
||||
};
|
||||
|
||||
class Gadget: public std::enable_shared_from_this<Gadget> {
|
||||
public:
|
||||
virtual ~Gadget() = default;
|
||||
};
|
||||
...
|
||||
class Button: public Widget, public Gadget {
|
||||
...
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, this->weak_from_this());
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Правильно! Добро пожаловать в ад вариаций ромбовидного наследования!
|
||||
```
|
||||
<source>:38:53: error: member 'weak_from_this' found in multiple base classes of different types
|
||||
38 | listener_->notify(EventType::Clicked, this->weak_from_this());
|
||||
|
||||
```
|
||||
|
||||
Просто не делайте так. И всё будет хорошо.
|
||||
|
||||
Ладно. Договоримся, что нас устраивает результат. Мы починили метод `click`. А как насчет конструктора?
|
||||
|
||||
```C++
|
||||
class Button: public Widget {
|
||||
public:
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener} {
|
||||
listener_->notify(EventType::Created, this->weak_from_this());
|
||||
}
|
||||
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, this->weak_from_this());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = std::make_shared<Button>(listener);
|
||||
button->click();
|
||||
}
|
||||
```
|
||||
|
||||
Работет, [но не так как хочется](https://godbolt.org/z/MY93EjPEa).
|
||||
```
|
||||
Program returned: 0
|
||||
event received
|
||||
event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Из конструктора мы отправили в Listener нулевой указатель... Ну а чего еще мы хотели?! Ведь работа конструктора еще не завершена. А как мы видели из ручной имитации shared_from_this, внутренний weak_ptr инициализируеься только после полного создания объекта. Так что все работает правильно. Хоть и неожиданно.
|
||||
|
||||
Так что если хотите посылать сообщения из конструктора таким образом, то желательно перехотеть. И сделать статический фабричный метод вместо конструктора. Из него уже можно будет посылать уведомления сколько угодно.
|
||||
|
||||
```C++
|
||||
class Button: public Widget {
|
||||
public:
|
||||
|
||||
static std::shared_ptr<Button> create(std::shared_ptr<EventListener> listener) {
|
||||
auto button = std::make_shared<Button>(listener);
|
||||
listener->notify(EventType::Created, button);
|
||||
return button;
|
||||
}
|
||||
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener} {}
|
||||
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, this->weak_from_this());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = Button::create(listener);
|
||||
button->click();
|
||||
}
|
||||
```
|
||||
Вот теперь [все правильно](https://godbolt.org/z/9795d79vY)
|
||||
|
||||
```
|
||||
Program returned: 0
|
||||
event received
|
||||
widget valid
|
||||
event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Осталось пойти и сделать конструтор приватным с помощью привантного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = Button(listener);
|
||||
button.click();
|
||||
}
|
||||
```
|
||||
```
|
||||
Program returned: 0
|
||||
event received
|
||||
```
|
||||
|
||||
Либо, если вы бесстрашно использовали `shared_from_this()` вместо `weak_from_this()` (который появился только в C++17, если что), то вас ждет что-то более интересное
|
||||
|
||||
```C++
|
||||
class Button: public Widget {
|
||||
public:
|
||||
Button(std::shared_ptr<EventListener> listener): listener_ {listener} {}
|
||||
|
||||
void click() {
|
||||
listener_->notify(EventType::Clicked, this->shared_from_this());
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
};
|
||||
|
||||
int main() {
|
||||
auto listener = std::make_shared<EventListener>();
|
||||
auto button = Button(listener);
|
||||
button.click();
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
// https://godbolt.org/z/szYd841a1
|
||||
Program returned: 139
|
||||
terminate called after throwing an instance of 'std::bad_weak_ptr'
|
||||
what(): bad_weak_ptr
|
||||
Program terminated with signal: SIGSEGV
|
||||
```
|
||||
|
||||
Да, сейчас можно получить исключение. А вот до выхода C++17 никакого исключения не гарантировалось. Только неопределенное поведение. Этот дефект исправили и портировали во все версии стандарта, начиная c С++11. Главное, не используйте очень старые версии компиляторов и стандартной библиотеки в их составе.
|
||||
|
||||
## Полезные ссылки
|
||||
|
||||
1. https://en.cppreference.com/w/cpp/memory/enable_shared_from_this
|
||||
2. https://cplusplus.github.io/LWG/issue2529
|
||||
@@ -116,10 +116,8 @@ class MyComponent {
|
||||
private:
|
||||
// access token
|
||||
struct private_ctor_token {
|
||||
// только MyComponent может их создавать
|
||||
friend class MyComponent;
|
||||
private:
|
||||
private_ctor_token() = default;
|
||||
// только MyComponent cможет их создавать, явно обращаясь к конструктору по-умолчанию
|
||||
explicit private_ctor_token() = default;
|
||||
};
|
||||
public:
|
||||
static auto make(Arg1 arg1, Arg2 arg2) -> std::shared_ptr<MyComponent> {
|
||||
@@ -136,9 +134,9 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
И [работает](https://godbolt.org/z/Yqsq7Mr7c).
|
||||
И [работает](https://godbolt.org/z/57vo1jE3c).
|
||||
|
||||
Стоит обратить внимание, что конструктор токена должен быть явно приватным, иначе всю нашу систему безопасности с приватным типом легко обойти вот так:
|
||||
Стоит обратить внимание, что конструктор токена должен быть помечен как `explicit`, иначе всю нашу систему безопасности с приватным типом легко обойти вот так:
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
|
||||
Reference in New Issue
Block a user