mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-16 20:37:03 +03:00
Fix a number of typos (#128)
Co-authored-by: Sergey Fukanchik <s.fukanchik@postgrespro.ru>
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
|
||||
Если вы собираетесь писать на C++ код, в работоспособности которого хотите быть хоть немного уверенными, стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка, его библиотеке, и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
|
||||
|
||||
В этой книге я собрал множество самых разных примеров как в коде на C и C++ можно наткнуться на неопределенное, неожиданное и совершенно ошибочное поведение. И хотя основной фокус книги всё же на неопределенном поведении, в некоторых разделах описываются вещи вполе специфицированные, но довольно неочевидные.
|
||||
В этой книге я собрал множество самых разных примеров как в коде на C и C++ можно наткнуться на неопределенное, неожиданное и совершенно ошибочное поведение. И хотя основной фокус книги всё же на неопределенном поведении, в некоторых разделах описываются вещи вполне специфицированные, но довольно неочевидные.
|
||||
|
||||
**Важно:** этот сборник **не является учебным пособием** по языку и рассчитан на тех, кто уже знаком с программированием, с C++, и понимает основные его конструкции.
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
И почти всегда это нежелательная, ошибочная ситуация, последствия которой варьируются от просто неправильного результата до чудовищных уязвимостей в безопасности.
|
||||
|
||||
Иногда, конечно, система может быть толерантна к таким ошибкам и серьезных проблем не возникнет: например, если страничка магазина запрашивает список товаров на складе, а в этот момент база данных склада обновляется, то вы, как пользователь, возможно, увидите неполный или устаревший список товаров или смесь из старых и новых данных. Но критического ничего не произойдет, если разработчики предусмотрели дополнительные проверки с запросом к базе данных в момент взаимодейтсвия с конкретным товаром из списка... Чтобы вы не купили то, чего больше не существует.
|
||||
Иногда, конечно, система может быть толерантна к таким ошибкам и серьезных проблем не возникнет: например, если страничка магазина запрашивает список товаров на складе, а в этот момент база данных склада обновляется, то вы, как пользователь, возможно, увидите неполный или устаревший список товаров или смесь из старых и новых данных. Но критического ничего не произойдет, если разработчики предусмотрели дополнительные проверки с запросом к базе данных в момент взаимодействия с конкретным товаром из списка... Чтобы вы не купили то, чего больше не существует.
|
||||
|
||||
Файловая система — один из таких ресурсов, гонки за которым естественны и должны учитываться при разработке приложений. Да, самые низкоуровневые проблемы синхронизации чтения и записи в файловую систему берет на себя операционная система и (или) конкретный драйвер. Можно «спокойно» одновременно из разных процессов читать и писать в один и тот же файл и получать мусор или штатные ошибки: низкоуровневые операции `read` и `write` будут как-то упорядочены планировщиком ввода-вывода. С самой файловой системой при этом всё будет в порядке.
|
||||
|
||||
@@ -33,7 +33,7 @@ int main() {
|
||||
|
||||
Это класcическая **TOCTOU** (*Time-of-Check-Time-of-Use*) ошибка. Между проверкой и открытием файла, файл может быть удален.
|
||||
|
||||
Но причем тут С++, если такая проблема существет для любого языка программирования?
|
||||
Но причем тут С++, если такая проблема существует для любого языка программирования?
|
||||
|
||||
Действительно. Например, во всех версиях стандартной библиотеки Rust с 1.0 до 1.58.1 [была](https://blog.rust-lang.org/2022/01/20/cve-2022-21658.html) похожая ошибка в реализации функции `remove_dir_all`.
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
Так почему бы не взять какую-нибудь готовую серьезную библиотеку (`boost`, `abseil`) — там наверняка умные люди уже пострадали многие часы, чтобы предоставить удобные и безопасные инструменты — и забот не знать?!
|
||||
|
||||
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контроллировать самостоятельно, пристально изучая каждую строчку кода.
|
||||
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контролировать самостоятельно, пристально изучая каждую строчку кода.
|
||||
Мы все равно втыкаемся в проблемы синхронизации доступа, с аккуратным развешиванием мьютексов и атомарных переменных.
|
||||
|
||||
Ситуация (_data race_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значение. Независимо от языка программирования.
|
||||
Ситуация (_data race_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее перемешанное значение. Независимо от языка программирования.
|
||||
|
||||
Но в C++ это не просто ошибка. Это неопределенное поведение. И «возможности» для оптимизации
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ elapsed: 100
|
||||
- Переданная функция каким-то образом начнет исполнятся асинхронно (в фоновом потоке или в пуле потоком — это тоже не специфировано). И такое поведение по умолчанию во всех современных версиях GCC, Clang и MSVC.
|
||||
- Переданная функция не будет выполнятся до тех пор, пока вы не вызовете `wait` или `get` у возвращенной `std::future`. И такое поведение долгое время было со старыми версиями компиляторов. Например, [GCC 5.4](https://gcc.godbolt.org/z/nY6Kv4Gdz)
|
||||
|
||||
Какое именно поведение вы хотите можно и нужно контроллировать с помощью вызова перегрузки `std::async` с дополнительным первым параметром типа `std::launch`:
|
||||
Какое именно поведение вы хотите можно и нужно контролировать с помощью вызова перегрузки `std::async` с дополнительным первым параметром типа `std::launch`:
|
||||
- `std::launch::async` — если вы действительно хотите асинхронное исполнение
|
||||
- `std::launch::deferred` — если нужно отложить до точки вызова `wait` или `get`
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ awaitable<void> handle_request(const Request& r)
|
||||
Корутины — очень сложные объекты, которые обманчиво просты в использовании из-за синтаксического сахара. В этом же весь смысл! Поддержка `async/await` на уровне языка и компиляторов делает простым то, что всегда было делать сложно вручную... Так в происходит в высокоуровневых и безопасных языках с автоматическим управлением памятью: Python, C#, JavaScript, Kotlin.
|
||||
Но не в C++. И не в Rust. (И не в Zig).
|
||||
|
||||
В примере выше есть как минимуи **три** точки отказа, содержащих ошибки. Можете подумать об этом, пока мы будем разворачивать проблемы корутин С++.
|
||||
В примере выше есть как минимум **три** точки отказа, содержащих ошибки. Можете подумать об этом, пока мы будем разворачивать проблемы корутин С++.
|
||||
|
||||
### Что такое корутина?
|
||||
|
||||
@@ -635,7 +635,7 @@ batch_processor 15ms
|
||||
```
|
||||
|
||||
------
|
||||
Отслеживать время жизни ссылок в асинхронном коде вручную крайне тяжело. Автоматика, как в Rust, делает это намного лучше, но при этом может выдавать совершенно непонятные репорты, в которых можно разобраться только если знаешь, что именно могло пойти не так — за это async в Rust и не любят и критикуют. А в качестве самого простого и продуктивного решения, чтоб убложить borrow checker, выбирается копирование всего подряд (`.clone()`, везде `.clone()`)
|
||||
Отслеживать время жизни ссылок в асинхронном коде вручную крайне тяжело. Автоматика, как в Rust, делает это намного лучше, но при этом может выдавать совершенно непонятные репорты, в которых можно разобраться только если знаешь, что именно могло пойти не так — за это async в Rust и не любят и критикуют. А в качестве самого простого и продуктивного решения, чтоб ублажить borrow checker, выбирается копирование всего подряд (`.clone()`, везде `.clone()`)
|
||||
|
||||
С++ отдает полный контроль вам с невероятной кучей неявных захватов ссылок! Делайте с ними что хотите и как хотите. Скомпилируется без проблем и проверок. Вы можете приложить ментальные усилия, отследить все ссылки и убедиться что объекты не умрут не вовремя. Либо можно отчаяться, прочитать гайдлайны и передавать все и всегда by value. Копируя и перемещая. Никаких ссылок.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ auto p = std::make_unique<Pair>(1, 5);
|
||||
|
||||
```C++
|
||||
// https://godbolt.org/z/fTM4n7nqz
|
||||
auto p = std::make_unique<Pair>(1, 5); // теперь компилирутся в C++20
|
||||
auto p = std::make_unique<Pair>(1, 5); // теперь компилируется в C++20
|
||||
```
|
||||
|
||||
Ну добавили и добавили... Стало же удобнее? Можно теперь всегда круглые скобки использовать?...
|
||||
@@ -90,7 +90,7 @@ struct Widget {
|
||||
};
|
||||
|
||||
// Эта строчка после вашей оптимизации продолжает молча компилироваться
|
||||
// но теперь влечет неопределенное поведенине
|
||||
// но теперь влечет неопределенное поведение
|
||||
// пример: https://gcc.godbolt.org/z/q73erhYWs
|
||||
auto parent_widget = std::make_unqiue<Widget>(read_config());
|
||||
// И статические анализаторы пока молчат https://gcc.godbolt.org/z/aMsT3afxb
|
||||
@@ -119,7 +119,7 @@ int main() {
|
||||
|
||||
## Значения по умолчанию для ссылочных полей
|
||||
|
||||
Закончить, пожалуй, нужно еще одним недоразуменинем с ссылочными полями. Им же можно задать значения по умолчанию...
|
||||
Закончить, пожалуй, нужно еще одним недоразумением с ссылочными полями. Им же можно задать значения по умолчанию...
|
||||
|
||||
```C++
|
||||
struct Config {
|
||||
|
||||
@@ -139,7 +139,7 @@ test_default_getter(m, 42, []{ return "default"sv; });
|
||||
```
|
||||
`const string& : string_view` — первый неявно приводим ко второму. Взятие string_view от string происходит без копирования. Все отлично... И вроде безопасно.
|
||||
|
||||
А что если наша таблица с конфигурацией отпимизирована хранить string_view на части одной большой json-конфигурационной строки и ключа в ней нет?
|
||||
А что если наша таблица с конфигурацией оптимизирована хранить string_view на части одной большой json-конфигурационной строки и ключа в ней нет?
|
||||
|
||||
```C++
|
||||
using RefMap = std::map<int, std::string_view>;
|
||||
|
||||
@@ -19,7 +19,7 @@ struct CharTable {
|
||||
};
|
||||
```
|
||||
|
||||
Все ли впорядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами.
|
||||
Все ли в порядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами.
|
||||
|
||||
|
||||
Давайте [потестируем](https://godbolt.org/z/75rTW1nMG)
|
||||
@@ -89,4 +89,4 @@ reference operator[]( size_type pos );
|
||||
1. https://en.cppreference.com/w/cpp/language/types
|
||||
2. https://en.cppreference.com/w/cpp/container/array/operator_at
|
||||
3. https://en.cppreference.com/w/cpp/types/climits
|
||||
4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html
|
||||
4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html
|
||||
|
||||
@@ -146,7 +146,7 @@ usummate_squares(unsigned long): # @usummate_squares(unsigned l
|
||||
*/
|
||||
```
|
||||
|
||||
GCC 13 на данный момент *(2024 год)* в принципе [не делает](https://godbolt.org/z/xjf7zj768) таких оптимизаций по умолчанию. При этом последнии версии Clang 18
|
||||
GCC 13 на данный момент *(2024 год)* в принципе [не делает](https://godbolt.org/z/xjf7zj768) таких оптимизаций по умолчанию. При этом последние версии Clang 18
|
||||
уже способны свернуть цикл суммирования квадратов и для беззнаковых:
|
||||
|
||||
```asm
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# ODR-violation и разделяемые библиотеки
|
||||
|
||||
[Ранее](odr_violation.md) я рассматривал ODR-violation в общих чертах и предупреждал о том, что может произойти, если случайно выбрать не то имя переменной, структуры или функции в C++. В этой же части я бы хотел продемострировать более изящный пример, не требующий приложения никаких усилий по написанию кривого кода. Достаточно просто иметь кривой код в ваших third-party зависимостях.
|
||||
[Ранее](odr_violation.md) я рассматривал ODR-violation в общих чертах и предупреждал о том, что может произойти, если случайно выбрать не то имя переменной, структуры или функции в C++. В этой же части я бы хотел продемонстрировать более изящный пример, не требующий приложения никаких усилий по написанию кривого кода. Достаточно просто иметь кривой код в ваших third-party зависимостях.
|
||||
|
||||
Недавно я имел дело со странным баг-репортом:
|
||||
|
||||
Во внутренем репозитории с пакетами обновилcя пакет с библиотекой [gtest](https://github.com/google/googletest) -- известная уважаемая библиотека для написания самых разных тестов на C++. И в результате обновления некоторые тесты в конечных приложениях стали внезапно падать.
|
||||
Во внутреннем репозитории с пакетами обновилcя пакет с библиотекой [gtest](https://github.com/google/googletest) -- известная уважаемая библиотека для написания самых разных тестов на C++. И в результате обновления некоторые тесты в конечных приложениях стали внезапно падать.
|
||||
|
||||
Падать они стали по-разному. У одних стали валиться проверяющие ассерты. У других же все работало, проверки проходили, но [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) рапортовал что тестирующий процесс вышел с ненулевым кодом возврата.
|
||||
|
||||
@@ -190,7 +190,7 @@ Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
Ура! Падает!
|
||||
Обе библиотеки имеют свою собсвенную версию глобальной переменной с одним и тем же неявно экспортируемым именем. Использоваться опять-таки будет только **одна**.
|
||||
Обе библиотеки имеют свою собственную версию глобальной переменной с одним и тем же неявно экспортируемым именем. Использоваться опять-таки будет только **одна**.
|
||||
После загрузки библиотеки, после конструирования глобальной переменной, стандарт C++ требует зарегистрировать (например через `__cxa_atexit`) функцию для вызова деструктора. У нас две библиотеки -- значит две функции будут вызваны. На одном и том же объекте. Double free. Конструктор, кстати, также вызывается дважды по одному и тому же адресу:
|
||||
|
||||
```C++
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Команде однажды завели баг-репорт: "Сервис упал c segmentation fault, в core dump стэк-трейс указывает как последнюю функцию перед падением что-то из вашей библиотеки. Разберитесь!". Упал сервис ровно один раз за полгода.
|
||||
|
||||
Этим чем-то был вызов `free` где-то глубоко-глубоко внутри библиотери Protobuf. И несколько последующих стэк-фреймов указывали на вызов деструктора уже в нашей библиотеке. Потратив некоторое время на анализ кода деструктора, дежурный инженер не нашел ничего подозрительного и предположил, что это похоже на какую-то ранее встреченную проблему в Protobuf. И как воспроизвести никто не представлял. Тупик...
|
||||
Этим чем-то был вызов `free` где-то глубоко-глубоко внутри библиотеки Protobuf. И несколько последующих стэк-фреймов указывали на вызов деструктора уже в нашей библиотеке. Потратив некоторое время на анализ кода деструктора, дежурный инженер не нашел ничего подозрительного и предположил, что это похоже на какую-то ранее встреченную проблему в Protobuf. И как воспроизвести никто не представлял. Тупик...
|
||||
|
||||
Я заинтересовался этой загадочной историей и залез в core dump поглубже.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public:
|
||||
};
|
||||
```
|
||||
|
||||
Пользователи интерфейса нареализовывали свойх имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада!
|
||||
Пользователи интерфейса нареализовывали своих имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада!
|
||||
|
||||
Вы были уставши и злы. А быть может это были и не вы, а какой-то менее опытный коллега, которому поручили доработать интерфейс. В общем, на свет родилась правка
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ int32_t foo(int32_t x, int32_t y) {
|
||||
```
|
||||
При вызове функции `foo` на стеке, после аргументов (хотя не факт что аргументы будут переданы через стек), будет выделено (просто вершина стека будет сдвинута) еще 4 байта (а может быть и больше, кто ж знает, что там настроено у компилятора!) под переменную z. А может быть и не будет выделено (например, если компилятор оптимизирует переменную и сложит результат сразу в регистр `rax`). Дикая природа удивительна, неправда ли?
|
||||
|
||||
Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете `attribute ( ( naked ) )`. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобожается автоматически. Потому, как известно, вот такой код порождает висячий указатель
|
||||
Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете `attribute ( ( naked ) )`. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобождается автоматически. Потому, как известно, вот такой код порождает висячий указатель
|
||||
|
||||
```C
|
||||
int32_t* foo(int32_t x, int32_t y) {
|
||||
@@ -78,7 +78,7 @@ int main() {
|
||||
Тут каждый вызов `alloca` не приводит к переполнению стека сам по себе. Но
|
||||
если `use_alloca` будет заинлайнена компилятором по какой-либо причине, мы получим [SIGSEGV](https://godbolt.org/z/1xWsjqK4G)
|
||||
|
||||
Использование `alloca` и VLA крайне не рекомендуется. [man](https://man7.org/linux/man-pages/man3/alloca.3.html) упоминает случай, когда их использование может быть оправдано: ваш код полагается на setjmp/longjump и нормальный менеджмент динамически выделенной памяти можеь быть осложнен, а стек все равно будет очищен даже при longjmp. Не буду спрашивать, зачем оно вам...
|
||||
Использование `alloca` и VLA крайне не рекомендуется. [man](https://man7.org/linux/man-pages/man3/alloca.3.html) упоминает случай, когда их использование может быть оправдано: ваш код полагается на setjmp/longjump и нормальный менеджмент динамически выделенной памяти может быть осложнен, а стек все равно будет очищен даже при longjmp. Не буду спрашивать, зачем оно вам...
|
||||
|
||||
|
||||
alloca и vla действительно в среднем быстрее чем динамическая аллокация. Но если уж нужно действительно быстро, то вариант с преаллоцированным массивом или массивом фиксированной длины [получше будет](https://quick-bench.com/q/JWSPzPFknaSECE2W1fPiQvnEdGs)
|
||||
@@ -106,4 +106,4 @@ int main(int argc, char* argv[]) {
|
||||
1. https://lwn.net/Articles/749064/
|
||||
2. https://man7.org/linux/man-pages/man3/alloca.3.html
|
||||
3. https://nullprogram.com/blog/2019/10/27/
|
||||
4. https://en.cppreference.com/w/c/language/array
|
||||
4. https://en.cppreference.com/w/c/language/array
|
||||
|
||||
@@ -129,14 +129,14 @@ auto f = [](float a) -> float {
|
||||
|
||||
int main() {
|
||||
return integrate((float(*)(float))(f));
|
||||
// комилируется и работает
|
||||
// компилируется и работает
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
|
||||
// https://godbolt.org/z/fqzdse1Ya
|
||||
|
||||
// ниблоиды в std чаще определяются так, а не с помощью лямбл
|
||||
// ниблоиды в std чаще определяются так, а не с помощью лямбд
|
||||
struct {
|
||||
static float operator()(float x) {
|
||||
return x;
|
||||
@@ -210,7 +210,7 @@ float integrate<float (*)(float)>(float (*)(float)):
|
||||
pxor xmm0, xmm0
|
||||
cvtsi2ss xmm0, ebx
|
||||
add ebx, 1
|
||||
call rbp // ! нет информации о функии -- вызов по указателю
|
||||
call rbp // ! нет информации о функции -- вызов по указателю
|
||||
addss xmm0, DWORD PTR [rsp+12]
|
||||
movss DWORD PTR [rsp+12], xmm0
|
||||
cmp ebx, 26
|
||||
|
||||
@@ -127,7 +127,7 @@ NULL-терминированные строки -- унаследованная
|
||||
В новых C-библиотеках стараться проектировать API, использующие пару указатель + длина, а не только лишь указатель на NULL-терминированную последовательность.
|
||||
|
||||
От этого наследия страдают программы на всех языках, вынужденные взаимодействовать с C API.
|
||||
Rust, например, использует отдельные типы `CStr` и `CString` для подобных строк и переход к ним из нормального кода всегда сопровоздается мучительными тяжелыми преобразованиями.
|
||||
Rust, например, использует отдельные типы `CStr` и `CString` для подобных строк и переход к ним из нормального кода всегда сопровождается мучительными тяжелыми преобразованиями.
|
||||
|
||||
Использование NULL-терминатора встречается не только для текстовых строк. Так, например, библиотека [SRILM](http://www.speech.sri.com/projects/srilm/) активно использует 0-терминированные последовательности числовых идентификаторов, создавая этим дополнительные проблемы.
|
||||
Семейство функций exec в Linux принимают NULL-терминированные последовательности указателей.
|
||||
|
||||
@@ -83,7 +83,7 @@ free(): invalid pointer
|
||||
Program terminated with signal: SIGSEGV
|
||||
```
|
||||
|
||||
`std::shared_ptr<Button>(this)` создает новый shared_ptr, который ничего знаеть не знает о существовании другого умного указателя, управляющего объектом. Что разумеется приводит к попытке повторного освобождения памяти: сначала одним указателем, затем другим. Что иногда даже может работать успешно... Неопределенное поведение все-таки!
|
||||
`std::shared_ptr<Button>(this)` создает новый shared_ptr, который ничего знает не знает о существовании другого умного указателя, управляющего объектом. Что разумеется приводит к попытке повторного освобождения памяти: сначала одним указателем, затем другим. Что иногда даже может работать успешно... Неопределенное поведение все-таки!
|
||||
|
||||
Хорошо, давайте чинить. Забудем пока про конструктор. Попробуем хотя бы починить метод `click`.
|
||||
|
||||
@@ -156,7 +156,7 @@ int main() {
|
||||
|
||||
Но что-то все еще не то. Полная красота не достигнута... Ну разумеется!
|
||||
Ведь в вашем приложении будут не только кнопки. Но и комбо-боксы, текстовые поля и прочее...
|
||||
И что к каждому классу поотдельности `public std::enable_shared_from_this<ClassName>` пририсовывать?
|
||||
И что к каждому классу по отдельности `public std::enable_shared_from_this<ClassName>` пририсовывать?
|
||||
|
||||
Нет. Разумнее прицепить его к базовому классу и забыть. Давайте так сделаем.
|
||||
|
||||
@@ -320,7 +320,7 @@ event received
|
||||
widget valid
|
||||
```
|
||||
|
||||
Осталось пойти и сделать конструтор приватным с помощью приватного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
|
||||
Осталось пойти и сделать конструктор приватным с помощью приватного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
|
||||
@@ -8,9 +8,9 @@ std::function<R(Args)> f = /* все что угочно,
|
||||
и результатом будет R */
|
||||
```
|
||||
|
||||
Благодаря технике _type-erasure_ (стирание типа), `std::function` может хранить в себе что угодно. У этого, конечно, есть цена — посредственная производительность: конкретный вызываемый объет должен быть перемещен в кучу, выделение памяти, динамическая диспетчеризация вызова... Если мы не пишем чего-то высоконагруженного, то цена не очень высока.
|
||||
Благодаря технике _type-erasure_ (стирание типа), `std::function` может хранить в себе что угодно. У этого, конечно, есть цена — посредственная производительность: конкретный вызываемый объект должен быть перемещен в кучу, выделение памяти, динамическая диспетчеризация вызова... Если мы не пишем чего-то высоконагруженного, то цена не очень высока.
|
||||
|
||||
Однако, благодаря тому же самомуму стиранию типов и тому, как оно реализовано, `std::function` обладает еще некоторыми потрясающими спецэффектами!
|
||||
Однако, благодаря тому же самому стиранию типов и тому, как оно реализовано, `std::function` обладает еще некоторыми потрясающими спецэффектами!
|
||||
|
||||
### Спецэффект 1. Вариантность
|
||||
|
||||
@@ -107,7 +107,7 @@ std::function<void(Keyboard*)> junion = senior;
|
||||
|
||||
```C++
|
||||
std::function<void(InputDevice*)> senior = [](auto){}; // Аллокация и перемещение лямбды на кучу! Стерли тип лямбды внутри
|
||||
std::function<void(Keyboard*)> junion = std::move(senior); // Типы разные. Шаблоны инвариантны. Еще одна аллокация! и перемещенине исходной std::function на кучу. Стираем ее тип.
|
||||
std::function<void(Keyboard*)> junion = std::move(senior); // Типы разные. Шаблоны инвариантны. Еще одна аллокация! и перемещение исходной std::function на кучу. Стираем ее тип.
|
||||
```
|
||||
А если цепочки передачи таких функций с изменением типов будут более длинными — становится уже не так здорово.
|
||||
|
||||
@@ -133,7 +133,7 @@ Program terminated with signal: SIGSEGV
|
||||
```
|
||||
И разумеется при наличии таких цепочек сохранять переданные в аргументах ссылки становится особенно сомнительным занятием.
|
||||
|
||||
Проблему с переизбытком аллокаций C++26 пердлагает решать с помощью `std::function_ref` — невладеющих ссылок на вызаваемый объект. Создайте ваш объект один раз и храните, а дальше передавайте на него ссылку с удобным интерфейсом. Вариантность остается в комплекте с возможностью получить dangling reference на другой `std::function_ref` в цепочке присваиваний.
|
||||
Проблему с переизбытком аллокаций C++26 предлагает решать с помощью `std::function_ref` — невладеющих ссылок на вызаваемый объект. Создайте ваш объект один раз и храните, а дальше передавайте на него ссылку с удобным интерфейсом. Вариантность остается в комплекте с возможностью получить dangling reference на другой `std::function_ref` в цепочке присваиваний.
|
||||
|
||||
### Спецэффект 2. Отломанный const
|
||||
|
||||
@@ -227,4 +227,4 @@ std::function<int(int)> f = [data = std::make_unique<int>(42)](int x) { return *
|
||||
## Полезные ссылки
|
||||
1. [Copyable Function proposal](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf)
|
||||
2. [Variance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science))
|
||||
3. [Type Erasure](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure)
|
||||
3. [Type Erasure](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# transform | filter — zero-cost абстракция, за которую нужно платить дважды
|
||||
|
||||
Когда С++20 анонсировал добавление `ranges` в стандарнтую библиотеку, вокруг было очень много обсуждений.
|
||||
Когда С++20 анонсировал добавление `ranges` в стандартную библиотеку, вокруг было очень много обсуждений.
|
||||
Кто-то радовался (как, например, я, по наивности), кто-то наоборот высказывал опасения насчет этой новой и якобы удобной функциональности. Особенно много возмущений и критики было со стороны разработчиков игр:
|
||||
|
||||
- Эти шаблоны на шаблонах компилируются долго! (и это правда)
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
Пример с бэнчмарком `transform | filter` против обычного старого дедовского for-loop обсуждался на форумах, показывался на конференциях, ну и здесь тоже без него не обойдется.
|
||||
|
||||
Как известно, [std::ranges примудали чтоб в C++ было удобнее реализовывать печать календаря](https://www.youtube.com/watch?v=8yV2ONeWXyI&t=1s), а также суммировать квадраты чисел. Календарь печатать слишком долго, поэтому будем суммировать квадраты.
|
||||
Как известно, [std::ranges придумали чтоб в C++ было удобнее реализовывать печать календаря](https://www.youtube.com/watch?v=8yV2ONeWXyI&t=1s), а также суммировать квадраты чисел. Календарь печатать слишком долго, поэтому будем суммировать квадраты.
|
||||
|
||||
```C++
|
||||
// Суммировать будем "маленькие" квадраты, чтоб как-то оправдать использование filter после transform
|
||||
@@ -54,11 +54,11 @@ static int sum_small_squares_ranges(std::vector<int>& v) {
|
||||
|
||||
----
|
||||
|
||||
Обычно в этом сборнике я овещаю случаи undefined и unspecified поведения. И очень редко какие-то well-defined вещи. Однако, ситуация с `transform | filter` сочетанием оказалась настолько неожиданной, что обойти ее стороной нельзя. И речь не о производительности.
|
||||
Обычно в этом сборнике я оcвещаю случаи undefined и unspecified поведения. И очень редко какие-то well-defined вещи. Однако, ситуация с `transform | filter` сочетанием оказалась настолько неожиданной, что обойти ее стороной нельзя. И речь не о производительности.
|
||||
|
||||
Давайте попробуем другой пример, более приближенный к реальности. Ведь на C++ не только квадраты считают, но еще и какую-то бизнес-логику программируют.
|
||||
|
||||
Вы подготовили список API calls запросов к AWS и собиратесь их исполнить, какие-то могут завершиться ошибкой, какие-то выполниться успешно.
|
||||
Вы подготовили список API calls запросов к AWS и собираетесь их исполнить, какие-то могут завершиться ошибкой, какие-то выполниться успешно.
|
||||
|
||||
Конструкция
|
||||
`requests | transform(execute) | filter(succeded)` выглядит очень чисто, красиво и понятно.
|
||||
@@ -184,7 +184,7 @@ https://github.com/ericniebler/range-v3/issues/1090
|
||||
|
||||
Вернемся к началу. Цепочка `filter | transform` медленнее в 2 раза. Успешные реквесты выполняются два раза. Так и задумано. Из лучших побуждений. И из проблемного дизайна итераторов в C++.
|
||||
|
||||
`Range` `r` в современном C++ это абстактная штука, у которой есть `begin(r)`, возвращающий итератор `it`. И `end(r)`, возвращающий так называемый _sentinel_ `s` — еще одна абстрактная штука, с которой сравнивается итератор, чтоб понять, закончилась ли последовательность.
|
||||
`Range` `r` в современном C++ это абстрактная штука, у которой есть `begin(r)`, возвращающий итератор `it`. И `end(r)`, возвращающий так называемый _sentinel_ `s` — еще одна абстрактная штука, с которой сравнивается итератор, чтоб понять, закончилась ли последовательность.
|
||||
|
||||
Так что процесс итерации по какому-либо range выглядит следующим образом
|
||||
|
||||
@@ -202,7 +202,7 @@ while (it != s) {
|
||||
2. Разыменование — извлечение элемента
|
||||
3. Продвижение к следующему элементу
|
||||
|
||||
Посмотрим, что пишут в стадарте про итератор `views::filter`
|
||||
Посмотрим, что пишут в стандарте про итератор `views::filter`
|
||||
|
||||
|
||||
```
|
||||
@@ -219,7 +219,7 @@ current_ = ranges::find_if(std::move(++current_),
|
||||
return *this;
|
||||
```
|
||||
|
||||
Ага, то есть при продвижении итератора выполняется `find_if`, который разымeyет `++current_`, чтоб пременить к нему предикат фильтрации.
|
||||
Ага, то есть при продвижении итератора выполняется `find_if`, который разымeyет `++current_`, чтоб применить к нему предикат фильтрации.
|
||||
И потом мы его опять разыменуем, чтоб достать значение.
|
||||
|
||||
Вот оно удвоение!
|
||||
@@ -260,7 +260,7 @@ console.log(result); // Array [1, 1, 3]
|
||||
|
||||
`flatMap` — довольно общая операция, трансформирующая каждый элемент последовательности в какую-то новую последовательность и объединяющая их в одну "плоскую" последовательность.
|
||||
|
||||
В стандартной библиотеке С++ непосредственно одного комбинатора `flatMap` (тут бы он неверняка назывался `flat_transform`) нету. Но того же эффекта можно достичь с помощью цепочки `transform | join`. Остенется лишь найти подходящий контейнер для представления последовательности из одного либо нуля элементов.
|
||||
В стандартной библиотеке С++ непосредственно одного комбинатора `flatMap` (тут бы он наверняка назывался `flat_transform`) нету. Но того же эффекта можно достичь с помощью цепочки `transform | join`. Остенется лишь найти подходящий контейнер для представления последовательности из одного либо нуля элементов.
|
||||
|
||||
И такой контейнер есть — `std::optional`. В C++26 ему добавили методы `.begin()` и `.end()`. Этот диапазон состоит либо из одного элемента, если `optional` содержит объект, или из нуля, если не содержит. И его можно использовать для фильтрации элементов!
|
||||
|
||||
@@ -427,7 +427,7 @@ address of grades after: 37319408 // ! same address, no copy
|
||||
|
||||
```Rust
|
||||
// Rust вариант страдает от своих особенностей с лайфтаймами
|
||||
// Которые частично рещаются с помощью Generic Associated Types
|
||||
// Которые частично решаются с помощью Generic Associated Types
|
||||
// Но как абстрактный пример сойдет
|
||||
trait Iterator {
|
||||
type Value;
|
||||
|
||||
@@ -97,7 +97,7 @@ int main() {
|
||||
|
||||
Я не смог найти ни одного компилятора доступного онлайн, на котором бы можно было бы воспроизвести последовательность оптимизаций, приводящих к падению невероятной красоты. Но я видел несколько закрытых bug репортов в отношении Apple Clang, который такое проворачивал.
|
||||
|
||||
LLVM может генерировать под x86 инструкцию `ud2` -- это недопустимая инструкция, часто используема как индикатор недостижимого кода. Если программа попытается ее выполнить, она умрет от сигнала SIGILL.
|
||||
LLVM может генерировать под x86 инструкцию `ud2` -- это недопустимая инструкция, часто используемая как индикатор недостижимого кода. Если программа попытается ее выполнить, она умрет от сигнала SIGILL.
|
||||
Код, который провоцирует неопределенное поведение может быть помечен как недостижимы и в дальнейшем заменен на `ud2` или выброшен.
|
||||
В нашем замечательном примере компилятору вполне известно, что `buffer.size() == 0`. И его не меняли.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Атрибут [[assume]]
|
||||
|
||||
"Есть некоротая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
|
||||
"Есть некоторая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
|
||||
|
||||
Да, С++23 наконец-то дал простым пользователям инструмент целенаправленного **внедрения** неопределенного поведения в их код. Такой инструмент, правда, давно уже был и так, но специфичный для конкретного компилятора. C++23 же всего лишь стандартизировал его. Так что радуйтесь, никаких больше уродливых `__builtin_assume`!
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
На самом деле, конечно, причина есть: компиляторы глупые, быстрый и оптимальный код получить хочется, а на ассемблере писать не очень хочется. Хотя, конечно, разработчики ffmpeg с этим не согласятся — они поэтому целенаправленно делают ассемблерные вставки, не доверяя компиляторам С.
|
||||
|
||||
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику новвовведения С++23.
|
||||
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику нововведения С++23.
|
||||
|
||||
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианну величину
|
||||
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианную величину
|
||||
|
||||
```Rust
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
@@ -97,7 +97,7 @@ void sum(int count, /* чтобы получить доступ к списку
|
||||
|
||||
C простой, маленький язык. В нем не так много типов. Примитивы, указатели, да пользовательские структуры.
|
||||
|
||||
В C++ есть ссылки. Есть объекты с интересными конструкторами и деструкторами. И вы уже наверняка догадались о том, что будет неопеределенное поведение, если засунуть ссылку или такой объект в качестве аргумента вариативной функции. Еще больше возможностей для веселой отладки!
|
||||
В C++ есть ссылки. Есть объекты с интересными конструкторами и деструкторами. И вы уже наверняка догадались о том, что будет неопределённое поведение, если засунуть ссылку или такой объект в качестве аргумента вариативной функции. Еще больше возможностей для веселой отладки!
|
||||
|
||||
Но C++ не был бы самим собой, если бы в нем эту проблему не «решили». И так у нас есть C++-style вариадики:
|
||||
|
||||
@@ -145,4 +145,4 @@ ProcessBy([](auto&&...){ do_something(); });
|
||||
|
||||
# Полезные ссылки
|
||||
1. https://habr.com/ru/post/430064/
|
||||
2. https://en.cppreference.com/w/c/variadic/va_list
|
||||
2. https://en.cppreference.com/w/c/variadic/va_list
|
||||
|
||||
@@ -205,7 +205,7 @@ struct Unit {
|
||||
```
|
||||
|
||||
Из-за константного поля объекты `Unit` теряют операцию присваивания.
|
||||
Их нелья менять местами — `std::swap` не работает больше.
|
||||
Их нельзя менять местами — `std::swap` не работает больше.
|
||||
`std::vector<Unit>` нельзя больше просто так отсортировать... В общем, сплошное удобство.
|
||||
|
||||
Но самое интересное начинается, если сделать что-то такое
|
||||
@@ -224,7 +224,7 @@ std::cout << unit.back().id << "";
|
||||
Компилятор имеет право воспринимать происходящее следующим образом:
|
||||
- В векторе 1 элемент
|
||||
- Вектор не реаллоцировался.
|
||||
- Указатель на элемет в первом `cout` и во втором `cout` один и тот же.
|
||||
- Указатель на элемент в первом `cout` и во втором `cout` один и тот же.
|
||||
- И там и там используется константное поле
|
||||
- Я его уже читал при первом `cout`
|
||||
- Зачем мне его читать еще раз, это же константа!
|
||||
@@ -279,4 +279,4 @@ std::cout << unit.back().id << "";
|
||||
## Полезные ссылки
|
||||
1. https://isocpp.org/wiki/faq/const-correctness
|
||||
2. https://miyuki.github.io/2016/10/21/std-launder.html
|
||||
3. https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func
|
||||
3. https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func
|
||||
|
||||
@@ -217,7 +217,7 @@ int main() {
|
||||
m.set("Metric", val, std::move(comm)); // компилируется, как и хотели
|
||||
m.set("MName", val, std::string_view("comment")); // не компилируется, хорошо
|
||||
auto gen_comment = []()->std::string { return "comment"; };
|
||||
m.set("MName", val, gen_comment()); // рабоатет отлично
|
||||
m.set("MName", val, gen_comment()); // работает отлично
|
||||
}
|
||||
// https://godbolt.org/z/zjWGWY4xh
|
||||
```
|
||||
|
||||
@@ -72,7 +72,7 @@ int main() {
|
||||
std::cout << (5 * OptionalPositive { 5 });
|
||||
}
|
||||
```
|
||||
Это компилируется и выдает результат `6005`. Потому как выполняется user-defined неявное приведенине к `bool`, который далее неявно приводится к `int`. Все правильно.
|
||||
Это компилируется и выдает результат `6005`. Потому как выполняется user-defined неявное приведение к `bool`, который далее неявно приводится к `int`. Все правильно.
|
||||
|
||||
Последние версии Clang хотя бы вывают частично [предупреждения](https://gcc.godbolt.org/z/fs98G3o3f)
|
||||
```
|
||||
@@ -86,13 +86,13 @@ int main() {
|
||||
Правда, если поменять тип константы слева на `double`, в Clang 19. предупреждение исчезнет. Но компилироваться оно [не перестанет](https://gcc.godbolt.org/z/MhafPcTvv).
|
||||
|
||||
Никогда, если только у вас не C++98, не определяйте неявный `operator bool`! Он всегда должен быть `explicit`. Если вы боитесь, что это заставит вас делать `static_cast<bool>` там, где этого не хочется делать, то не переживайте!
|
||||
С++ определяет несколько контекстов, в которых `explicit operator bool` все равно может быть вызван неявно: в условиях `if`, `for` и `while`, а также в логических операциях. Этого достаточно для большинства использнований `operator bool`.
|
||||
С++ определяет несколько контекстов, в которых `explicit operator bool` все равно может быть вызван неявно: в условиях `if`, `for` и `while`, а также в логических операциях. Этого достаточно для большинства использований `operator bool`.
|
||||
|
||||
Если у вас C++98... Я вам очень соболезную. Но и даже в вашем печальном случае есть решение. Чудовищно громоздкое, но решение — можете ознакомиться с устаревшей [Safe Bool Idiom](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool) в свободное время в качестве домашнего задания. Если коротко, вместо `operator bool` предгалалось определить
|
||||
Если у вас C++98... Я вам очень соболезную. Но и даже в вашем печальном случае есть решение. Чудовищно громоздкое, но решение — можете ознакомиться с устаревшей [Safe Bool Idiom](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool) в свободное время в качестве домашнего задания. Если коротко, вместо `operator bool` предлагалось определить
|
||||
|
||||
```C++
|
||||
// Указатель на метод в приватном классе!
|
||||
typedef void (SomePrivateClass::*bool_type) () const;
|
||||
operator bool_type(); // неявное приведение к этому указателю
|
||||
```
|
||||
И тогда бы ваш объект в условных операциях неявно приводился бы к указателю, а указатель бы далее неявно приводился к `bool`.
|
||||
И тогда бы ваш объект в условных операциях неявно приводился бы к указателю, а указатель бы далее неявно приводился к `bool`.
|
||||
|
||||
@@ -50,7 +50,7 @@ fn add(x: i32, y: i32) -> i32 {
|
||||
```
|
||||
|
||||
Обоснования, почему не обязательно писать в конце функции `return`, следующие:
|
||||
1. В функции может быть ветвление логики. В одной из веток может вызываться код, который не предполагает возврата: бесконечный цикл, исключение, `std::exit`, `std::longjmp` или что-то иное, помеченное аттрибутом `[[noreturn]]`. Проверить на наличие такого кода не всегда возможно.
|
||||
1. В функции может быть ветвление логики. В одной из веток может вызываться код, который не предполагает возврата: бесконечный цикл, исключение, `std::exit`, `std::longjmp` или что-то иное, помеченное атрибутом `[[noreturn]]`. Проверить на наличие такого кода не всегда возможно.
|
||||
2. Функция может содержать ассемблерную вставку со специальным кодом финализации и инструкцией `ret`.
|
||||
|
||||
Проверить наличие формального `return`, конечно, можно. Но нам разрешили не писать иногда (очень иногда!) чисто формальную строчку, а компиляторам разрешили не считать это ошибкой.
|
||||
|
||||
@@ -10,8 +10,8 @@ template <class T>
|
||||
struct STagged {};
|
||||
|
||||
|
||||
using S1 = STagged<struct Tag1>; // преобъявление струкруты Tag1
|
||||
using S2 = STagged<struct Tag2*>; // преобъявление струкруты Tag2
|
||||
using S1 = STagged<struct Tag1>; // предобъявление струкруты Tag1
|
||||
using S2 = STagged<struct Tag2*>; // предобъявление струкруты Tag2
|
||||
|
||||
void fun(struct Tag3*); // предобъявление структуры Tag3
|
||||
|
||||
@@ -93,7 +93,7 @@ int main() {
|
||||
}
|
||||
```
|
||||
|
||||
Clang способен предепреждать о подобном.
|
||||
Clang способен предупреждать о подобном.
|
||||
|
||||
С++11 и новее предлагают *universal initialization* (через `{}`), которая не совсем *universal* и имеет свои проблемы.
|
||||
C++20 предлагает еще одну *universal* инициализацию, но уже снова через `()`...
|
||||
|
||||
@@ -55,7 +55,7 @@ struct Matrix {
|
||||
};
|
||||
}
|
||||
|
||||
// И вот месяц назад вы добавили восхитетельную
|
||||
// И вот месяц назад вы добавили восхитительную
|
||||
// перегрузку для доступа к элементу.
|
||||
// Библиотека используется с разными версиями C++, так что
|
||||
// перегрузка под feature-control флагом -- все отлично
|
||||
|
||||
Reference in New Issue
Block a user