mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 13:14:41 +03:00
Много typos и небольших правок (#124)
* Update comparison_operator_rewrite.md: typos * Update default_default_constructor.md: typos Не уверен насчёт добавления `2D` к Point. По смыслу, вроде бы, нужно * Update shared_from_this.md * Update function_pass_and_address_restriction.md * Update enable_if_void_t.md * Update static_initialization_order_fiasco.md * Update uninitialized.md * Update ownership_and_exceptions.md * Update vptr.md
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
class Actor {
|
||||
public:
|
||||
virtual ~Actor() = default;
|
||||
// мы опустим детали и обхекты
|
||||
// мы опустим детали и объекты
|
||||
// для передачи сообщений между акторами
|
||||
// важно лишь что был метод run
|
||||
virtual void run() = 0;
|
||||
|
||||
@@ -49,7 +49,7 @@ if (auto item_handle = lru_get(cache, key)) {
|
||||
}
|
||||
```
|
||||
|
||||
Код восхтитителен тем, что явно выполняет целых два обращения к кэшу: на вставку и на проверку. Но ведь можно было бы ограничиться только одной вставкой, если `lru_insert` может предоставить необходимую информацию об успехе... Может ли дело быть в этом? Нет ли в этом сервисе случайно гонок, которые могут вклиниться между вставкой и проверкой? Но меня уверили, что процесс однопоточный.
|
||||
Код восхитителен тем, что явно выполняет целых два обращения к кэшу: на вставку и на проверку. Но ведь можно было бы ограничиться только одной вставкой, если `lru_insert` может предоставить необходимую информацию об успехе... Может ли дело быть в этом? Нет ли в этом сервисе случайно гонок, которые могут вклиниться между вставкой и проверкой? Но меня уверили, что процесс однопоточный.
|
||||
|
||||
Наверное, стоит углубиться в функцию `lru_insert`. Ее написали 10 лет назад и больше не трогали. Ее протестировали. Она надежна. Как я могу в ней сомневаться?
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(в разные .c/.cpp файлы). Они могут компилироваться параллельно.
|
||||
Скорость сборки повышается. И все было бы хорошо.
|
||||
|
||||
Но только в одном «модуле» появляется глобальная переменная, используемая в другом модуле, начинаются проблемы. И проблемы не только от того, что глобальные переменные в принципе признак не самого удачного дизайна. Проблема в том, что связи между модулями нет (заголовочные файлы ничего не связывают). И после объединения модулей код с инициализацией глобальной переменной может оказаться ПОСЛЕ кода с использованием.
|
||||
Но как только в одном «модуле» появляется глобальная переменная, используемая в другом модуле, начинаются проблемы. И проблемы не только от того, что глобальные переменные в принципе признак не самого удачного дизайна. Проблема в том, что связи между модулями нет (заголовочные файлы ничего не связывают). И после объединения модулей код с инициализацией глобальной переменной может оказаться ПОСЛЕ кода с использованием.
|
||||
|
||||
Стандарты C и С++ гарантируют, что глобальные переменные будут сконструированы в порядке их объявления внутри единицы трансляции. А между единицами трансляции — неопределен. И вместе с порядком неопределено и поведение программы.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Новые современные языки программирования обычно запрещают использование неинициализированных переменных. Переменные либо всегда инициализируются значением по умолчанию (например, в [Go](https://golang.org/ref/spec#The_zero_value)). Либо попытка чтения из неинициализированной переменной дает ошибку компиляции (в [Kotlin](https://pl.kotl.in/PoVXtB7AB) или в [Rust](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=261f92c8ca39b10c1ac565e4f8a1e28a)).
|
||||
|
||||
C и C++ — старые языки. В них можно легко и просто объявить переменную, а инициализировать ее как-нибудь потом. Или забыть иницаилизировать вовсе. Но в отличие от совсем низкоуровневого ассемблера, в котором читать из неинициализированной переменной никто не запрещает — ну получите вы свои мусорные байтики и ладно — в C/C++ (а также в Rust, см [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html)) это влечет за собой неопределенное поведение.
|
||||
C и C++ — старые языки. В них можно легко и просто объявить переменную, а инициализировать ее как-нибудь потом. Или забыть инициализировать вовсе. Но в отличие от совсем низкоуровневого ассемблера, в котором читать из неинициализированной переменной никто не запрещает — ну получите вы свои мусорные байтики и ладно — в C/C++ (а также в Rust, см [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html)) это влечет за собой неопределенное поведение.
|
||||
|
||||
Но время не стоит на месте даже для C++ и в последних версиях стандарта (C++26 и новее) всё же произошли некоторые изменения: стандарт вводит новое понятие *ошибочного* *(erroneous)* поведения. И ошибка чтения неинициализированной переменной считается теперь ошибочным, а не неопределенным поведением. На практике же это значит, что вы все также успешно отстрелите себе ногу, но компиляторам рекомендуется выдать диагностику и запрещается делать оптимизации — какой-то определенный мусор должен быть успешно прочитан, а оптимизации кода до и после такого чтения не должны делать никаких предположений об этом мусорном значении.
|
||||
|
||||
@@ -120,7 +120,7 @@ int answer = 0;
|
||||
Специальные функции, например, `std::make_unique_for_overwrite` мы не рассматриваем. Функции выделения сырой памяти: `*alloc` тоже. Хотя напомнить, что писать `(T*)malloc(N)` в ожидании инициализированной памяти нельзя.
|
||||
|
||||
|
||||
В боле общем случае, если верно, что `is_trivially_constructible<T> == true`, то
|
||||
В более общем случае, если верно, что `is_trivially_constructible<T> == true`, то
|
||||
|
||||
1. `T x;`
|
||||
2. `T x[N];`
|
||||
|
||||
@@ -81,7 +81,7 @@ fun(T) { // 2
|
||||
fun(X{}); // несмотря на то что значение std::is_same_v<T, T> всегда истинно, X::Outer не существует. И SFINAE сработает не из-за значения предиката, а из-за его аргументов.
|
||||
```
|
||||
|
||||
И тут начинатся первая неприятность:
|
||||
И тут начинается первая неприятность:
|
||||
`std::enable_if` против `std::enable_if_t`.
|
||||
|
||||
```C++
|
||||
|
||||
@@ -92,7 +92,7 @@ int main() {
|
||||
|
||||
Да, почти все функции стандартной библиотеки C++17, после инстанциирования шаблонов, все-таки оказываются нормальными функциями и потому у нас уж сколько лет все работает.
|
||||
|
||||
C функциями стандартной библиотеки C все, конечно, хуже -- они могут быть макросами, и черт его знает от чего вы на самом деле взяли адресс в таком случае.
|
||||
C функциями стандартной библиотеки C все, конечно, хуже -- они могут быть макросами, и черт его знает от чего вы на самом деле взяли адрес в таком случае.
|
||||
|
||||
С C++20 (вдохновленные ranges [Эрика Ниблера](https://github.com/ericniebler)) новые (а также потенциально старые после перехода std на модули) функции внезапно могут оказаться *ниблоидами*. Глобальными объектами с определенным `operator()` -- так что они могут выглядеть и крякать как старые добрые функции, но таковыми не быть.
|
||||
И если вы использовали `С-style` каст вместо громоздкого `static_cast`, то вас могут ждать интересные результаты:
|
||||
@@ -161,7 +161,7 @@ int main() {
|
||||
|
||||
Плохая новость: замечательно не будет, поэтому придется писать код
|
||||
|
||||
Проблема решится оборачиваением вызова к std функции в вашу функцию или в лямбду.
|
||||
Проблема решится оборачиванием вызова к std функции в вашу функцию или в лямбду.
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
@@ -253,7 +253,7 @@ int main() {
|
||||
integrate(LAMBDA_WRAP(std::sqrt));
|
||||
}
|
||||
```
|
||||
Что поледать, раздутие кода -- известный результат [мономорфизации](https://en.wikipedia.org/wiki/Monomorphization) шаблонов/generic функций.
|
||||
Что поделать, раздутие кода -- известный результат [мономорфизации](https://en.wikipedia.org/wiki/Monomorphization) шаблонов/generic функций.
|
||||
|
||||
Переиспользуйте лямбду, и будет лучше:
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# std::shared_from_this
|
||||
|
||||
Обсуждая особенности [`std::make_shared`](shared_ptr_constructor.md), я упоминул, что иногда крайне необходимо убедиться, что объекты вашего класса всегда создаются только в куче и управляются с помощью умного указателя. Вот сейчас будет еще один такой случай.
|
||||
Обсуждая особенности [`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) за чудовищную сложность именно из-за владений.
|
||||
Вы разрабатываете графический интерфейс и, как это было принято лет 20 назад, решили что все должно быть объектно-ориентированно и красиво. Компонентики. Виджеты. Всех мы будем по требованию создавать, управлять умными shared и weak указателями, чтоб не очень сильно задумываться о владении — очень стандартный подход, между прочим. Rust библиотеки для GUI, например, часто [критикуют](https://www.warp.dev/blog/why-is-building-a-ui-in-rust-so-hard) за чудовищную сложность именно из-за владений.
|
||||
|
||||
И так у вас появились некоторые базовые типы:
|
||||
|
||||
@@ -107,7 +107,6 @@ public:
|
||||
listener_->notify(EventType::Clicked, self_);
|
||||
}
|
||||
|
||||
void
|
||||
private:
|
||||
std::shared_ptr<EventListener> listener_;
|
||||
std::weak_ptr<Button> self_;
|
||||
@@ -127,7 +126,7 @@ int main() {
|
||||
Некрасиво, но работает...
|
||||
|
||||
|
||||
Но С++11 предлагает нам решенине получше! `std::enable_shared_from_this`
|
||||
Но С++11 предлагает нам решение получше! `std::enable_shared_from_this`
|
||||
Который позволит нам автоматически добавить такое же self-поле и позаботится о его правильное заполнении
|
||||
при конструировании `shared_ptr`.
|
||||
|
||||
@@ -281,7 +280,7 @@ event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Из конструктора мы отправили в Listener нулевой указатель... Ну а чего еще мы хотели?! Ведь работа конструктора еще не завершена. А как мы видели из ручной имитации shared_from_this, внутренний weak_ptr инициализируеься только после полного создания объекта. Так что все работает правильно. Хоть и неожиданно.
|
||||
Из конструктора мы отправили в Listener нулевой указатель... Ну а чего еще мы хотели?! Ведь работа конструктора еще не завершена. А как мы видели из ручной имитации shared_from_this, внутренний weak_ptr инициализируется только после полного создания объекта. Так что все работает правильно. Хоть и неожиданно.
|
||||
|
||||
Так что если хотите посылать сообщения из конструктора таким образом, то желательно перехотеть. И сделать статический фабричный метод вместо конструктора. Из него уже можно будет посылать уведомления сколько угодно.
|
||||
|
||||
@@ -321,7 +320,7 @@ event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Осталось пойти и сделать конструтор приватным с помощью привантного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
|
||||
Осталось пойти и сделать конструтор приватным с помощью приватного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
|
||||
@@ -32,9 +32,9 @@ c автоматической реализацией (через `=default`).
|
||||
Добавим заголовок и все будет [работать](https://godbolt.org/z/5E4csxK6b). Здорово? Конечно же!
|
||||
Но есть кое-что еще.
|
||||
|
||||
Не все типы можно или осмысленно упорядочивать. Иногда достаточно только равенства.
|
||||
Не все типы можно осмысленно упорядочивать. Иногда достаточно только равенства.
|
||||
В C++20 можно определить `operator ==` и `operator !=` будет выведен автоматически.
|
||||
Более того, реализация через `= defalt` также [работает](https://godbolt.org/z/Mc9YEsGWK).
|
||||
Более того, реализация через `= default` также [работает](https://godbolt.org/z/Mc9YEsGWK).
|
||||
|
||||
```C++
|
||||
struct Pair {
|
||||
@@ -70,7 +70,7 @@ struct String {
|
||||
bool operator==(const StringView &sv) const {
|
||||
// а тут меняем порядок местами, ведь у StringView уже есть operator ==
|
||||
// overload resolution наедет его по первому аргументу
|
||||
// а ко второму (*this: String) можно применить неявное приведенине типа. Все отлично!
|
||||
// а ко второму (*this: String) можно применить неявное приведение типа. Все отлично!
|
||||
return sv == *this;
|
||||
}
|
||||
};
|
||||
@@ -102,7 +102,7 @@ Program terminated with signal: SIGSEGV
|
||||
С++20 позаботился о разработчиках. И теперь им не нужно выдумывать странные перестановки аргументов местами чтоб писать поменьше перегрузок операторов сравнения.
|
||||
C++20 ввел правила переписывания [всех операторов сравнения](https://en.cppreference.com/w/cpp/language/overload_resolution#Call_to_an_overloaded_operator), так что компиляторы выполнят перестановку за вас. Даже если в ней нет надобности. И разумеется веселые и находчивые разработчики старых кодовых баз получат бесконечную рекурсию. А c ней и неопределенное поведение. И SIGSEGV от переполнения стека, если повезет.
|
||||
|
||||
Эти изменения в правила поиска перегрузок для операций сравнения имели и другие побочные эффекты. Их постарались исправить в предложении [P2468R2 The Equality Operator You Are Looking For](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2468r2.html#code-patterns-which-fail-at-runtime). Он принято и реализовано в C++23.
|
||||
Эти изменения в правила поиска перегрузок для операций сравнения имели и другие побочные эффекты. Их постарались исправить в предложении [P2468R2 The Equality Operator You Are Looking For](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2468r2.html#code-patterns-which-fail-at-runtime). Оно принято и реализовано в C++23.
|
||||
|
||||
Но внезапную рекурсию все равно не убрали! Полагайтесь на предупреждения компилятора.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Конструктор по умолчанию и = default
|
||||
|
||||
Гайдлайны по современному C++ всячески намекают, а иногда напрямую советуют: [следуюйте "правилу нуля" (rule of zero)](https://en.cppreference.com/w/cpp/language/rule_of_three) для ваших классов, и структур и будет вам счастье! Используйте инициализацторы по умолчанию! C++20 улучшил поддержку структур-аггрегатов, так что не надо писать вручную конструкторы там где это не надо... Но legacy код существут, его затратно переписывать... А также существуют legacy разработчики, которые застряли в C++98...
|
||||
Гайдлайны по современному C++ всячески намекают, а иногда напрямую советуют: [следуйте "правилу нуля" (rule of zero)](https://en.cppreference.com/w/cpp/language/rule_of_three) для ваших классов, и структур и будет вам счастье! Используйте инициализаторы по умолчанию! C++20 улучшил поддержку структур-аггрегатов, так что не надо писать вручную конструкторы там где это не надо... Но legacy код существует, его затратно переписывать... А также существуют legacy разработчики, которые застряли в C++98...
|
||||
|
||||
Так что в старых кодовых базах можно встретить что-нибудь такое:
|
||||
|
||||
@@ -12,7 +12,7 @@ public:
|
||||
Point2D(int _x, int _y);
|
||||
// Раз добавили какой-то конструктор,
|
||||
// нужно добавить и конструктор по умолчанию
|
||||
Point();
|
||||
Point2D();
|
||||
|
||||
int x;
|
||||
int y;
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
// Point.cpp
|
||||
Point2D::Point2D(int _x, int _y) : x {_x}, y {_y} {}
|
||||
// И даже такие!
|
||||
Point::Point2D() = default;
|
||||
Point2D::Point2D() = default;
|
||||
```
|
||||
|
||||
Делать так в современном C++ крайне не рекомендуется. Не только из-за обилия бессмысленного бойлерплейта, но и из-за риска получить неинициализированные поля и неопределенное поведение вместе с ними.
|
||||
|
||||
Reference in New Issue
Block a user