Add shared_from_this

This commit is contained in:
Nekrolm
2024-10-11 01:25:06 +01:00
parent a3a05c772a
commit f6f62e78e9
3 changed files with 373 additions and 17 deletions
+12 -11
View File
@@ -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)
+357
View File
@@ -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
+4 -6
View File
@@ -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() {