├── README.md ├── README_ENG.md ├── concurrency ├── condition_variable.md ├── double_lock.md ├── filesystem.md ├── jthread.md ├── race_condition.md ├── shared_ptr.md ├── signal_unsafe.md ├── std_async.md └── vptr.md ├── how_to_find_ub.md ├── lifetime ├── coroutines_and_lifetimes.md ├── decltype_auto_and_explicit_types.md ├── direct_initialization_references.md ├── for_loop.md ├── lambda_capture.md ├── lifetime_extension.md ├── proxy_objects.md ├── self_init.md ├── string_view.md ├── ternary_operator.md ├── tuple_creation.md ├── unexpected_mutability.md ├── use-after-move.md ├── use_after_free_in_general.md └── vector_invalidation.md ├── numeric ├── char_sign_extension.md ├── floats.md ├── integer_promotion.md ├── narrowing.md ├── overflow.md └── unsigned_unary_minus.md ├── pointer_provenance ├── array_placement_new.md ├── invalid_pointer.md ├── misaligned_reference.md └── strict_aliasing.md ├── runtime ├── array_overrun.md ├── dll_and_odr_violation.md ├── endless_loop.md ├── garbage_collector.md ├── noexcept.md ├── nullptr_dereference.md ├── odr_violation.md ├── ownership_and_exceptions.md ├── recursion.md ├── reserved_names.md ├── rvo_vs_raii.md ├── static_initialization_order_fiasco.md ├── static_inline.md ├── trivial_types_and_ABI.md ├── uninitialized.md ├── unreachable_sentinel.md ├── virtual_functions.md └── vla.md ├── standard_lib ├── aligned_storage.md ├── enable_if_void_t.md ├── forward.md ├── function_pass_and_address_restriction.md ├── images │ └── transform_filter_bench.png ├── iostreams.md ├── map_subscript.md ├── null_terminated_string.md ├── ranges_views_lazy.md ├── shared_from_this.md ├── shared_ptr_constructor.md ├── std_function_const.md ├── stl_constructors.md ├── transform_filter_ranges.md ├── uniform_int_distribution.md └── vector_resize_reserve.md ├── syntax ├── assume.md ├── c_variadic.md ├── comma_operator.md ├── comparison_operator_rewrite.md ├── const_launder.md ├── default_default_constructor.md ├── explicit_but_implicit.md ├── function-try-catch.md ├── implicit_bool.md ├── missing_return.md ├── most_vexing_parse.md ├── move.md ├── multidimensional_subscript.md └── zero_size.md └── what_is_ub.md /README_ENG.md: -------------------------------------------------------------------------------- 1 | # C++ programmer's guide to undefined behavior 2 | 3 | The book (started just as a series of blogposts) is originaly written in Russian language, and this repository currenlty doesn't have full and organized translation for English 4 | 5 | In cooperation with [PVS-studio](https://pvs-studio.com/), I'm working on extended English version. It's available as a series of 11(12) posts in PVS-studio blog 6 | 7 | 8 | - [Part 1](https://pvs-studio.com/en/blog/posts/cpp/1129/) 9 | - [Part 2](https://pvs-studio.com/en/blog/posts/cpp/1136/) 10 | - [Part 3](https://pvs-studio.com/en/blog/posts/cpp/1149/) 11 | - [Part 4](https://pvs-studio.com/en/blog/posts/cpp/1156/) 12 | - [Part 5](https://pvs-studio.com/en/blog/posts/cpp/1160/) 13 | - [Part 6](https://pvs-studio.com/en/blog/posts/cpp/1163/) 14 | - [Part 7](https://pvs-studio.com/en/blog/posts/cpp/1174/) 15 | - [Part 8](https://pvs-studio.com/en/blog/posts/cpp/1178/) 16 | - [Part 9](https://pvs-studio.com/en/blog/posts/cpp/1182/) 17 | - [Part 10](https://pvs-studio.com/en/blog/posts/cpp/1193/) 18 | - [Part 11](https://pvs-studio.com/en/blog/posts/cpp/1199/) 19 | - [Part 12](https://pvs-studio.com/en/blog/posts/cpp/1211/) 20 | 21 | ------- 22 | 23 | **NOTE: This series may not contain some materials from the original Russian version. There were changes in sources after publication in PVS-studio blog** 24 | 25 | C++ language is constantly evolving. I learn more facinating bugs. Get feedback and fix mistakes in text. But as a full-time employed engineer, I cannot effectively maintain & update both Russian and English versions. 26 | 27 | Eventually, full English translation will be available in this repository. 28 | -------------------------------------------------------------------------------- /concurrency/condition_variable.md: -------------------------------------------------------------------------------- 1 | # Condition variable, или как сделать все правильно и уйти в deadlock 2 | 3 | Синхронизация потоков это сложно, хотя у нас и есть примитивы. Такой себе каламбур. 4 | Хорошо, если есть готовые высокоуровневые абстракции в виде очередей или каналов. 5 | Но иногда приходится мастерить их самому. С использованием более низкоуровневых вещей: мьютексов, атомарных переменных и обвязки вокруг них. 6 | 7 | `condition_variable` — примитив синхронизации, позволяющий одному или нескольким потокам ожидать сообщений от других потоков. Ожидать пассивно, не тратя время CPU впустую на постоянные проверки чего-то в цикле. Поток просто снимается с исполнения, ставится в очередь операционной системой, а по наступлении определенного события — уведомления — от другого потока пробуждается. Все замечательно и удобно. 8 | 9 | Сам по себе примитив `condition_variable` не передает никакой информации, а только служит для пробуждения или усыпления потоков. Причем пробуждения, из-за особенностей реализации блокировок, могут случаться ложно, самопроизвольно (spurious), а не только лишь по непосредственной команде через `condition_variable`. 10 | 11 | Потому типичное использование требует дополнительной проверки условий и выглядит как-то так. 12 | 13 | ```C++ 14 | std::condition_variable cv; 15 | std::mutex event_mutex; 16 | bool event_happened = false; 17 | 18 | // исполняется в одном потоке 19 | void task1() { 20 | std::unique_lock lock { event_mutex }; 21 | // предикат гарантированно проверяется под захваченной блокировкой 22 | cv.wait(lock, [&] { return event_happened; }); // безпредикатная версия wait ждет только уведомления, 23 | // но может произойти ложное пробуждения (обычно, если кто-то отпускает этот же мьютекс) 24 | ... 25 | // дождались — событие наступило 26 | // выполняем что нужно 27 | } 28 | 29 | // исполняется в другом потоке 30 | void task2() { 31 | ... 32 | { 33 | std::lock_guard lock {event_mutex}; 34 | event_happened = true; 35 | } 36 | // Обратите внимание: вызов notify не обязан быть под захваченной блокировкой. 37 | // Однако, в ранних версиях msvc, а также в очень старой версии из boost были 38 | // баги, требующие удерживать мьютекс захваченным во время вызова notify() 39 | // Но есть случай, когда делать вызов notify под блокировкой необходимо — если 40 | // другой тред может вызвать, например, завершаясь, деструктор объекта cv 41 | cv.notify_one(); // notify_all() 42 | } 43 | ``` 44 | 45 | Хм, внимательный читатель может сообразить, что в task2 мьютекс используется только для защиты булевого флага. 46 | Невиданное расточительство! Целых два системных вызова в худшем случае. Давайте лучше флаг сделаем атомарным! 47 | 48 | ```C++ 49 | std::atomic_bool event_happened = false; 50 | std::condition_variable cv; 51 | std::mutex event_mutex; 52 | 53 | void task1() { 54 | std::unique_lock lock { event_mutex }; 55 | cv.wait(lock, [&] { return event_happened; }); 56 | ... 57 | } 58 | 59 | void task2() { 60 | ... 61 | event_happened = true; 62 | cv.notify_one(); // notify_all() 63 | ... 64 | } 65 | ``` 66 | 67 | Компилируем, запускаем, работает — классно, срочно в релиз! 68 | 69 | Но однажды приходит пользователь и говорит, что запустил task1 и task2 как обычно одновременно, но сегодня внезапно task1 не завершается, хотя task2 отработал! Вы идете к пользователю, смотрите — висит. Перезапускаете — не зависает. Еще перезапускаете — опять не зависает. Перезапускаете 50 раз — все равно не зависает. Сбой какой-то железный разовый, думаете вы. 70 | 71 | Уходите. Через месяц пользователь опять приходит с той же проблемой. И опять не воспроизводится. Ну точно железный сбой, космическая радиация битик какой-то в локальном кэше треда выбивает. Ничего страшного... 72 | 73 | На самом деле в программе ошибка, приводящая к блокировке при редком совпадении в порядке инструкций. Чтобы понять ее, нужно посмотреть внимательнее на то, как устроен метод `wait` с предикатом. 74 | 75 | ```C++ 76 | // thread a 77 | std::unique_lock lock {event_mutex}; // a1 78 | // cv.wait(lock, [&] { return event_happened; }) это 79 | while (![&] { return event_happened; }()) { // a2 80 | cv.wait(lock); // a3 81 | } 82 | 83 | // ------------------------------- 84 | // thread b 85 | event_happened = true; // b1 86 | cv.notify_one(); // b2 87 | ``` 88 | 89 | Рассмотрим следующую последовательность исполнения строчек в двух потоках 90 | 91 | ``` 92 | a1 // поток 1 захватывает блокировку 93 | a2 // поток 1 проверяет условие --- истинно, событие не наступило 94 | b1 // поток 2 выставляет событие, 95 | b2 // уведомляет о нем, но поток 1 еще не начал ждать! уведомление потеряно! 96 | a3 // поток 1 начинает ждать и никогда не дождется! 97 | ``` 98 | Отоптимизировали? Возвращайте захват мьютекса обратно. 99 | 100 | Захват мьютекса в уведомляющем потоке гарантирует, что ожидающий уведомления поток либо еще не начал ждать и проверять событие, либо уже ждет. Если же мьютекс не захвачен, мы можем попасть в промежуточное состояние. 101 | 102 | Осторожнее с примитивами! 103 | 104 | ## Полезные ссылки 105 | 1. https://en.cppreference.com/w/cpp/thread/condition_variable 106 | 2. https://stackoverflow.com/questions/2531359/do-condition-variables-still-need-a-mutex-if-youre-changing-the-checked-value-a/2531397#2531397 107 | 3. https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables -------------------------------------------------------------------------------- /concurrency/double_lock.md: -------------------------------------------------------------------------------- 1 | # Повторный захват mutex 2 | 3 | Deadlock это, конечно, печально. Система завязалась в узел и никогда не развяжется. 4 | А сколько мьютексов нужно, чтобы уйти в deadlock? 5 | 6 | Немного подумав, можно решить, что одного достаточно — просто захвати его два раза подряд, не отпуская, в одном и том же потоке. 7 | 8 | Возможно, под какой-то платформой это и так. Но в C++ это неопределенное поведение и 9 | для красивого показательного дедлока нужно два мьютекса. А с одним — ваш фокус не удастся и превратится в фокус от мира UB. 10 | 11 | ```C++ 12 | struct Test{ 13 | std::mutex mutex; 14 | std::vector v = { 1,2,3,4,5}; 15 | 16 | auto fun(int n){ 17 | mutex.lock(); // захватываем 18 | return std::shared_ptr(v.data() + n, 19 | [this](auto...){mutex.unlock();}); 20 | // освободим при смерти указателя 21 | } 22 | }; 23 | 24 | 25 | int main(){ 26 | 27 | Test tt; 28 | auto a = tt.fun(1); // захватили первый раз 29 | std::cout << *a << std::endl; 30 | // указатель жив 31 | auto b = tt.fun(2); // захватили второй раз. UB 32 | std::cout << *b << std::endl; 33 | 34 | return 0; 35 | } 36 | ``` 37 | 38 | Этот пример дает [разные](https://godbolt.org/z/aoren4) результаты на одном и том же компиляторе, на одной и той же платформе, на одном и том же уровне оптимизаций. Просто подключили `pthread` или нет. 39 | 40 | Кто в здравом уме будет такое делать-то? Никто же никогда не захватывает один и тот же мьютекс два раза подряд. 41 | 42 | Даже не знаю... Зачем-то же существуют рекурсивные мьютексы, которые можно захватывать по нескольку раз. 43 | 44 | Да и сводить задачу к уже решенной и переиспользовать написанный код любят: 45 | 46 | ```C++ 47 | template 48 | struct ThreadSafeQueue { 49 | 50 | bool empty() const { 51 | std::scoped_lock lock { mutex_ }; 52 | ... 53 | } 54 | 55 | void push(T x) { 56 | std::scoped_lock lock { mutex_ }; 57 | ... 58 | } 59 | 60 | std::optional pop() { 61 | std::scoped_lock lock { mutex_ }; 62 | if (empty()) { // ! ПОВТОРНЫЙ ЗАХВАТ ! 63 | return std::nullopt; 64 | } 65 | ... 66 | } 67 | 68 | ... 69 | std::mutex mutex_; 70 | }; 71 | ``` 72 | 73 | Чтобы исправить, нужно либо подумать, либо использовать рекурсивный мьютекс. 74 | Но лучше подумать. 75 | 76 | Методов у объекта может быть много. Разработчиков тоже. Они могут не помнить, где есть блокировка, а где нет. Могут засунуть блокировку в один метод, забыв про другие. Так что от повторного захвата мьютекса в одном и том же потоке никто не застрахован. 77 | -------------------------------------------------------------------------------- /concurrency/filesystem.md: -------------------------------------------------------------------------------- 1 | # Конкурентный доступ к файловой системе 2 | 3 | Условия гонки за ресурсы могут возникать на разных уровнях: 4 | - внутри одной программы 5 | - между несколькими программами на одном компьютере 6 | - между разными программами на разных компьютерах 7 | 8 | И почти всегда это нежелательная, ошибочная ситуация, последствия которой варьируются от просто неправильного результата до чудовищных уязвимостей в безопасности. 9 | 10 | Иногда, конечно, система может быть толерантна к таким ошибкам и серьезных проблем не возникнет: например, если страничка магазина запрашивает список товаров на складе, а в этот момент база данных склада обновляется, то вы, как пользователь, возможно, увидите неполный или устаревший список товаров или смесь из старых и новых данных. Но критического ничего не произойдет, если разработчики предусмотрели дополнительные проверки с запросом к базе данных в момент взаимодейтсвия с конкретным товаром из списка... Чтобы вы не купили то, чего больше не существует. 11 | 12 | Файловая система — один из таких ресурсов, гонки за которым естественны и должны учитываться при разработке приложений. Да, самые низкоуровневые проблемы синхронизации чтения и записи в файловую систему берет на себя операционная система и (или) конкретный драйвер. Можно «спокойно» одновременно из разных процессов читать и писать в один и тот же файл и получать мусор или штатные ошибки: низкоуровневые операции `read` и `write` будут как-то упорядочены планировщиком ввода-вывода. С самой файловой системой при этом всё будет в порядке. 13 | 14 | Но высокоуровневые операции над файловой системой в рамках вашей бизнес-логики требуют внимания и осторожности. Ведь самый простой способ получить условие гонки — добавить проверки при открытии файла! 15 | 16 | ```C++ 17 | #include 18 | #include 19 | #include 20 | 21 | namespace fs = std::filesystem; 22 | 23 | int main() { 24 | if (!fs::exists("/my/file")) { 25 | return EXIT_FAILURE; 26 | } 27 | std::ifstream input("/my/file"); 28 | std::string line; input >> line; 29 | // do something 30 | return EXIT_SUCCESS; 31 | } 32 | ``` 33 | 34 | Это класcическая **TOCTOU** (*Time-of-Check-Time-of-Use*) ошибка. Между проверкой и открытием файла, файл может быть удален. 35 | 36 | Но причем тут С++, если такая проблема существет для любого языка программирования? 37 | 38 | Действительно. Например, во всех версиях стандартной библиотеки Rust с 1.0 до 1.58.1 [была](https://blog.rust-lang.org/2022/01/20/cve-2022-21658.html) похожая ошибка в реализации функции `remove_dir_all`. 39 | 40 | ```Rust 41 | // это упрощенный код 42 | // directory: Path 43 | if directory.is_symlink() { 44 | remove_link(directory) 45 | } else { 46 | remove_recursive(directory) 47 | } 48 | ``` 49 | 50 | Между проверкой и удалением злоумышленник мог подменить настоящий каталог на символьную ссылку и добиться удаления данных, к которым у него нет доступа. 51 | 52 | Ошибку исправили: вместо работы с путями, функция стала работать с элементами в каталоге через единожды открытываемый файловый дескриптор. 53 | 54 | А теперь мы можем вернуться к C++: 55 | 56 | В std::filesystem также есть функция `remove_all`, с тем же значением что и версия из Rust. И в большинстве ее реализаций в 2022 году также нашли точно такую же [ошибку](https://issuetracker.google.com/issues/42410010?pli=1)! 57 | 58 | Обсуждение этой ошибки было довольно [бурным](https://www.reddit.com/r/cpp/comments/s8ok0h/possible_toctou_vulnerabilities_in/), поскольку стандарт C++ объявляет неопределенным поведением любые вызовы функций std::filesystem, приводящие к гонке! 59 | 60 | Но они все приводят к гонке! Их корректная работа зависит только от благонадежности других приложений, обращающихся к той же файловой системе. Можно ли в таком случае ничего не исправлять в функции `std::filesystem::remove_all`? 61 | 62 | Конечно нужно! Ошибка была исправлена в GCC версии [11.5](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=104161) и в [Clang-14](https://github.com/llvm/llvm-project/commit/4f67a909902d8ab9e24e171201db189b661700bf) 63 | -------------------------------------------------------------------------------- /concurrency/jthread.md: -------------------------------------------------------------------------------- 1 | # Threads joining 2 | 3 | А вы уже заметили, что в [предыдущих](./race_condition.md) заметках я использую `std::jthread` из C++20 вместо `std::thread`? И зачем? 4 | 5 | А все очень просто: деструктор `std::thread` дурной. 6 | 7 | Везде, где может начать вызываться деструктор `std::thread`, нужно втыкать 8 | ```C++ 9 | // std::thread t1; 10 | if (t1.joinable()) { // Если вы не уверены в богатой жизненной истории 11 | // объекта t1, обязательно выполняйте эту проверку 12 | t1.join(); // или t1.detach() 13 | } 14 | ``` 15 | 16 | Чтобы ознаменовать свое желание (или нежелание) дожидаться окончания выполнения потока. 17 | Иначе деструктор потока повалит вашу программу, вызвав `std::terminate`. 18 | Очень удобно и очень по RAII-шному, неправда ли? 19 | 20 | Нет, конечно, совсем везде втыкать не надо. Если вы знаете, что кто-то другой уже выполнил это заклинание, или содержимое объекта `std::thread` переместили в другой объект (`t2 = std::move(t1)`). 21 | 22 | И тем более не надо просто так втыкать этот код, обращающийся к одному и тому же объекту `std::thread` из разных потоков. Иначе — race condition. Надо синхронизировать. 23 | 24 | И, конечно же, убедитесь что этот код ни в коем случае не будет выполняться параллельно с вызовом деструктора `t1`; Деструктор тоже вызывает `joinable`, а это опять race condition. 25 | 26 | Собираетесь сделать обертку над `std::thread`, чтобы вызывать `join` в ее деструкторе? Спешу порадовать: `join`/`detach` кидают исключения. Со всеми вытекающими [проблемами](../runtime/noexcept.md). 27 | 28 | Здорово, да? Поэтому в примерах был и будет `std::jthread`. Его деструктор сам выполняет `join` и снимает хотя бы часть головной боли. 29 | 30 | А если вас `join` не устраивает, не хотите ждать и пользуетесь `detach`... Ну что ж. Право ваше. Только помните, что все потоки резко и внезапно помрут, когда закончится `main`. 31 | 32 | 33 | ## Полезные ссылки 34 | 1. https://en.cppreference.com/w/cpp/thread/thread 35 | 2. https://en.cppreference.com/w/cpp/thread/jthread -------------------------------------------------------------------------------- /concurrency/race_condition.md: -------------------------------------------------------------------------------- 1 | # Многопоточность. Data race 2 | 3 | Разработка многопоточных приложений это всегда сложно. Проблема синхронизации доступа к разделяемым данным — вечная головная боль. Хорошо, если у вас уже есть оттестированная и проверенная временем библиотека контейнеров, высокоуровневых примитивов, параллельных алгоритмов, берущих на себя контроль за всеми инвариантами. Очень хорошо, если статические проверки компилятора не позволят вам использовать все это добро неправильно. Ах, как было бы хорошо... 4 | 5 | До C++11 и стандартизации модели памяти пользоваться потоками в принципе можно было лишь на свой страх и риск. Начиная с C++11, без привлечения сторонних сил, в стандартной библиотеке есть довольно низкоуровневые примитивы. С С++17 еще появились разные параллельные вариации алгоритмов, но о тонкой настройке количества потоков и приоритетов в них можете даже не думать. 6 | 7 | Так почему бы не взять какую-нибудь готовую серьезную библиотеку (`boost`, `abseil`) — там наверняка умные люди уже пострадали многие часы, чтобы предоставить удобные и безопасные инструменты — и забот не знать?! 8 | 9 | Увы, так не работает. Правильность использования этих инструментов в C++ нужно контроллировать самостоятельно, пристально изучая каждую строчку кода. 10 | Мы все равно втыкаемся в проблемы синхронизации доступа, с аккуратным развешиванием мьютексов и атомарных переменных. 11 | 12 | Ситуация (_data race_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значение. Независимо от языка программирования. 13 | 14 | Но в C++ это не просто ошибка. Это неопределенное поведение. И «возможности» для оптимизации 15 | 16 | ```C++ 17 | int func(const std::vector& v) { 18 | int sum = 0; 19 | for (size_t i = 0; i < v.size(); ++i) { 20 | sum += v[i]; 21 | } 22 | // Data race запрещен, от модификации v в 23 | // параллельном потоке нас «защищает» UB. 24 | 25 | // А значит можно соптимизировать вычисление size 26 | // const size_t v_size = v.size(); 27 | // for (size_t i = 0; i < v_size; ++i) { ... } 28 | return sum; 29 | } 30 | ``` 31 | 32 | А теперь почти что многопоточный `hello world`: 33 | 34 | ```C++ 35 | int main() { 36 | bool terminated = false; 37 | using namespace std::literals::chrono_literals; 38 | 39 | int add_ms = 0; 40 | std::cin >> add_ms; 41 | 42 | std::jthread t1 { [&] { 43 | std::size_t cnt = 0; 44 | while (!terminated) { 45 | ++cnt; 46 | } 47 | std::cout << "count: " << cnt << "\n"; 48 | } }; 49 | 50 | std::jthread t2 { [&] { 51 | std::this_thread::sleep_for(500ms + 52 | std::chrono::milliseconds(add_ms)); 53 | terminated = true; 54 | } }; 55 | } 56 | ``` 57 | 58 | Мы не синхронизировали доступ к всего лишь какому-то `bool`. Ничего же страшного, ведь да? 59 | И в отладочной сборке [все работает](https://godbolt.org/z/E9sf9b). 60 | 61 | Но [если включить оптимизации](https://godbolt.org/z/PoqbMb), цикл в первом потоке 62 | либо не выполнит ни одной итерации (clang), либо никогда не завершится (gcc)! 63 | 64 | Оба компилятора видят, что доступ не синхронизирован. Data race запрещен. Значит, и синхронизировать не надо. Значит, при обращении к переменной `terminate` в заголовке цикла 65 | всегда должно быть одно и то же значение. gcc решает, что всегда будет `false`. clang обнаруживает присваивание `terminated = true` в другом потоке и вытягивает его перед началом цикла. 66 | 67 | Конечно же, тут ошибка намеренная и исправляется легко заменой `bool` на `std::atomic`. 68 | Но в реальной кодовой базе допустить data race просто, а исправить сложнее. 69 | 70 | Однажды я написал что-то подобное: 71 | 72 | ```C++ 73 | enum Task { 74 | done, 75 | hello 76 | }; 77 | std::queue task_queue; 78 | std::mutex mutex; 79 | 80 | std::jthread t1 { [&] { 81 | std::size_t cnt_miss = 0; 82 | while (true) { 83 | if (!task_queue.empty()) { 84 | auto task = [&] { 85 | std::scoped_lock lock{mutex}; 86 | auto t = task_queue.front(); 87 | task_queue.pop(); 88 | return t; 89 | }(); 90 | if (task == done) { 91 | break; 92 | } else { 93 | std::cout << "hello\n"; 94 | } 95 | } else { 96 | ++cnt_miss; 97 | } 98 | } 99 | std::cout << "count miss: " << cnt_miss << "\n"; 100 | } }; 101 | 102 | std::jthread t2 { [&] { 103 | std::this_thread::sleep_for(500ms); 104 | { 105 | std::scoped_lock lock{mutex}; 106 | task_queue.push(done); 107 | } 108 | } }; 109 | ``` 110 | 111 | И оно прекрасно работало, пока код тестировался будучи собранным одним компилятором. 112 | Но при переносе на другую платформу с другим компилятором — [все сломалось](https://godbolt.org/z/f8f8xq). 113 | 114 | Если вы сразу поняли причину, то поздравляю. Иначе — обратите внимание на безобидный метод `empty`. Который «совершенно точно ничего не меняет» и «да ладно, как там вообще может нарушиться консистентность данных?!» 115 | 116 | ---- 117 | 118 | В поиске проблем с доступом к объектам из разных потоков вам помогут статические анализаторы и санитайзеры: например, tsan для gcc/clang (`-fsanitize=thread`). Насколько мне известно, текущая реализация tsan (2021 год) не дружит с asan (address sanitized). Так что не выйдет махом искать и race сondition, и обычные ошибки доступа к памяти с нарушением lifetime. 119 | 120 | 121 | В Rust нельзя создать data race и вызвать неопределенное поведение в безопасном подмножестве языка. Однако, неаккуратно используя `unsafe`, и в нем можно устроить себе проблемы. И будет неопределенное поведение. На то оно и `unsafe`. 122 | 123 | 124 | # Полезные ссылки 125 | 1. https://clang.llvm.org/docs/ThreadSanitizer.html 126 | 2. https://en.cppreference.com/w/cpp/thread/mutex 127 | 3. https://en.cppreference.com/w/cpp/atomic 128 | 4. https://devblogs.microsoft.com/cppblog/using-c17-parallel-algorithms-for-better-performance/ 129 | -------------------------------------------------------------------------------- /concurrency/shared_ptr.md: -------------------------------------------------------------------------------- 1 | # Потокобезопасен ли `std::shared_ptr`? 2 | 3 | Пожалуй, это самый популярный вопрос для собеседования на позицию C++-разработчика. 4 | И не без причины: этим прекрасным умным указателем так просто пользоваться (в сравнении с его собратом — `std::unique_ptr`), что легко не заметить подвох. В его названии есть `shared`. Да он и спроектирован так, чтобы его можно было разделять между потоками. Что может пойти не так?! 5 | 6 | Всё. 7 | 8 | Новички довольно быстро обнаруживают первую линию костыльно-грабельной обороны бастиона сложности `shared_ptr`: если доступ к самому указателю `shared_ptr` «безопасен», то к объекту `T` все равно надо синхронизировать. 9 | Это очевидно, это заметно, это понятно. Но дальше ведь все просто? 10 | 11 | Нет. 12 | 13 | Дальше притаились волчьи ямы с отравленными копьями. Сам объект-указатель `shared_ptr` не является потокобезопасным. И доступ к самому указателю тоже надо синхронизировать! 14 | 15 | Как же так?! Мы никогда не синхронизировали и у нас все работало. 16 | 17 | Поздравляю, у вас одно из двух: 18 | 1. Либо все доступы к указателю из разных потоков только на чтение. И тогда проблем действительно нет. 19 | 2. Программа работает по воле случая. 20 | 21 | ```C++ 22 | using namespace std::literals::chrono_literals; 23 | std::shared_ptr str = nullptr; 24 | 25 | std::jthread t1 { [&]{ 26 | std::size_t cnt_miss = 0; 27 | while (!str) { 28 | ++cnt_miss; 29 | } 30 | std::cout << "count miss: " << cnt_miss << "\n"; 31 | std::cout << *str << "\n"; 32 | } }; 33 | 34 | std::jthread t2 { [&] { 35 | std::this_thread::sleep_for(500ms); 36 | str = std::make_shared("Hello World"); 37 | } 38 | }; 39 | ``` 40 | 41 | Аналогично другим примерам с [race condition](./race_condition.md) код выше [перестает](https://godbolt.org/z/zocsYo) работать при изменении уровня оптимизации. 42 | 43 | Но ведь вы наверняка что-то слышали; все-таки есть в `shared_ptr` кое-что потокобезопасное... 44 | 45 | Да. Есть. Счетчик ссылок. Больше ничего потокобезопасного в `std::shared_ptr` нет. 46 | Атомарный счетчик ссылок как раз и позволяет без проблем копировать один и тот же указатель (увеличивая счетчики) в разные потоки и не синхронизировать вручную вызовы деструкторов (уменьшающих счетчики) в разных потоках. 47 | 48 | Если вам надо менять указатель из разных потоков, то вам нужен `std::atomic>` (C++20). Либо использовать функции ` std::atomic_load`/`std::atomic_store` и прочие — у них есть специальные перегрузки для `shared_ptr`. 49 | 50 | С `std::weak_ptr` все то же самое. 51 | 52 | ## Полезные ссылки 53 | 1. https://stackoverflow.com/questions/9127816/stdshared-ptr-thread-safety-explained 54 | 2. https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2 55 | -------------------------------------------------------------------------------- /concurrency/signal_unsafe.md: -------------------------------------------------------------------------------- 1 | # Сигнало(не)безопасность 2 | 3 | Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться 4 | вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов. 5 | 6 | Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку — 7 | выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает I/O, захватывает блокировки... 8 | 9 | Сигналы прерывают нормальный ход исполнения программы и могут быть обработаны в произвольном потоке. 10 | Поток мог начать выделять память, захватить блокировку в аллокаторе и в этот момент быть прерванным сигналом. Если обработчик сигнала в свою очередь запросит выделение памяти... Будет повторный захват блокировки в одном и том же потоке. Неопределенное поведение. 11 | 12 | И результат может быть самым неожиданным. Например, в OpenSSH в 2006 году была обнаружена критическая уязвимость, с возможностью удаленно получить root доступ к системам с запущенным sshd сервером. Баг непосредственно связан с кодом, вызывавшим malloc и free при обработке сигналов. Уязвимость исправили, но в 2020, спустя 14 лет, ee случайно занесли обратно. Ошибку снова обнаружили и исправили лишь в 2024 году, и кто знает сколько раз и кто воспользовался этой [RegreSSHion](https://en.wikipedia.org/wiki/RegreSSHion) за 4 года! 13 | 14 | Очень легко можно продемонстрировать проблему на следующем примере 15 | ```C++ 16 | std::mutex global_lock; 17 | 18 | int main() { 19 | std::signal(SIGINT, [](int){ 20 | std::scoped_lock lock {global_lock}; 21 | printf("SIGINT!\n"); 22 | }); 23 | 24 | { 25 | std::scoped_lock lock {global_lock}; 26 | printf("start long job\n"); 27 | sleep(10); 28 | printf("end long job\n"); 29 | } 30 | sleep(10); 31 | } 32 | ``` 33 | 34 | Если мы скомпилируем эту программу под Linux (не забыв указать `-pthread`), запустим и нажмем `Ctrl+C`, то она зависнет навсегда из-за повторного захвата мьютекса одним и тем же потоком. Если же забудем `-pthread`, то не зависнет и отработает «ожидаемым» образом. 35 | 36 | Под Windows эта программа также работает «ожидаемо» из-за специфики обработки сигналов — там для обработки `SIGINT`/`SIGTERM` всегда неявно порождается новый поток. 37 | 38 | В любом случае этот код некорректен из-за использования сигналонебезопасной функции внутри обработчика сигналов. 39 | 40 | Обработка сигналов — вопрос крайне платформоспецифичный и зависит от конкретной прикладной задачи и архитектуры вашего приложения. Также это довольно сложный вопрос, если учитывать, что во время обработки одного сигнала нас могут прервать для обработки другого. 41 | 42 | Наиболее часто встречаемое использование обработки сигналов — корректное завершение приложения, с очисткой ресурсов, закрытием соединений — graceful shutdown. В таком случае обычно обработка сигналов сводится к выставлению и проверке некоторого глобального флага. 43 | 44 | Стандарты C и C++ описывают специальный целочисленный тип — `sig_atomic_t`. При доступе к переменным этого типа гарантируется сигналобезопасность. На практике этот тип может оказаться просто алиасом для `int` или `long`. `volatile sig_atomic_t` можно использовать в качестве глобального флага, выставляемого в обработчике сигналов. Но только в однопоточной среде. Тут `volatile` необходим только для предотвращения нежелательной оптимизаций — компилятор не делает предположений о возможной обработке сигналов и прерывании нормального потока выполнения программы. 45 | 46 | Нужно помнить, что `volatile` не дает гарантий потокобезопасности. И в многопоточной среде необходимо использовать настоящие атомарные типы, поддерживаемые на вашей платформе. Например, `std::atomic`. Если, конечно, `std::atomic::is_lock_free` истинно. 47 | 48 | ### Как бороться? 49 | 50 | 1. Делать обработчики сигналов как можно более простыми 51 | 2. Отключать автоматический прием сигналов и выполнять их обработку в рамках обычного исполнения программы (см., например, `sigprocmask` и `sigwait`) 52 | 3. Сверяться с документацией, безопасно ли использование той или иной функции в контексте обработчика сигналов 53 | 4. Для флагов обработки сигналов использовать атомарные переменные, lock-free структуры или, если приложение однопоточное, `volatile sig_atomic_t`. 54 | 55 | 56 | ### Полезные ссылки 57 | 1. https://man7.org/linux/man-pages/man7/signal-safety.7.html 58 | 2. https://www.gnu.org/software/libc/manual/html_node/Blocking-Signals.html 59 | 3. https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal 60 | 4. https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_chapter/libc_24.html 61 | 5. https://en.cppreference.com/w/cpp/utility/program/sig_atomic_t 62 | -------------------------------------------------------------------------------- /concurrency/vptr.md: -------------------------------------------------------------------------------- 1 | # Гонки за vptr 2 | 3 | Одна команда инженеров очень любила модель акторов. А еще они очень любили писать на C++ и реализовывать все с нуля, чтобы ни в коем случае не брать внешние зависимости. Поэтому они стали делать свою собственную реализацию акторной модели. 4 | 5 | Так сначала у них появился базовый класс 6 | 7 | ```C++ 8 | class Actor { 9 | public: 10 | virtual ~Actor() = default; 11 | // мы опустим детали и объекты 12 | // для передачи сообщений между акторами 13 | // важно лишь что был метод run 14 | virtual void run() = 0; 15 | }; 16 | ``` 17 | 18 | И этого было им достаточно и довольно долго. Пока внезапно не обнаружилось, что надо бы уметь запускать несколько акторов конкурентно и параллельно. 19 | 20 | Тогда появился класс-наследник 21 | 22 | ```C++ 23 | class AsyncActor: public Actor { 24 | protected: 25 | virtual void RunImpl() = 0; 26 | 27 | void run() final { 28 | actor_thread_ = std::make_unique([this]{ 29 | LOG_DEBUG("Started Asynchronously"); 30 | this->RunImpl(); 31 | }) 32 | } 33 | private: 34 | std::unique_ptr actor_thread_; 35 | } 36 | ``` 37 | 38 | И все было также здорово как и раньше. И все наследники `AsyncActor` работали исправно как и было задумано. Да вот только некрасиво как-то это все было! Метод `RunImpl` вместо `run` нужно переопределять. Да и вообще все использования асинхронных акторов следовали паттерну 39 | 40 | ```C++ 41 | SomeAsyncActor actor{...}; 42 | actor.run(); 43 | ``` 44 | 45 | `run` всегда надо было вызывать явно. А если его забыть — то ведь ничего не запустится же. А если запустить дважды, то произойдет страшное! Неудобно. Да еще и `unique_ptr` глаза мозолит... 46 | А что если сделать по-умному?! 47 | 48 | И тогда они переписали `AsyncActor` 49 | 50 | ```C++ 51 | // protected, чтоб не вызвать run больше извне 52 | class AsyncActor: protected Actor { 53 | private: 54 | // Хопа! Можно написать так красиво и будет компилироваться! 55 | // используем jthread, чтоб не думать про join 56 | std::jthread actor_thread_ { 57 | [actor=this]{ 58 | LOG_DEBUG("Started Asynchronously"); 59 | actor->run(); 60 | } 61 | }; 62 | }; 63 | ``` 64 | 65 | И теперь достаточно было просто сделать 66 | 67 | ```C++ 68 | class SomeAsyncActor : public SomeAsyncActor { 69 | void run() override {...} 70 | }; 71 | 72 | SomeAsyncActor actor{...}; 73 | ``` 74 | чтобы актор был успешно создан и сразу же запущен, как и требовалось всегда. 75 | 76 | Команда была крайне довольна таким изящным решением. Они тестировали его долго и упорно вручную. И все было отлично. На радостях они решили, что можно отключить печать отладочных логов на этапе компиляции. Так что строка `LOG_DEBUG(...)` превратилась в ничто. 77 | 78 | Они пересобрали программу. Протестировали несколько раз. Все продолжало работать. Они задеплоили приложение... И оно упало с ошибкой сегментации при старте. Открывши core dump, разработчики увидели: `Pure virtual function called`. Но ведь все же работало?! Они включили логи обратно... И программа снова стала работать корректно. 79 | 80 | ----- 81 | 82 | В нашей истории оказалось целых два race conditions, спрятанных в самом неожиданном месте: в неявном обращении к указателю на таблицу виртуальных методов (vptr)! 83 | 84 | В главе про [невиртуальные виртуальные функции](../runtime/virtual_functions.md) мы уже обсуждали, что при вызове виртуального метода из конструктора или деструктора вызывается метод текущего класса, а не переопределенный. 85 | 86 | В основных реализациях (Clang и GCC) виртуальных методов такой эффект достигается за счет **переписывания** указателя на таблицу виртуальных функций при входе в конструктор/деструктор и выходе из них. 87 | 88 | Таким образом, если `actor_thread` вызовет `run` раньше чем исполнение дойдет до конструктора `SomeAsyncActor`, будет вызван метод не того класса, который ожидается быть вызванным. 89 | Аналогично с деструктором. Ну а чего вы хотели, случайно вызывая методы на частично уничтоженном или еще не сконструированном объекте?! Это вообще-то неопределенное поведение. 90 | 91 | Мы можем легко продемонстрировать эффект: 92 | 93 | #### Падение на деструкторе 94 | 95 | ```C++ 96 | class SomeAsyncActor : public AsyncActor { 97 | public: 98 | SomeAsyncActor() = default; 99 | private: 100 | void run() override { 101 | printf("DO DO DO\n"); 102 | } 103 | }; 104 | 105 | int main() { 106 | SomeAsyncActor s; 107 | // Раскомментируйте, чтоб перестало падать 108 | // std::this_thread::sleep_for(std::chrono::milliseconds(5)); 109 | } 110 | ``` 111 | [Результат](https://gcc.godbolt.org/z/fhbrE135q) c `Clang 18.1 -O3 -std=c++20 -pthread`: 112 | ``` 113 | pure virtual method called 114 | terminate called without an active exception 115 | Program terminated with signal: SIGSEGV 116 | ``` 117 | 118 | #### Падение на конструкторе 119 | ```C++ 120 | ... 121 | class AsyncActor: protected Actor { 122 | private: 123 | std::jthread actor_thread_ { 124 | [actor = this]{ 125 | actor->run(); 126 | } 127 | }; 128 | // искусственная задержка начала конструирования наследника 129 | std::string metadata { 130 | (std::this_thread::sleep_for(std::chrono::milliseconds(1)), "metadata") 131 | }; 132 | }; 133 | 134 | class SomeAsyncActor : public AsyncActor { 135 | public: 136 | SomeAsyncActor() = default; 137 | private: 138 | void run() override { 139 | printf("DO DO DO\n"); 140 | } 141 | }; 142 | 143 | int main() { 144 | SomeAsyncActor s; 145 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); 146 | } 147 | ``` 148 | [Результат](https://gcc.godbolt.org/z/5vrWzWPGe) c `Clang 18.1 -O3 -std=c++20 -pthread`: 149 | ``` 150 | pure virtual method called 151 | terminate called without an active exception 152 | Program terminated with signal: SIGSEGV 153 | ``` 154 | ---- 155 | Что ж, а на вопрос "при чем же тут отладочные логи?" я предлагаю теперь читателю ответить самостоятельно. 156 | -------------------------------------------------------------------------------- /how_to_find_ub.md: -------------------------------------------------------------------------------- 1 | # Как искать неопределенное поведение? 2 | 3 | Очень частный вопрос, который задавали мне, задавал я сам себе и другим. Да и каждый C++ разработчик, к сожалению, должен его задавать. 4 | 5 | Ответ на него в общем случае — никак. Это алгоритмически неразрешимая задача практически ничем не отличающаяся от задачи останова. Но программистов как палками ни гоняй, все равно будут решать неразрешимые задачи, так что 6 | для конкретного кода и для конкретных входных данных иногда есть способы дать ответ. 7 | 8 | ---- 9 | 10 | Можно проверить код до компиляции различными статическими анализаторами: 11 | - CppCheck 12 | - Clang Static Analyzer 13 | - PVS Studio 14 | - И другие 15 | 16 | Достаточно умный анализатор, работающий с графом потока выполнения программы, знающий сотни ловушек стандарта, способен найти многие проблемы и привлечь внимание к сомнительному коду. Но не все и не всегда. 17 | 18 | ---- 19 | 20 | Компиляторы `Clang` и `GCC` с включенными флагами `-Wall -Wpedantic` [способны](https://godbolt.org/z/zM4r1s) находить некоторые ошибки. 21 | 22 | ---- 23 | 24 | Мы можем сами проверять часть кода в compile-time на различных наборах входных данных, используя `constexpr`. В контексте, вычисляемом на этапе компиляции, UB [запрещено](https://godbolt.org/z/qGGYeP): 25 | 26 | ```C++ 27 | constexpr int my_div(int a, int b) { 28 | return a / b; 29 | } 30 | 31 | namespace test { 32 | template 33 | constexpr int div_test(const int (&A)[N], const int (&B)[N]) { 34 | int x = 0; 35 | for (auto i = 0u; i < N; ++i) { 36 | x = ::my_div(A[i], B[i]); 37 | } 38 | return x; 39 | } 40 | 41 | constexpr int A[] = {1,2,3,4,5}; 42 | constexpr int B[] = {1,2,3,4,0}; 43 | static_assert((div_test(A, A), true)); // OK 44 | static_assert((div_test(A, B), true)); // Compliation error, zero division 45 | } 46 | ``` 47 | 48 | Но `constexpr` не везде применим, в зависимости от версии стандарта налагает ограничения на тело функции, а также неявно применяет `inline` спецификатор, «запрещая» отрывать определение функции в отдельную единицу трансляции (или, по-простому, определение придется разместить в заголовочном файле). 49 | 50 | ---- 51 | 52 | Наконец, если мы не смогли найти ошибки статическим анализом (внешними утилитами или компилятором), можно прибегнуть в динамическому анализу. 53 | 54 | При сборке компиляторами `Clang` или `GCC` можно включить санитайзеры 55 | `-fsanitize=undefined`, `-fsanitize=address`, `-fsanitize=thread`, позволяющие [отлавливать](https://godbolt.org/z/va44E7) ошибки в run-time, но ценой значительных накладных расходов, так что пользоваться этими средствами нужно только на этапе тестирования и разработки. 56 | 57 | Также для отладочных сборок код стандартных библиотек иногда инструментирован assert'aми. Так, например, сделано для различных итераторов стандартной библиотеке в поставке `msvc` (Visual Studio) 58 | 59 | ---- 60 | 61 | Поскольку неопределенное поведение проявляется в возможностях оптимизации тем или иным компилятором, нужно 62 | собирать свой код под разные платформы, с разными уровнями оптимизаций, и сравнивать его поведение. Код без ошибок должен быть переносимым и вести себя одинаково (если, конечно, его задача не генерировать совершенно случайные значения). 63 | 64 | 65 | ---- 66 | Тесты, различные сборки, статический и динамический анализ — способы немного поднять уверенность в том, что в вашем коде нет UB. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять. И даже этого может быть недостаточно. 67 | 68 | Еще есть путь отключения каких-либо оптимизаций флагами компилятора, флаги включающие различные нарушения стандарта (знаменитый `-fpermissive`), превращающие язык C++ во что-то совершенно иное. Но призываю вас никогда не идти этим путем. Ваш код станет непереносимым. Ваш код перестанет быть кодом на C++. Лучше сразу возьмите другой язык программирования. 69 | 70 | ### Полезные ссылки 71 | 1. https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html 72 | 2. https://clang.llvm.org/docs/AddressSanitizer.html 73 | 3. https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html 74 | 4. https://shafik.github.io/c++/undefined_behavior/2019/05/11/explporing_undefined_behavior_using_constexpr.html 75 | 5. https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance?view=msvc-160 76 | 6. http://cppcheck.sourceforge.net/ -------------------------------------------------------------------------------- /lifetime/for_loop.md: -------------------------------------------------------------------------------- 1 | # Синтаксический сахар с ложкой дегтя: range-based for 2 | 3 | Как мы уже выяснили ранее, константные lvalue (да и rvalue тоже) ссылки доставляют много радости в C++ благодаря правило продления жизни для временных объектов. 4 | 5 | Правило хитрое и состоит не только в том, что `const&&` или `&&` продляют жизнь временному объекту (но только первая такая ссылка). На самом деле правило такое: 6 | 7 | все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(`;`). 8 | ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта `const&` или `&&` ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта. 9 | 10 | То есть: 11 | 12 | ```C++ 13 | const int& x = 1 + 2; // временные объекты 1, 2, 14 | // порождают временный объект 3 (сумма). 15 | // Их время жизни закончится на ; 16 | // Но мы присваиваем 3 константной ссылке, 17 | // Ее область видимости простирается ниже, дальше ; 18 | // Так что время жизни продлевается. 19 | // Таким образом: 1, 2 — умирают. 3 — продолжает жить 20 | 21 | 22 | const int& y = std::max([](const int& a, const int& b) -> const int& { 23 | return a > b ? a : b; 24 | }(1 + 2, 4), 5); // временные объекты 1,2, 3(сумма), 4, 5 живут до ЭТОЙ ; 25 | // 3, 4 присваиваются константным ссылкам в аргументах лямбда-функии. 26 | // область видимости этих ссылок заканчивается после return 27 | // — она МЕНЬШЕ времени жизни временного объекта. 28 | // ссылки ничего не продлили, но лишили временных объект будущего. 29 | 30 | // 5 прибивается к константной ссылке в аргументе std::max 31 | // Со ссылками на 4, 5 успешно отрабатывает std::max — 32 | // их время жизни еще не закончилось. Ссылки валидны. 33 | 34 | // Ссылка-результат присваивается `y`. Продлений жизни не происходит — 35 | // все временные объекты уже безуспешно попытали счастья на аргументах функций. 36 | // Дело доходит до ; Время жизни всех объектов 1,2,3,4,5 заканчивается. 37 | // `y` становится висячей. Занавес. 38 | ``` 39 | 40 | 41 | Вооружившись полученным пониманием, рассмотрим другой пример и перестанем опять все понимать: 42 | 43 | ```C++ 44 | struct Point { 45 | int x; 46 | int y; 47 | }; 48 | 49 | struct Shape { 50 | public: 51 | using VertexList = std::vector; 52 | VertexList vertexes; 53 | }; 54 | 55 | Shape MakeShape() { 56 | return { Shape::VertexList{ {1,0}, {0,1}, {0,0}, {1,1} } }; 57 | } 58 | 59 | int main() { 60 | for (auto v : MakeShape().vertexes) { 61 | std::cout << v.x << " " << v.y << "\n"; 62 | } 63 | } 64 | ``` 65 | Все [работает](https://godbolt.org/z/r1zbzK), как и ожидается 66 | 67 | Повысим инкапсуляцию, проведем минимальный рефакторинг — сделаем `vertexes` приватным полем с read-only доступом: 68 | 69 | ```C++ 70 | struct Shape { 71 | public: 72 | using VertexList = std::vector; 73 | explicit Shape(VertexList v) : vertexes(std::move(v)) {} 74 | 75 | const VertexList& Vertexes() const { 76 | return vertexes; 77 | } 78 | 79 | private: 80 | VertexList vertexes; 81 | }; 82 | 83 | ... 84 | 85 | int main() { 86 | for (auto v : MakeShape().Vertexes()) { 87 | std::cout << v.x << " " << v.y << "\n"; 88 | } 89 | } 90 | ``` 91 | 92 | И все [сломалось](https://godbolt.org/z/Ejq745). В коде неопределенное поведение. 93 | 94 | Как же так? Разгадка в том, что, несмотря на то, что заголовок range-based for выглядит как единое выражение, пишется и воспринимается как единое выражение, единым выражением он не является. 95 | 96 | С 17 стандарта и дальше конструкция 97 | ```C++ 98 | for (T v : my_cont) { 99 | ... 100 | } 101 | ``` 102 | Рассахаривается в примерно следующее: 103 | ```C++ 104 | 105 | auto&& container_ = my_cont; // sic! 106 | auto&& begin_ = std::begin(container_); 107 | auto&& end_ = std::end(container_); 108 | for (; begin_ != end_; ++begin_) { 109 | T v = *begin_; 110 | } 111 | ``` 112 | 113 | В первом случае 114 | ```C++ 115 | auto&& container_ = MakeShape().vertexes; 116 | // временный объект Shape живет до ;. Он не встретил еще ни одной const& или && 117 | // ссылки 118 | // Подобъект vertexes — считается таким же временным. 119 | // Его время жизни закончится на ; 120 | // Но он встречает && ссылку, область видимости которой простирается ниже 121 | // и продлевает ему жизнь. Причем продлевается жизнь не только лишь подобъекту 122 | // vertexes, а целиком временному объекту Shape, его содержащему 123 | ``` 124 | 125 | Во втором случае: 126 | ```C++ 127 | auto&& container_ = MakeShape().Vertexes(); 128 | // временный объект Shape живет до ;. Но он встречает неявную const& 129 | // ссылку в методе Vertexes(). Ее область видимости ограничена телом метода. 130 | // Продления жизни не происходит. Возвращается ссылка на часть временного объекта 131 | // и присваивается ссылке `container_`. 132 | // Дело доходит до ;. Временный Shape умирает. 133 | // `container_` становится висячей ссылкой. Занавес. 134 | ``` 135 | 136 | Вот так все просто и сломано. 137 | 138 | Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагается на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o). 139 | 140 | --- 141 | 142 | Как избежать проблемы с range-based for? 143 | 144 | - Никогда не забывать делать rvalue [перегрузку](https://godbolt.org/z/TPYzEj) для любых const-методов 145 | - Никогда не использовать никакие выражения после `:` в заголовке цикла. Только переменные или их поля. 146 | - В C++20 использовать синтаксис range-based-for с инициализатором 147 | ```C++ 148 | for (auto cont = expr; auto x : cont) 149 | ``` 150 | - При использовании синтаксиса с инициализатором думать, прежде чем использовать 151 | `auto&&` или `const auto&` для инициализатора. Впрочем, это не только про for... 152 | - Использовать `std::ranges::for_each` 153 | - Не использовать range-based for в C++, пока его не починят 154 | 155 | 156 | ## С++23 157 | 158 | Продление времени жизни объекта в заголовке `range-based-for` было наконец-то исправлено. И теперь получить висячую ссылку стало тяжелее. Но теперь стало проще получить другую [проблему](lifetime_extension.md) 159 | 160 | ## Полезные ссылки 161 | 1. https://en.cppreference.com/w/cpp/algorithm/ranges/for_each 162 | 2. https://en.cppreference.com/w/cpp/language/range-for 163 | 3. https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary 164 | 4. http://josuttis.com/download/std/D2012R0_fix_rangebasedfor_201029.pdf 165 | 166 | -------------------------------------------------------------------------------- /lifetime/lambda_capture.md: -------------------------------------------------------------------------------- 1 | # Списки захвата лямбда-функций 2 | 3 | C++11 подарил нам лямбда-функции и, вместе с ними, еще один способ неявного получения висячих ссылок. 4 | 5 | Лямбда-функция, захватывающая что-либо по ссылке, безопасна до тех пор, пока она не возвращается куда-либо за пределы области, в которой ее создали. Как только мы куда-то возвращаем или сохраняем лямбду, начинается веселье: 6 | 7 | ```C++ 8 | auto make_add_n(int n) { 9 | return [&](int x) { 10 | return x + n; // n — станет висячей ссылкой! 11 | }; 12 | } 13 | 14 | ... 15 | auto add5 = make_add_n(5); 16 | std::cout << add5(5); // UB! 17 | ``` 18 | 19 | Ничего принципиально нового — тут все те же проблемы, что и с возвратом ссылки из функции. 20 | clang иногда [способен выдать предупреждение](https://godbolt.org/z/rsq8hM). 21 | 22 | Но стоит нам принять аргумент `make_add_n` по ссылке — и [никаких предупреждений не будет](https://godbolt.org/z/1K89z9). 23 | 24 | Аналогично проблему [можно наиграть](https://godbolt.org/z/31KdTj) и для методов объектов: 25 | ```C++ 26 | struct Task { 27 | int id; 28 | 29 | std::function GetNotifier() { 30 | return [this]{ 31 | // this — может стать висячей ссылкой! 32 | std::cout << "notify " << id << "\n"; 33 | }; 34 | } 35 | }; 36 | 37 | int main() { 38 | auto notify = Task { 5 }.GetNotifier(); 39 | notify(); // UB! 40 | } 41 | ``` 42 | 43 | Но в этом примере можно заметить `this` в списке захвата и насторожиться. До C++20 же можно [отстрелить ногу](https://godbolt.org/z/WExKPo) чуть менее явно: 44 | ```C++ 45 | struct Task { 46 | int id; 47 | 48 | std::function GetNotifier() { 49 | return [=]{ 50 | // this — может стать висячей ссылкой! 51 | std::cout << "notify " << id << "\n"; 52 | }; 53 | } 54 | }; 55 | ``` 56 | `=` предписывает захватывать все по значению, но захватывается не поле `id`, а сам указатель `this`. 57 | 58 | --------- 59 | 60 | Если видите лямбду, в списке захвата которой есть `this`, `=` (до С++20) или `&`, 61 | обязательно проверьте, как и где эта лямбда используется. Добавьте перегрузки проверки времени жизни захватываемых переменных. 62 | ```C++ 63 | struct Task { 64 | int id; 65 | 66 | std::function GetNotifier() && = delete; 67 | 68 | std::function GetNotifier() & { 69 | return [this]{ 70 | // для this теперь намного сложнее стать висячей ссылкой 71 | std::cout << "notify " << id << "\n"; 72 | }; 73 | } 74 | }; 75 | ``` 76 | 77 | Если возможно, вместо захвата по ссылке, лучше использовать захват по значению или захват с инициализацией перемещением. 78 | 79 | ```C++ 80 | auto make_greeting(std::string msg) { 81 | return [message = std::move(msg)] (const std::string& name) { 82 | std::cout << message << name << "\n"; 83 | }; 84 | } 85 | ... 86 | auto greeting = make_greeting("hello, "); 87 | greeting("world"); 88 | ``` 89 | 90 | ## Полезные ссылки 91 | 1. https://en.cppreference.com/w/cpp/language/lambda 92 | 2. http://cppatomic.blogspot.com/2018/03/modern-effective-c-avoid-default.html -------------------------------------------------------------------------------- /lifetime/self_init.md: -------------------------------------------------------------------------------- 1 | # Еще не мертв, но еще и не жив. Self-reference 2 | 3 | Область видимости объекта начинается сразу же после его объявления. В той же строчке. Поэтому 4 | в С++ очень легко сконструировать синтаксически корректное выражение, использующее еще не сконструированный объект. 5 | 6 | ```C++ 7 | // просто и прямолинейно 8 | int x = x + 5; // UB 9 | 10 | //-------------- 11 | // менее явно 12 | const int max_v = 10; 13 | 14 | void fun(int y) { 15 | const int max_v = [&]{ 16 | // локальный max_v перекрывает глобальный max_v 17 | return std::min(max_v, y); 18 | }(); 19 | ... 20 | } 21 | ``` 22 | 23 | Конечно, такой код вряд ли кто-то будет писать целенаправлено. 24 | Но он может возникать самопроизвольно при применении средств автоматического 25 | рефакторинга. Локальный `max_v` во втором примере мог изначально называться как-то по-другому. Применили автоматическое переименование и получили вместо некомпилирующегося кода, код с неопределенным поведением. 26 | 27 | Причем в следующей версии никакой проблемы не возникает: 28 | 29 | ```C++ 30 | const int max_v = 10; 31 | 32 | void fun(int y) { 33 | const int max_v = [y]{ 34 | // тут виден только глобальный max_v 35 | return std::min(max_v, y); 36 | }(); 37 | ... 38 | } 39 | ``` 40 | 41 | Код, уходящий в область неопределенного поведения при добавлении лишь одного символа — все как мы любим. 42 | 43 | --- 44 | Такой код синтаксически валиден и никто не собирается его запрещать. Более того, он еще и [не всегда](https://godbolt.org/z/7jqo61) приводит к UB. 45 | К UB приводит только использование с, грубо говоря, разыменованием ссылки на этот объект. Почему грубо? Потому что правила такие же, как и с разыменованием `nullptr` — то есть довольно [путанные](https://habr.com/ru/post/513058/), а не просто лишь «никогда нельзя — всегда UB». Хотя использование такой радикальной трактовки уберет вас от многих бед. 46 | 47 | ```C++ 48 | struct ExtremelyLongClassName { 49 | 50 | using UnspeekableInternalType = size_t; 51 | 52 | UnspeekableInternalType val; 53 | 54 | static UnspeekableInternalType Default() { return 5;} 55 | }; 56 | 57 | ExtremelyLongClassName x { x.Default() + 5 }; // Ok, well-defined 58 | 59 | 60 | ExtremelyLongClassName y { 61 | [] ()-> ExtremelyLongClassName::UnspeekableInternalType { 62 | // сложные вычисления 63 | return 1; 64 | }() 65 | }; 66 | 67 | ExtremelyLongClassName z { 68 | [] ()-> decltype(z.Default()) { // Ok, well-defined 69 | // сложные вычисления 70 | return 1; 71 | }() 72 | }; 73 | ``` 74 | 75 | Также эта фича может быть полезна в каких-то [специфических](https://godbolt.org/z/qY18c8) случаях, в которых вам зачем-то нужен объект, ссылающийся сам на себя 76 | 77 | 78 | ```C++ 79 | struct Iface { 80 | virtual ~Iface() = default; 81 | virtual int method(int) const = 0; 82 | }; 83 | 84 | struct Impl : Iface { 85 | explicit Impl(const Iface* other_ = nullptr) : other(other_) { 86 | 87 | }; 88 | 89 | int method(int x) const override { 90 | if (x == 0) { 91 | return 1; 92 | } 93 | if (other){ 94 | return x * other->method(x - 1); 95 | } 96 | return 0; 97 | } 98 | const Iface* other = nullptr; 99 | }; 100 | 101 | int main() { 102 | Impl impl {&impl}; 103 | std::cout << impl.method(5); 104 | } 105 | ``` 106 | 107 | Точно таким же образом, но более запутанно, можно завязать объекты в узел, используя делегирующие конструкторы. Но об этом в отдельной заметке. 108 | 109 | Избежать использования объекта при инициализации его же самого можно, следуя правилу `AAA` (almost always auto): 110 | 111 | Всегда, если это возможно, использовать запись `auto x = T {....}` для объявления и инициализации переменных. 112 | 113 | В такой записи использование объявляемой переменной внутри инициализирующего дает [ошибку компиляции](https://godbolt.org/z/P1rj66). 114 | 115 | 116 | ## Полезные ссылки 117 | 1. https://habr.com/ru/post/513058/ 118 | 2. http://cginternals.github.io/guidelines/articles/almost-always-auto/ 119 | -------------------------------------------------------------------------------- /lifetime/string_view.md: -------------------------------------------------------------------------------- 1 | # string_view — тот же const& только больнее 2 | 3 | С++17 подарил нам тип `std::string_view`, призванный убить сразу двух зайцев: 4 | - Проблемы с перегрузками для функций, которые должны работать хорошо как с C, так и с C++ строками 5 | - А также программистов, периодически забывающих о правилах продления жизни временным объектам. 6 | 7 | И так, проблема: функция хочет считать количество вхождений какого-то символа в строку: 8 | 9 | ```C++ 10 | int count_char(const std::string& s, char c) { 11 | .... 12 | } 13 | 14 | count_char("hello world", 'l'); // создастся временный объект std::string, 15 | // выделится память, скопируется строка, а потом строка умрет и память 16 | // деаллоцируется — плохо, много лишних операций 17 | ``` 18 | 19 | Так что нам нужна перегрузка для С-строк 20 | ```C++ 21 | int count_char(const char* s, char c) { 22 | // мы тут не знаем ничего про длину строки 23 | // она вообще null-териминированная? 24 | 25 | // Можем только написать код, наивно рассчитывающий, что его 26 | // будут вызывать правильно. 27 | ... 28 | } 29 | ``` 30 | 31 | И будем либо дублировать код, немного адаптируя его под C-строки, либо сделаем функцию 32 | 33 | ```C++ 34 | int count_char_impl(const char* s, size_t len, char c) { 35 | ... 36 | } 37 | ``` 38 | 39 | В которую поместим весь дублирующийся код и вызовем ее из перегрузок: 40 | ```C++ 41 | int count_char(const std::string& s, char c) { 42 | return count_char_impl(s.data(), s.size(), c); 43 | } 44 | 45 | int count_char(const char* s, char c) { 46 | return count_char_impl(s, strlen(s), c); 47 | } 48 | ``` 49 | 50 | И тут на помощь приходит `string_view`, как раз таки являющийся парой: указатель и размер. И убивает обе перегрузки: 51 | 52 | ```C++ 53 | int count_char(std::string_view s, char c) { 54 | ... 55 | } 56 | ``` 57 | 58 | И все здорово, хорошо и замечательно, кроме одного но: 59 | 60 | `std::string_view` по сути является ссылочным типом, как `const&`, и его можно конструировать из временных значений. Но, в отличие от просто `const&`, никакого продления жизни не будет. Вернее будет, но не там, где [ожидается](https://godbolt.org/z/nxxrYb). 61 | 62 | ```C++ 63 | auto GetString = []() -> std::string { return "hello"; }; 64 | std::string_view sv = GetString(); 65 | std::cout << sv << "\n"; // dangling reference! 66 | ``` 67 | 68 | В этом примере мы, конечно, почти явно стреляем себе в голову. Можно сделать стрельбу [менее явной](https://godbolt.org/z/PPcarE): 69 | 70 | ```C++ 71 | std::string_view common_prefix(std::string_view a, std::string_view b) { 72 | auto len = std::min(a.size(), b.size()); 73 | auto common_count = [&]{ 74 | for (size_t common_len = 0; common_len < len; ++common_len) { 75 | if (a[common_len] != b[common_len]) { 76 | return common_len; 77 | } 78 | } 79 | return len; 80 | }(); 81 | return a.substr(0, common_count); 82 | } 83 | 84 | 85 | int main() { 86 | using namespace std::string_literals; 87 | { 88 | auto common = common_prefix("helloW", 89 | "hello"s + "World111111111111111111111"); 90 | std::cout << common << "\n"; // ok 91 | } 92 | { 93 | auto common = common_prefix("hello"s + "World111111111111111111111111", 94 | "helloW"); 95 | std::cout << common << "\n"; // dangling ref 96 | } 97 | } 98 | ``` 99 | 100 | Ситуация такая же, как с [ранее рассмотренным](use_after_free_in_general.md) `std::min`. 101 | Только защититься от такой функции `common_prefix`, обернув ее в шаблон с помощью анализа rvalue/lvalue, 102 | намного сложнее: нам нужно разобрать случаи `const char*` и `std::string` для каждого аргумента — в общем, все то, от чего нас введение `std::string_view` «избавило». 103 | 104 | 105 | Влететь в `string_view` можно еще изящнее: 106 | 107 | ```C++ 108 | struct Person { 109 | std::string name; 110 | 111 | std::string_view Initials() const { 112 | if (name.length() <= 2) { 113 | return name; 114 | } 115 | return name.substr(0, 2); // copy — dangling reference! 116 | } 117 | }; 118 | ``` 119 | 120 | Причем [видно](https://godbolt.org/z/TPc4zq), что Clang хотя бы выдает предупреждение. 121 | А gcc — нет. 122 | 123 | Все потому что `std::string_view` настолько легендарный, что в clang сделали хоть какой-то lifetime checker сперва для него. 124 | 125 | 126 | ## Полезные ссылки 127 | 1. https://quuxplusone.github.io/blog/2018/03/27/string-view-is-a-borrow-type/ 128 | 2. https://foonathan.net/2017/03/string_view-temporary/ 129 | 3. https://www.learncpp.com/cpp-tutorial/6-6a-an-introduction-to-stdstring_view/ 130 | 4. https://github.com/isocpp/CppCoreGuidelines/blob/master/docs/Lifetime.pdf 131 | -------------------------------------------------------------------------------- /lifetime/tuple_creation.md: -------------------------------------------------------------------------------- 1 | # Кортежи, стреляющие по ногам 2 | 3 | С C++11 в стандартной библиотеке есть замечательный шаблон класса `std::tuple`. 4 | Кортеж. Гетерогенный список. Отличная и полезная штука. Вот только создать кортеж, ничего не сломав 5 | и при этом получив именно то, что вы хотели — задача совершенно не тривиальная. 6 | 7 | Явно указывать типы элементов очень длинного контейнера — занятие не из приятных. 8 | 9 | С++11 дал нам целых три способа сэкономить на указании типов — разные функции создания кортежей: 10 | 11 | - `make_tuple` 12 | - `tie` 13 | - `forward_as_tuple` 14 | 15 | С++17 дает еще и возможность использовать автовыведение типов и просто писать 16 | 17 | ```C++ 18 | auto t = tuple { 1, "string", 1.f }; 19 | ``` 20 | 21 | Все это великолепное разнообразие дает нам возможность тонко настраивать то, 22 | какие именно типы мы хотим получить в элементах контейнера — ссылочные или нет. 23 | А также возможность ошибиться и получить проблему с lifetime. 24 | 25 | `std::make_tuple` отбрасывает ссылки, приводит ссылки на массивы к указателям, отбрасывает `const`. В общем, 26 | применяет `std::decay_t`. 27 | 28 | При этом есть особенный частный случай, сделанный, как обычно, из благих побуждений. 29 | 30 | Если типом аргумента `make_tuple` является `std::reference_wrapper`, то в кортеже он [превращается](https://godbolt.org/z/bv17q5fEM) 31 | в `T&`. 32 | 33 | ```C++ 34 | int x = 5; 35 | float y = 6; 36 | auto t = std::make_tuple(std::ref(x), 37 | std::cref(y), 38 | "hello"); 39 | static_assert(std::is_same_v>); 43 | ``` 44 | 45 | Конструктор с автовыводом типов особый случай `std::reference_wrapper` [не рассматривает](https://godbolt.org/z/cEd3e69bj). 46 | Но decay происходит. 47 | 48 | ```C++ 49 | int x = 5; 50 | float y = 6; 51 | auto t = std::tuple(std::ref(x), std::cref(y), "hello"); 52 | static_assert(std::is_same_v, 54 | std::reference_wrapper, 55 | const char*>>); 56 | ``` 57 | 58 | `std::forward_as_tuple` всегда конструирует кортеж ссылок. И соответственно можно получить [ссылку на мертвый временный объект](https://godbolt.org/z/8c8EjGq7c). 59 | 60 | ```C++ 61 | int x = 5; 62 | auto t = std::forward_as_tuple(x, 6.f, std::move("hello")); 63 | static_assert(std::is_same_v>); // Да, это rvalue ссылка на массив 67 | 68 | std::get<1>(t); // UB! 69 | ``` 70 | 71 | `std::tie` конструирует кортеж только из `lvalue` ссылок. И подорваться на нем сложнее, но все равно [можно](https://godbolt.org/z/WPP7qca6a), если вы захотите полученный кортеж возвращать из функции. Но эта ситуация совершенно аналогична случаям возврата любых ссылок из функций. 72 | 73 | ```C++ 74 | template 75 | auto tie_consts(const T&... args) { 76 | return std::tie(args...); 77 | } 78 | 79 | int main(int argc, char **argv) { 80 | auto t = tie_consts(1, 1.f, "hello"); 81 | static_assert(std::is_same_v>); 85 | std::cout << std::get<1>(t) << "\n"; // UB 86 | } 87 | ``` 88 | 89 | Общие рекомендации 90 | 91 | 1. Для создания возвращаемых кортежей использовать `make_tuple` с явным указанием cref/ref 92 | либо конструктор, если ссылки не нужны. 93 | 2. `std::tie` использовать только чтобы временно представить набор переменных в виде кортежа: 94 | ```C++ 95 | std::tie(it, inserted) = map.insert({x, y}); // распаковка кортежей 96 | std::tie(x1, y1, z1) == std::tie(x2, y2, z2); // покомпонентное сравнение 97 | ``` 98 | 3. `std::forward_as_tuple` использовать только при передаче аргументов. Нигде не сохранять получаемый кортеж. 99 | 100 | 101 | И в конце бонус. 102 | 103 | Особые любители Python могут захотеть попробовать использовать `std::tie` для выполнения обмена значений переменных. 104 | 105 | ```Python 106 | x, y = y, x 107 | ``` 108 | 109 | ```C++ 110 | int x = 5; 111 | int y = 3; 112 | std::tie(x, y) = std::tie(y, x); 113 | std::cout << x << " " << y; 114 | ``` 115 | 116 | У нас тут не Python, поэтому поведение этого кода неопределено. Но не печальтесь. Всего лишь `unspecified`. 117 | В результате вы получите либо `5 5`, либо `3 3`. -------------------------------------------------------------------------------- /lifetime/vector_invalidation.md: -------------------------------------------------------------------------------- 1 | # std::vector и инвалидация ссылок 2 | 3 | В стандартной библиотеке C++ не очень много последовательных контейнеров с динамической длиной: 4 | - std::list 5 | - std::forward_list 6 | - std::deque 7 | - std::vector 8 | 9 | Из них `std::vector` используется в большинстве случаев. А остальные — только если их особенности становятся действительно необходимыми и дают заметную разницу в улучшении производительности. Так, например, возможность вставки в произвольную позицию за константное число операций в `std::list` [не дает преимущества](https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vector-vs-list.html) в сравнении с `std::vector` (требует линейного времени), пока контейнеры недостаточно большие или размер элементов мал. 10 | 11 | `std::vector`, будучи самым эффективным контейнером, является еще и самым небезопасным. Из-за инвалидации ссылок и итераторов. 12 | 13 | Неосторожное использование `std::vector` вкупе с обилием засахаренных синтаксических конструкций очень легко приводит к неопределенному поведению. 14 | 15 | Простенький пример с очередью задач: 16 | ```C++ 17 | std::optional evaluate(const Action&); 18 | 19 | void run_actions(std::vector actions) { 20 | for (auto&& act: actions) { // UB 21 | if (auto new_act = evaluate(act)) { 22 | actions.push_back(std::move(*new_act)); // UB 23 | } 24 | } 25 | } 26 | ``` 27 | Красиво, коротко, с неопределенным поведением и неправильно. 28 | 29 | - `push_back` может вызвать реаллокацию вектора. Итераторы begin/end инвалидируются — цикл продолжится по уничтоженным данным. 30 | - Если реаллокации не произойдет, цикл пройдет только по тому набору элементов, что были в векторе изначально. До добавленных в процессе дело не дойдет. 31 | 32 | Корректный код: 33 | ```C++ 34 | void run_actions(std::vector actions) { 35 | for (size_t idx = 0; idx < actions.size(); ++idx) { 36 | const auto& act = actions[idx]; 37 | if (auto new_act = evaluate(act)) { 38 | actions.push_back(std::move(*new_act)); 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | В какой-то момент нам захотелось по-быстрому добавить логгирование, чтобы что-то проверить: 45 | ```C++ 46 | void run_actions(std::vector actions) { 47 | for (size_t idx = 0; idx < actions.size(); ++idx) { 48 | const auto& act = actions[idx]; 49 | if (auto new_act = evaluate(act)) { 50 | actions.push_back(std::move(*new_act)); 51 | } 52 | std::cerr << act.Id() << "\n"; // UB! 53 | } 54 | } 55 | ``` 56 | И у нас опять неопределенное поведение — `push_back` может вызвать реаллокацию вектора 57 | и тогда ссылка `act` станет висячей. 58 | 59 | Корректный код: 60 | ```C++ 61 | void run_actions(std::vector actions) { 62 | for (size_t idx = 0; idx < actions.size(); ++idx) { 63 | if (auto new_act = evaluate(actions[idx])) { 64 | actions.push_back(std::move(*new_act)); 65 | } 66 | std::cerr << actions[idx].Id() << "\n"; 67 | } 68 | } 69 | ``` 70 | 71 | Этот простой паттерн с инвалидацией ссылок в векторе может очень легко спрятаться под слоем абстракций. Например — цикл обработки является публичным методом класса `TaskQueue`, а обработка одной задачи — его приватный метод. В таком случае изменение в одном методе, совершенно корректное в рамках него, приведен к UB из-за неявного влияния на другой метод. 72 | 73 | Кое-как защититься от подобной неприятности можно с помощью статических анализаторов, работающих с потоком исполнения программы. Также проблема почти точно ловится санитайзерами или утилитами проверки памяти (например, valgrind). Если, конечно, у вас достаточно хорошие тесты. 74 | 75 | В языке Rust проблема отлавливается на этапе компиляции с помощью borrow checker'а. 76 | 77 | Если вы можете позволить себе просадку производительности, лучше использовать специализированные контейнеры (или адапторы контейнеров) для специфичных задач. 78 | Так `std::queue` по умолчанию использует `std::deque` и не инвалидирует ссылки при добавлении новых элементов. А также ее нельзя неосторожно использовать в range-based-for — у нее нет итераторов begin/end 79 | 80 | ## Полезные ссылки 81 | 1. https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vector-vs-list.html 82 | 2. https://stackoverflow.com/questions/6438086/iterator-invalidation-rules -------------------------------------------------------------------------------- /numeric/char_sign_extension.md: -------------------------------------------------------------------------------- 1 | # char и знаковое расширение 2 | 3 | Возьмем следующую простенькую структуру 4 | 5 | ```C++ 6 | // пример утащен и изменен отсюда: 7 | // https://twitter.com/hankadusikova/status/1626960604412928002 8 | struct CharTable { 9 | static_assert(CHAR_BIT == 8); 10 | std::array _is_whitespace {}; 11 | 12 | CharTable() { 13 | _is_whitespace.fill(false); 14 | } 15 | 16 | bool is_whitespace(char c) const { 17 | return this->_is_whitespace[c]; 18 | } 19 | }; 20 | ``` 21 | 22 | Все ли впорядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами. 23 | 24 | 25 | Давайте [потестируем](https://godbolt.org/z/75rTW1nMG) 26 | 27 | ```C++ 28 | int main() { 29 | CharTable table; 30 | char c = 128; 31 | bool is_whitespace = table.is_whitespace(c); 32 | std::cout << is_whitespace << "\n"; 33 | return is_whitespace; 34 | } 35 | ``` 36 | 37 | При сборке с `-fsanitize=undefined` получаем дивный результат 38 | 39 | ``` 40 | /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36: runtime error: index 18446744073709551488 out of bounds for type 'bool [256]' 41 | /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/array:61:36: runtime error: index 18446744073709551488 out of bounds for type 'bool [256]' 42 | /app/example.cpp:14:38: runtime error: load of value 64, which is not a valid value for type 'bool' 43 | ``` 44 | 45 | Конкретное значение в третьей строке -- совершенно случайное. Было бы очень здорово стабильно увидеть 42, но увы. 46 | 47 | Зато индекс в первых двух строках совсем не случайный. 48 | 49 | Но погодите! 50 | `char c = 128;` это же точно меньше 256. Откуда `18446744073709551488`? 51 | 52 | Будем разбираться. В деле замешаны две удачно разложенные ловушки. 53 | 54 | 1. С/C++ специфичная ловушка: знаковость типа `char` не специфицирована. В зависимости от платформы он может быть как знаковым, так и беззнаковым. На x86 чаще всего является знаковым. И из `char c = 128` получается `c = -128`. 55 | 56 | 2. Ловушка, распространенная во многих языках, имеющих разные типы целых чисел, разной знаковости и длины. Например, [Rust](https://godbolt.org/z/cY1v3rvrK) 57 | ```Rust 58 | pub fn main() { 59 | let c : i8 = -5; 60 | let c_direct_cast = c as u16; 61 | let c_two_casts = c as u8 as u16; 62 | println!("{c_direct_cast} != {c_two_casts}"); 63 | } 64 | ``` 65 | Мы увидим `65531 != 251`. 66 | 67 | При преобразовании знакового целого меньшей длины к беззнаковому целому большей длины происходит знаковое расширение: старшие биты заполняются битом знака. 68 | 69 | Тоже [действует и в C/C++](https://godbolt.org/z/cfcdb5fr3). 70 | 71 | 72 | А теперь остается только взглянуть на сигнатуру `std::array::operator[]`: 73 | ```C++ 74 | reference operator[]( size_type pos ); 75 | ``` 76 | 77 | `size_type` это беззнаковый `size_t`. Под x86 он определенно больше чем `char`. 78 | Происходит прямой каст знакового `char` в `size_t`, знак расширяется, код ломается. Дело закрыто. 79 | 80 | ## Что делать 81 | 82 | Со знаковым расширением иногда способны помочь статические анализаторы. 83 | Нужно понимать что вы делаете при касте чисел и что хотите получить. Часто можно встретить конструкцию вида `uint32_t extended_val = static_cast(byte_val) & 0xFF`, чтоб гарантированно занулить верхние байты и избежать знакового расширения. Аналогичная конструкция может быть и при преобразовании `int32 -> uint64`, и при любых других комбинациях -- только константу правильную писать не забывайте. 84 | 85 | Из-за своей знаковой неспецифицированности тип `char` очень опасен при работе с ним как с типом чисел. Крайне рекомендуется пользоваться соответствующими типами `uint8_t` или `int8_t`. Или другими подходящими, если на вашей целевой платформе в `char` внезапно не 8 бит. 86 | 87 | 88 | ## Полезные ссылки 89 | 1. https://en.cppreference.com/w/cpp/language/types 90 | 2. https://en.cppreference.com/w/cpp/container/array/operator_at 91 | 3. https://en.cppreference.com/w/cpp/types/climits 92 | 4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html -------------------------------------------------------------------------------- /numeric/floats.md: -------------------------------------------------------------------------------- 1 | # Числа с плавающей точкой 2 | 3 | С `float` и `double` в принципе всегда все сложно. Особенно в C++. 4 | 5 | Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что 6 | по IEEE 754 выражение `x/0.0` определяется как `-INF`, `NaN`, или `INF` в зависимости от знака числа `x` (`NaN` для нуля). 7 | 8 | Сравнение вещественных чисел — излюбленная головная боль. 9 | 10 | Выражение `x == y` фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями `-0.0` и `+0.0`, и `NaN`. 11 | О существовании этого и `!=` операторов для вещественных чисел стоит забыть и никогда не вспоминать. 12 | 13 | Для побитового сравнения нужно использовать `memcmp`. 14 | Для сравнения чисел — приближенные варианты вида `std::abs(x - y) < EPS`, где `EPS` — какое-то абсолютное или вычисляемое на основе `x` и `y` значение. А также различные манипуляции с [`ULP`](https://en.wikipedia.org/wiki/Unit_in_the_last_place) сравниваемых чисел. 15 | 16 | Так как стандарт C++ не форсирует IEEE 754, 17 | проверки на `x == NaN` через его свойство `(x != x) == true` могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого 18 | функций `std::isnan`. 19 | 20 | Поддерживается или нет IEEE 754 можно проверить с помощью предопределенной константы 21 | `std::numeric_limits::is_iec559` 22 | 23 | Сужающие преобразования из `float` в знаковые или беззнаковые целые могут повлечь неопределенное поведение, если значение непредставимо в целочисленном типе. Никаких обрезок по модулю `2^N` не предполагается. 24 | 25 | ```C++ 26 | constexpr uint16_t x = 1234567.0; // CE, undefined behavior 27 | ``` 28 | 29 | Обратное преобразование, из целочисленных типов во `float`/`double`, также имеет свои подвохи, не связанные с неопределенным поведением: большие по абсолютной величине целые числа [теряют точность](https://godbolt.org/z/xnr5rMGKf) 30 | 31 | ```C++ 32 | static_assert( 33 | static_cast(std::numeric_limits::max()) == 34 | static_cast(static_cast(std::numeric_limits::max()) + 1) // OK 35 | ); 36 | 37 | static_assert( 38 | static_cast((1LL << 53) - 1) == 39 | static_cast(1LL << 53) // fire! 40 | ); 41 | 42 | static_assert( 43 | static_cast((1LL << 54) - 1) == 44 | static_cast(1LL << 54) // OK 45 | ); 46 | 47 | static_assert( 48 | static_cast((1LL << 55) - 1) == 49 | static_cast(1LL << 55) // OK 50 | ); 51 | 52 | static_assert( 53 | static_cast((1LL << 56) - 1) == 54 | static_cast(1LL << 56) // OK 55 | ); 56 | ``` 57 | 58 | В качестве домашнего задания читателю предлагается самостоятельно сформулировать, почему никогда нельзя хранить деньги в типах с плавающей запятой. 59 | 60 | 61 | ### Плавающая точка и шаблоны 62 | 63 | Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же — [не стоит](https://godbolt.org/z/q55891). 64 | 65 | Для простой параметризации типов константами этот механизм вполне можно использовать без опасений. Однако строить на них паттерн-матчинг с выбором специализаций шаблонов крайне [не рекомендуется](https://godbolt.org/z/cGf9h94cn): 66 | 67 | ```C++ 68 | template 69 | struct X { 70 | static constexpr double val = x; 71 | }; 72 | 73 | template <> 74 | struct X<+0.> { 75 | static constexpr double val = 1.0; 76 | }; 77 | 78 | template <> 79 | struct X<-0.> { 80 | static constexpr double val = -1.0; 81 | }; 82 | 83 | 84 | int main() { 85 | constexpr double a = -3.0; 86 | constexpr double b = 3.0; 87 | std::cout << X::val << "\n"; // печатает +1 88 | std::cout << X<-1.0 * (a + b)>::val << "\n"; // печатает -1 89 | static_assert(a + b == -1.0 * (a + b)); // ok 90 | } 91 | ``` 92 | 93 | По тем же причинам ни в одном языке программирования не рекомендуется использовать значения с плавающей точкой 94 | в качестве ключей ассоциативных массивов. 95 | 96 | ### Полезные ссылки 97 | 1. https://en.cppreference.com/w/cpp/numeric/math/isnan 98 | 2. https://bitbashing.io/comparing-floats.html 99 | 3. https://diego.assencio.com/?index=67e5393c40a627818513f9bcacd6a70d 100 | -------------------------------------------------------------------------------- /numeric/integer_promotion.md: -------------------------------------------------------------------------------- 1 | # Integer promotion 2 | 3 | C++ от C досталось тяжелое наследство. Одна его часть была исправлена и беспощадно зарезана для большей надежности — так, например, поступили с неявными `const` преобразованиями. Другая же часть, доставляющая не меньше проблем, перешла в первозданном виде. 4 | 5 | В C и C++ много различных типов целых чисел разных размеров. И над ними определены операции. Правда, операции определены не для каждого типа чисел. 6 | 7 | Например, здесь нет +, -, *, / для `uint16_t`. Но применить мы их можем. 8 | 9 | ```C++ 10 | uint16_t x = 1; 11 | uint16_t y = 2; 12 | auto a = x - y; 13 | auto b = x + y; 14 | auto c = x * y; 15 | auto d = x / y; 16 | ``` 17 | и [результатом](https://godbolt.org/z/bdKjsor9T) операций над беззнаковыми числами станет число со знаком. 18 | Но стоит тип хотя бы одного аргумента поменять на `uint32_t`, как результат сразу же [теряет знак](https://godbolt.org/z/aY1nhdr37). 19 | 20 | ## Что происходит? 21 | 22 | Происходят две неявные операции: 23 | 24 | 1. Типы, меньшие `int`, приводятся к `int` (integer promotion). Знаковому! Независимо от знаковости исходного типа! 25 | 2. Когда в операции участвуют аргументы разных типов целых чисел, они приводятся к общему типу (usual arithmetic conversion): 26 | - Меньший тип приводится к большему 27 | - Если размеры одинаковы, то знаковый приводится к беззнаковому 28 | 29 | Аналогичные операции проводятся и над числами с плавающей точкой. 30 | За полной таблицей и цепочкой, что и в кого неявно превращается, стоит обратиться к тексту [стандарта](https://eel.is/c++draft/conv.rank). 31 | 32 | ## К чему это приводит? 33 | 34 | 1. К ошибкам в логике: 35 | Неявные преобразования вовлекаются в любую операцию. Вы выполняете сравнение знакового и беззнакового числа и забыли явно привести типы? Готовьтесь к тому, что `-1 < 1` может [вернуть](https://godbolt.org/z/sqvrasjE4) `false`: 36 | ```C++ 37 | std::vector v = {1}; 38 | auto idx = -1; 39 | if (idx < v.size()) { 40 | std::cout << "less!\n"; 41 | } else { 42 | std::cout << "oops!\n"; 43 | } 44 | ``` 45 | 2. К [неопределенному поведению](https://godbolt.org/z/M3Kx3e3q6): 46 | ```C++ 47 | unsigned short x=0xFFFF; 48 | unsigned short y=0xFFFF; 49 | auto z=x*y; 50 | ``` 51 | Integer promotion неявно приводит `x` и `y` к `int`, в котором происходит переполнение. Переполнение `int` — неопределенное поведение. 52 | 3. К трудностями в переносе программ с одной платформы на другую. Если меняется размер `int`/`long` применение правил неявных конверсий к вашему коду также [меняется](https://godbolt.org/z/hs59o3zca): 53 | ```C++ 54 | std::cout << (-1L < 1U); 55 | ``` 56 | Выводит разные значения в зависимости от размера типа `long`. 57 | 58 | ## Что делать? 59 | 60 | 1. Не смешивать в одном выражении знаковые и беззнаковые типы 61 | 2. Уделять особое внимание коду, работающему с типами, меньшими `int`. 62 | 3. Включать предупреждения от компилятора (`-Wconversion`, не всегда работает) 63 | 64 | 65 | ## Полезные ссылки 66 | 1. https://eel.is/c++draft/conv.prom 67 | 2. https://eel.is/c++draft/expr.arith.conv 68 | 3. https://stackoverflow.com/questions/46073295/implicit-type-promotion-rules 69 | 4. https://shafik.github.io/c++/2021/12/30/usual_arithmetic_confusions.html 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /numeric/unsigned_unary_minus.md: -------------------------------------------------------------------------------- 1 | # Унарный минус и беззнаковые числа 2 | 3 | Вы разрабатываете графический интерфейс для игры. У вас уже есть кнопочки, панельки, иконки. Все отлично. И вот вы решаете, чтоб интерфейс ощущался интереснее, реализовать анимацию для _check box_ элемента -- при нажатии для снятия галочки, check_box элемент будет красиво отъезжать в сторону где-то на 30% своей ширины. 4 | 5 | 6 | У вас были подобные структуры и функции 7 | ```C++ 8 | struct Element { 9 | size_t width; // original non scaled width 10 | ... 11 | }; 12 | 13 | using ElementID = uint64_t; // you are using smart component system that uses IDs to refer to elements 14 | 15 | // Positions in OpenGL/DirectX/Vulkan worlds are floats 16 | struct Offset { 17 | float x; 18 | float y; 19 | }; 20 | 21 | size_t get_width(ElementID); 22 | float screen_scale(); 23 | void move_by(ElementID, Offset); 24 | ``` 25 | 26 | И вы добавили свою 27 | 28 | ```C++ 29 | void on_unchecked(ElementID el) { 30 | auto w = get_width(el); 31 | move_by(el, Offset { 32 | -w * screen_scale() * 0.3f, 33 | 0.0f 34 | }); 35 | } 36 | ``` 37 | 38 | Ваш check_box имел ширину 50 пикселей. Вы запустили тест... И элемент улетел за пределы экрана! 39 | 40 | Вы пошли смотреть логи и [обнаружили](https://godbolt.org/z/hbccqG5r8) 41 | ``` 42 | Offset: 5.5340234e+18 0 43 | ``` 44 | 45 | Как же так?! Неопределенное поведение? 46 | 47 | Нет. Вполне определенное. 48 | 49 | Всему виной унарный минус, который мы случайно применили к беззнаковой переменной. 50 | 51 | ``` 52 | For unsigned a, the value of -a is 2^N − a, where N is the number of bits after promotion. 53 | ``` 54 | 55 | Это очень злобная ошибка, которую **не** диагностируют Clang и GCC с флагами `-Wall -Wextra -Wpedantic`; 56 | MSVC же имеет такую [диагностику](https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4146?view=msvc-170) 57 | 58 | Статические анализаторы, например, [PVS-studio](https://pvs-studio.ru/ru/docs/warnings/v2553/) также могут найти ошибку. 59 | 60 | В более современных языках программирования применение унарного минуса к беззнаковым значениям чаще всего не компилируется. Так, например, сделано в Rust, Zig и в Kotlin. 61 | 62 | 63 | ## Полезные ссылки 64 | 1. [C: unary minus operator behavior with unsigned operands](https://stackoverflow.com/questions/8026694/c-unary-minus-operator-behavior-with-unsigned-operands) -------------------------------------------------------------------------------- /pointer_provenance/array_placement_new.md: -------------------------------------------------------------------------------- 1 | # placement new для массивов 2 | 3 | Вам посчастливилось добыть новую суперэффективную библиотеку для управления памятью? 4 | Вы хотите пользоваться ею в C++ и не сталкиваться с надуманным UB из-за проблем с лайфтаймами? 5 | 6 | Вам повезло! Просто выделяйте память своей библиотекой, создавайте 7 | в выделенном буфере объекты с помощью placement new и забот не знайте! 8 | 9 | ```C++ 10 | void* buffer = my_external_malloc(sizeof(T), alignof(T)); 11 | auto pobj = new (buffer) T(); 12 | ``` 13 | 14 | Красиво, просто, здорово! 15 | 16 | А что если мы захотим выделить память и разместить в ней массив? 17 | 18 | Нет ничего проще! 19 | ```C++ 20 | void* buffer = my_external_malloc(n * sizeof(T), alignof(T)); 21 | auto pobjarr = new (buffer) T[n]; 22 | ``` 23 | Все, можно идти пить чай. Задача решена. Мы молодцы. Как похорошел C++ с 11-го стандарта! 24 | 25 | Но не может же быть все так просто? 26 | 27 | Конечно же нет! До C++20 вариант placement new для массивов имеет полное право испоганить вашу память. 28 | 29 | Конструкция 30 | ```C++ 31 | new (buffer) T[n]; 32 | ``` 33 | согласно примерам (§ 8.5.2.4 (15.4)) из стандарта [C++17](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf), 34 | переводится в 35 | ```C++ 36 | operator new[](sizeof(T) * n + x, buffer); 37 | // или operator new[](sizeof(T) * n + x, std::align_val_t(alignof(T)), buffer); 38 | ``` 39 | Где `x` — никак не специфицируемое неотрицательное число, предназначенное, например, чтобы застолбить место под какую-либо 40 | метаинформацию о выделенном массиве: засунуть число элементов в начало области памяти или расставить маркеры начала/конца или еще что-нибудь, что обычно делают аллокаторы. 41 | 42 | То есть placement new для массива вполне может полезть за пределы предоставленного вами буфера. Очень удобно! 43 | 44 | В C++20 восхитительную [формулировку](https://eel.is/c++draft/expr.new#19.4) изменили. 45 | 46 | Теперь же, если конструкция 47 | ```C++ 48 | new (arg1, arg2...) T[n]; 49 | ``` 50 | соответствует вызову стандартного 51 | ```C++ 52 | void* operator new[]( std::size_t count, void* ptr); 53 | ``` 54 | То все будет хорошо. Никаких магических сдвигов на `+x` не возникнет. 55 | 56 | Но если же какой-то доброжелатель определил свой собственный operator placement new... Впрочем, это уже совсем другая история... 57 | 58 | -------------- 59 | 60 | Я не встречал ни одного компилятора, и ни одной поставки стандартной библиотеки, в которых стандартный placement new как-либо двигал указатель на пользовательский буфер. 61 | Реальную угрозу трудноотлавливаемого UB в большей степени представляют user-defined версии placement new. 62 | 63 | Чтобы обезопасить себя и вызвать настоящий стандартный placement new, нужно использовать 64 | `::new` и кастить указатель на буфер к `void*`. 65 | Либо положиться на алгоритмы `std::uninitialized_default_construct_n` и подобные ему. 66 | 67 | 68 | Также нужно отметить, что в C++ нет placement delete синтаксиса. 69 | Мы можем только явно вызвать `operator delete[](void* ptr, void* place)`, стандартная версия которого ничего не делает. 70 | 71 | Тут, конечно, нужно понимать разницу между самим `operator delete` и синтаксическими конструкциями 72 | `delete p` и `delete [] p`. Первый занимается только управлением памятью. Последние же — еще и вызывают деструкторы. 73 | 74 | В C++ нет именно отдельной синтаксической конструкции, чтобы махом вызывать деструкторы элементов массива, созданного с помощью placement new. Это нужно делать вручную или использовать алгоритм `std::destroy`. 75 | 76 | Ни в коем случае не стоит использовать `delete []` против указателя, полученного с помощью placement new []. 77 | Будет [плохо](https://godbolt.org/z/WeWPseKoG). 78 | 79 | 80 | -------------------------------------------------------------------------------- /pointer_provenance/invalid_pointer.md: -------------------------------------------------------------------------------- 1 | # Указатель Шредингера: валиден и невалиден одновременно. 2 | 3 | Что вообще такое указатель? 4 | Когда их пытаются объяснить новичкам в C++, часто говорят, что это число, адрес, указывающий на номер ячейки в памяти, где что-то лежит. 5 | 6 | Это в каком-то смысле справедливо на очень низком уровне — в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется. 7 | Более того, в C++ (не в C), есть указатели, которые вообще не являются адресами в памяти — указатели на поля и методы классов. Но о них мы говорить сейчас не будем. 8 | 9 | Указатель — это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации. 10 | 11 | Для указателей в стандарте C++ подробно расписано, откуда они могут появляться. 12 | Если коротко, то: 13 | 1. Как результат применения операции взятия адреса (`&x` или `std::addressof(x)`) 14 | 2. Как результат вызова оператора `new` (возможно, _placement new_) 15 | 3. Как результат неявного преобразования имени массива или имени функции в указатель. 16 | 4. Как результат некоторой _допустимой_ операции над другим указателем. 17 | 5. Копирование существующего указателя. В частности — копирование `nullptr`. 18 | 19 | Все остальные источники указателей — implementation defined или вообще undefined. 20 | 21 | Главная операция, выполняемая над указателями, — разыменование, то есть получение доступа 22 | к объекту, на который этот указатель ссылается. И вместе с этой операцией приходит главная проблема — ее не ко всем указателям применять можно. Есть и другие операции, которые также применимы не к любому указателю. 23 | Но, конечно, есть единственная операция, допустимая (почти) всегда — сравнение на равенство (не равенство). 24 | 25 | В идеальном светлом мире, от типа объекта зависит множество допустимых над ним операций. Но в случае указателей, и это очень печально, применимость или неприменимость зависит не только от значения указателя, но еще и от того, откуда этот указатель взялся. А также откуда взялись другие указатели! 26 | 27 | ```C++ 28 | int x = 5; 29 | auto x_ptr = &x; // валидный указатель, его МОЖНО разыменовывать 30 | 31 | auto x_end_ptr = (&x) + 1; // валидный указатель, но его НЕЛЬЗЯ разыменовывать 32 | 33 | auto x_invalid_ptr = (&x) + 2; // невалидный указатель, 34 | // само его существование недопустимо. 35 | ``` 36 | 37 | Сравнение указателей на больше или меньше определено только для указателей на элементы одного и того же массива. 38 | Для произвольных указателей — unspecified. 39 | 40 | Арифметика указателей допустима только в пределах одного и того же массива (от указателя на первый элемент до указателя на элемент за последним) Иначе — undefined behavior. 41 | Особый только случай `(&x) + 1` — любой объект считается массивом из одного элемента. 42 | 43 | Пример кода, который валится с UB именно на арифметике указателей, найти сложно, зато можно привести пример с итераторами (которые разворачиваются в указатели). 44 | 45 | ```C++ 46 | std::string str = "hell"; 47 | str.erase(str.begin() + 4 + 1 - 3); 48 | ``` 49 | 50 | Этот код [упадет](https://rextester.com/GPVRKM58250) в отладочной сборке под msvc. `str.begin() + 4` — указатель на элемент за последним. И еще `+1` 51 | выводит за пределы строки. Это UB. И не важно, что дальше вычитание возвращает внутренний указатель обратно в границы строки. 52 | 53 | Не стоит выполнять сложные вычисления с указателями. Прибавлять к ним или вычитать лучше всегда конечный числовой результат. В данном конкретном примере рассчет отступа (4 + 1 - 3) нужно выполнить отдельно — расставить скобки или (еще лучше) безопаснее и понятнее, вынести в отдельную переменную. 54 | 55 | ----------- 56 | 57 | Помимо выхода за границы объектов, невалидные указатели могут появляться после отрабатывания некоторых функций. 58 | Наиболее яркий пример такого UB представил Nick Lewycky для [Undefined Behavior Consequences Contest](https://blog.regehr.org/archives/767). Немного переделанная и под C++ (чтобы в ней было только одно UB, а не два) версия выглядит так: 59 | 60 | ```C++ 61 | int* p = (int*)malloc(sizeof(int)); 62 | int* q = (int*)realloc(p, sizeof(int)); 63 | if (p == q) { 64 | new(p) int (1); 65 | new(q) int (2); 66 | std::cout << *p << *q << "\n"; // print 12 67 | } 68 | ``` 69 | 70 | Этот код, собранный clang, [выводит](https://godbolt.org/z/31av9f) результат, противоречащий здравому смыслу (если вы не знаете, что в коде UB!). И этот же пример демонстрирует, что указатели это не просто число-адрес. 71 | 72 | Указателем, переданным в сишную функцию `realloc`, при успешной реаллокации, пользоваться более нельзя. Его можно только перезаписать (а потом уже использовать). 73 | 74 | Данный пример, конечно, искусственный, но в него можно легко влететь, если, например, по какой-то причине писать свою версию вектора, используя `realloc`, и захотеть немного «соптимизировать». 75 | 76 | 77 | ```C++ 78 | template 79 | struct Vector { 80 | static_assert(std::is_trivially_copyable_v); 81 | 82 | size_t size() const { 83 | return end_ - data_; 84 | } 85 | 86 | private: 87 | T* data_; 88 | T* end_; 89 | size_t capacity_; 90 | 91 | void reallocate(size_t new_cap){ 92 | auto ndata = realloc(data_, new_cap * sizeof(T)); 93 | if (!ndata) { 94 | throw std::bad_alloc(); 95 | } 96 | capacity_ = new_cap; 97 | if (ndata != data_) { 98 | const auto old_size = size(); // !access to invalidated data_! 99 | data_ = ndata; 100 | end_ = data_ + old_size; 101 | } // else — "ok", noop 102 | } 103 | } 104 | ``` 105 | 106 | Этот код с неопределенным поведением. Скорее всего, оно никак не проявится сейчас, но не значит, что так будет и в будущем. 107 | Возможно, вызов `reallocate` заинлайнится в неподходящем месте и все пойдет вверх дном. 108 | 109 | Однако, начиная любые попытки реализовать свой собственный вектор (стандартный вполне может кого-то не устраивать тем, что он по умолчанию инициализирует память), надо иметь в виду следующий печальный факт: это [невозможно](https://stackoverflow.com/questions/60481204/dynamic-arrays-in-c-without-undefined-behavior) сделать без неопределенного поведения (формального или реального). Основная причина — арифметика указателей внутри сырой памяти. В сырой памяти формально нет C++ массивов, только внутри которых арифметика и определена. 110 | 111 | -------------------------------------------------------------------------------- /pointer_provenance/misaligned_reference.md: -------------------------------------------------------------------------------- 1 | # Невыровненные ссылки 2 | 3 | Программист форматировал байтики. Ведь это же самое любимое развлечение C++ программистов: писать снова и снова код для форматного вывода пользовательских структур. 4 | 5 | Байтики у программиста были упакованными, чтоб никакого лишнего выравнивания! И поля у него были упорядочены также, чтоб никакого лишнего выравнивания 6 | 7 | ```C++ 8 | #pragma pack(1) 9 | struct Record { 10 | long value; 11 | int data; 12 | char status; 13 | }; 14 | 15 | int main() { 16 | Record r { 42, 42, 42}; 17 | static_assert(sizeof(r) == sizeof(int) + sizeof(char) + sizeof(long)); 18 | std::cout << std::format("{} {} {}", r.data, r.status, r.value); // 42 -- '*' 19 | } 20 | ``` 21 | 22 | О проверял этот код с санитайзером, и санитайзер [говорил](https://godbolt.org/z/nxGn3K1Td) ему что все в порядке. 23 | 24 | ``` 25 | Program returned: 0 26 | 42 * 42 27 | ``` 28 | 29 | Ну раз все впорядке, то можно больше байтиков отформатировать! 30 | 31 | ```C++ 32 | int main() { 33 | Record records[] = { { 42, 42, 42}, { 42, 42, 42} }; 34 | static_assert(sizeof(records) ==2 * ( sizeof(int) + sizeof(char) + sizeof(long) )); 35 | for (const auto& r: records) { 36 | std::cout << std::format("{} {} {}", r.data, r.status, r.value); // 42 -- '*' 37 | } 38 | } 39 | ``` 40 | 41 | И что-то [взорвалось](https://godbolt.org/z/zj81GY8Ec) (под ARM бы уж точно): 42 | 43 | ```C++ 44 | Program returned: 0 45 | /app/example.cpp:16:48: runtime error: reference binding to misaligned address 0x7ffd1eda9f85 for type 'const int', which requires 4 byte alignment 46 | 0x7ffd1eda9f85: note: pointer points here 47 | 00 00 00 00 2a 00 00 00 2a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 b0 48 | ``` 49 | 50 | Да, нельзя читать невыровненную память. Это влечет неопределенное поведение. Мы это уже знаем. Нельзя разыменовывать невыровненный указатель. 51 | Но вот беда. В C++ же есть ссылки. И они тоже обязаны быть правильно выровненными. 52 | 53 | Мы точно видим одну ссылку: 54 | 55 | ```C++ 56 | for (const auto& r: records); 57 | ``` 58 | 59 | Но там же не тип `const int`! Ну да. Это `Record` и с ней все в порядке. `#pragma pack(1)` задает требование к выравниванию 1, так что тут никакой проблемы. 60 | 61 | Откуда же взялась ссылка на `const int`? 62 | 63 | А она у нас неявно взялась. Ведь неявное создание ссылок это ключевая особенность C++! 64 | ```C++ 65 | template< class... Args > 66 | std::string format( std::format_string fmt, Args&&... args ); // Вот они эти два коварных &&! 67 | ``` 68 | 69 | ```C++ 70 | std::cout << std::format("{} {} {}", r.data, r.status, r.value); // все три поля будут переданы по ссылке! 71 | ``` 72 | Да, "универсальная ссылка" это все еще ссылка. 73 | 74 | В упакованной структуре поля не выровнены. Ссылки на них брать нельзя. 75 | 76 | Но ведь же в первоначальном варианте с одной структурой работало без предупреждений... 77 | 78 | Ха! Нам просто повезло, что 79 | - Поля в структуре упорядочены чтоб и без `pragma pack` не было паддинга между ними 80 | - Стек обычно выровнен на `sizeof(void*)` чего достаточно для всех полей в структуре 81 | 82 | Мы можем добавить один лишний `char` на стек и все [изменится](https://godbolt.org/z/eb7WM5ddb) 83 | ```C++ 84 | int main() { 85 | char data[1]; 86 | Record r { 42, 42, 42}; 87 | memset(data, 0, 1); 88 | std::cout << std::format("{} {} {}", r.data, r.status, r.value); // 42 -- '*' 89 | } 90 | ``` 91 | ``` 92 | Program returned: 0 93 | /app/example.cpp:17:44: runtime error: reference binding to misaligned address 0x7ffe3b4e1f36 for type 'int', which requires 4 byte alignment 94 | 0x7ffe3b4e1f36: note: pointer points here 95 | 00 00 00 00 2a 00 00 00 2a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 96 | ``` 97 | 98 | 99 | Как же исправить это досадное недоразумение? 100 | 101 | Нужно сделать отдельно чтение из каждого поля во временную правильно выровненную переменную -- сделать копию. 102 | 103 | ```C++ 104 | int main() { 105 | Record records[] = { { 42, 42, 42}, { 42, 42, 42} }; 106 | for (const auto& r: records) { 107 | // В C++23 для этого есть замечательный auto() 108 | std::cout << std::format("{} {} {}", auto(r.data), auto(r.status), auto(r.value)); 109 | // В С++20 110 | auto data = r.data; auto status = r.status; auto value = r.value; 111 | std::cout << std::format("{} {} {}", data, status, r.value); 112 | // Или совершенно уродливо и не устойчиво к изменениям в типах 113 | std::cout << std::format("{} {} {}", static_cast(r.data), 114 | static_cast(r.status), 115 | static_cast(r.value>)); 116 | } 117 | } 118 | ``` 119 | 120 | ----- 121 | 122 | В чуть более безопасных языках взятие невыровненных ссылок на поля упакованных структур просто не компилируется 123 | 124 | В [Rust](https://godbolt.org/z/Po4bevG17) 125 | 126 | ```Rust 127 | #[repr(C, packed)] 128 | struct Record { 129 | value: i64, 130 | data: i32, 131 | status: i8, 132 | } 133 | 134 | fn main() { 135 | let r = Record { value: 42, data: 42, status: 42 }; 136 | // В Rust макросы -- одно из немногих мест, где ссылки могут появляться неявно для читающего код 137 | println!("{} {} {}", r.data, r.status, r.value); 138 | /* 139 | error[E0793]: reference to packed field is unaligned 140 | --> :10:26 141 | | 142 | 10 | println!("{} {} {}", r.data, r.status, r.value); 143 | = note: packed structs are only aligned by one byte, and many modern architectures penalize unaligned field accesses 144 | = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced) 145 | = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers) 146 | */ 147 | 148 | // Вот так правильно: 149 | println!("{} {} {}", {r.data}, {r.status}, {r.value}); 150 | } 151 | ``` 152 | 153 | 154 | -------------------------------------------------------------------------------- /runtime/array_overrun.md: -------------------------------------------------------------------------------- 1 | # Переполнение буфера 2 | 3 | Переполнение буфера и выход за границы массива — злобные ошибки и причины не только лишь простых падений программ, но дыр в безопасности, позволяющих получать доступ куда не следует или даже исполнять произвольный код. 4 | 5 | В стандартной библиотеке C, доставшейся C++ по наследству, великое множество дырявых функций, позволяющих добиться переполнения буфера, если программист не удосужился проверить все возможные и невозможные варианты. 6 | 7 | - `scanf("%s", buf)` — нет проверки размера буфера 8 | - `strcpy(dst, src)` — нет проверки размера буфера 9 | - `strcat(dst, src)` — нет проверки размера буфера 10 | - `gets(str)` — нет проверки размера буфера 11 | - `memcpy(dst, src, n)` — проверку размера `dst` нужно делать вручную. 12 | 13 | И еще многие другие, преимущественно работающие со строками, функции. 14 | 15 | Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими функциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов. 16 | 17 | Для большинства старых небезопасных сишных функций сейчас есть «безопасные» аналоги с размерами буферов. Часть из них не стандартизирована, часть стандартизирована. Все это породило огромное количество костылей с макроподстановками для работы со всем этим зоопарком. Но сейчас не об этом. 18 | 19 | --- 20 | 21 | 22 | Проверки размеров — дополнительная работа. Генерировать под них инструкции — замедлять программу. Тем более программист мог все проверить сам. Так что в C/С++ обращение за границы массива, хоть на запись, хоть на чтение — влечет неопределенное поведение. И дыры в безопасности могут зарастать различными спецэффектами. 23 | 24 | 25 | В большинстве случаев, если нарушение размеров происходит не всегда, попытка почитать за границами массива проявится либо получением мусорных результатов, либо простой и так всеми любимой ошибкой сегментации (SIGSEGV). 26 | 27 | Но иногда начинается веселье. 28 | 29 | ```C++ 30 | const int N = 10; 31 | int elements[N]; 32 | 33 | bool contains(int x) { 34 | for (int i = 0; i <= N; ++i) { 35 | if (x == elements[i]) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | int main() { 43 | for (int i = 0; i < N; ++i) { 44 | std::cin >> elements[i]; 45 | } 46 | return contains(5); 47 | } 48 | ``` 49 | 50 | Эта программа, собранная gcc c оптимизациями, всегда [«найдет»](https://godbolt.org/z/949Kxc) пятерку в массиве. Независимо от того какие числа будут введены. 51 | Причем никаких предупреждений ни clang, ни gcc не производят. 52 | 53 | Происходит такой спецэффект из следующих соображений: 54 | 1. Компиляторы вольны считать, что UB в программах не бывает 55 | 2. ```C++ 56 | for (int i = 0; i <= N; ++i) { 57 | if (x == elements[i]) { 58 | return true; 59 | } 60 | } 61 | ``` 62 | В этом цикле будет обращение за границы массива, а значит UB. 63 | 3. Но, так как UB не бывает, до `N+1` итерации дело дойти не должно 64 | 4. Значит, мы выйдем из цикла по `return true` 65 | 5. А значит вся функция `contains` — это один `return true`. Оптимизировано! 66 | 67 | 68 | Или вот конечный цикл [становится бесконечным](https://godbolt.org/z/hPc1cf): 69 | 70 | ```C++ 71 | const int N = 10; 72 | int main() { 73 | int decade[N]; 74 | for (int k = 0; k <= N; ++k) { 75 | printf("k is %d\n",k); 76 | decade[k] = -1; 77 | } 78 | } 79 | ``` 80 | 81 | И фокус здесь не менее хитрый: 82 | 1. `decade[k] = -1;` Обращение к элементу массива должно быть без UB. А значит `k < N` 83 | 2. Раз `k < N`, то условие продолжения цикла `k <= N` — всегда истинно. Проверять его не надо. Оптимизировано! 84 | 85 | 86 | В этих примерах, конечно, сразу же должен броситься в глаза `<=` в заголовках циклов. Но и с более привычным `<` тоже можно изобрести себе проблемы. Константа `N`, например, может быть не связана с размером массива. И все, приехали. 87 | 88 | --- 89 | 90 | В дружелюбных и безопасных языках вы получите ошибку во время выполнения. Панику или исключение. В C++ же все надо проверять, проверять и еще раз проверять самим: 91 | 92 | - Не использовать отдельно висящие константы при проверке размеров. Лучше `std::size()` или метод `size()` 93 | - Писать меньше сырых циклов со счетчиками. Предпочтительнее range-based-for или стандартные алгоритмы из `#include ` 94 | - Не использовать `operator[]`, когда не критична производительность. Безопаснее метод `at()` контейнера, проверяющий границы. 95 | 96 | 97 | ## Полезные ссылки 98 | 1. https://blog.rapid7.com/2019/02/19/stack-based-buffer-overflow-attacks-what-you-need-to-know/ 99 | 2. https://dhavalkapil.com/blogs/Buffer-Overflow-Exploit/ 100 | 101 | -------------------------------------------------------------------------------- /runtime/endless_loop.md: -------------------------------------------------------------------------------- 1 | # Бесконечные циклы и проблема останова 2 | 3 | Определить, завершается или не завершается программа на конкретном наборе данных — алгоритмически невозможно в общем случае. 4 | 5 | Но в стандартах C и C++ зачем-то сказано, что валидная программа должна либо гарантированно завершаться, либо гарантированно производить обозреваемые эффекты: запрашивать ввод-вывод, взаимодействовать с `volatile` переменными и подобное. А иначе поведение программы неопределенное. Так что «правильные» компиляторы C++ настолько суровы, что способны решать алгоритмически неразрешимые задачи. 6 | 7 | Если в программе есть бесконечный цикл, и компилятор решил, что этот цикл не имеет обозреваемых эффектов, то цикл не имеет смысла и может быть выброшен. 8 | 9 | Занятный пример — таким образом можно [«опровергнуть» великую теорему Ферма](https://godbolt.org/z/nE7oWf) 10 | ```C++ 11 | #include 12 | 13 | int fermat () { 14 | const int MAX = 1000; 15 | int a=1,b=1,c=1; 16 | while (1) { 17 | if ( (a*a*a) == (b*b*b) + (c*c*c) ) return 1; 18 | a++; 19 | if (a>MAX) { 20 | a=1; 21 | b++; 22 | } 23 | if (b>MAX) { 24 | b=1; 25 | c++; 26 | } 27 | if (c>MAX) { 28 | c=1; 29 | } 30 | } 31 | return 0; 32 | } 33 | 34 | int main () { 35 | if (fermat()) { 36 | std::cout << "Fermat's Last Theorem has been disproved.\n"; 37 | } else { 38 | std::cout << "Fermat's Last Theorem has not been disproved.\n"; 39 | } 40 | return 0; 41 | } 42 | ``` 43 | Компилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1` 44 | 45 | Если же попытаться узнать, что за тройку «нашла» программа — цикл вернется. 46 | 47 | В `constexpr` контексте — получим [ошибку компиляции](https://godbolt.org/z/98MYzd). 48 | 49 | Может показаться, что проблема в том, что условие продолжения цикла не зависит от его тела. 50 | Но и в [исправленной](https://godbolt.org/z/o1Gcqc) версии цикл исчезает 51 | ```C++ 52 | int fermat() { 53 | const int MAX = 1000; 54 | int a=1,b=1,c=1; 55 | while ((a*a*a) != ((b*b*b)+(c*c*c))) { 56 | a++; 57 | if (a>MAX) { 58 | a=1; 59 | b++; 60 | } 61 | if (b>MAX) { 62 | b=1; 63 | c++; 64 | } 65 | if (c>MAX) { 66 | c=1; 67 | } 68 | } 69 | return 1; 70 | } 71 | ``` 72 | 73 | Даже если в цикле будут операции I/O, он все равно [может исчезнуть](https://godbolt.org/z/P8YxeT), 74 | если компилятор увидит, что эти операции от цикла не зависят 75 | ```C++ 76 | int fermat () { 77 | const int MAX = 1000; 78 | int a=1,b=1,c=1; 79 | while (1) { 80 | if ( (a*a*a) == (b*b*b) + (c*c*c) ) { 81 | std::cout << "Found!\n"; 82 | return 1; 83 | } 84 | a++; 85 | if (a>MAX) { 86 | a=1; 87 | b++; 88 | } 89 | if (b>MAX) { 90 | b=1; 91 | c++; 92 | } 93 | if (c>MAX) { 94 | c=1; 95 | } 96 | } 97 | return 0; 98 | } 99 | ``` 100 | 101 | Так что предполагать, что программа в каких-то случаях должна зацикливаться, и строить под эти случаи тесты в C/C++ просто так нельзя. Отлаживаться принтами с наскоку тоже нельзя. 102 | И строить тесты, проверяющие, что программа не зацикливается, также может оказаться бесполезно. -------------------------------------------------------------------------------- /runtime/noexcept.md: -------------------------------------------------------------------------------- 1 | # Ложный `noexcept` 2 | 3 | Начиная с 11 стандарта, мы можем помечать функции и методы спецификатором `noexcept`, говоря тем самым компилятору, что эта функция или метод не бросает исключения. 4 | 5 | И вроде бы все хорошо: получив такую информацию, компилятор может не генерировать дополнительные инструкции для обработки раскрутки стека. Бинарники становятся меньше, а программы быстрее. 6 | 7 | Но проблема в том, что этот спецификатор не заставляет компиляторы проверять, 8 | что функция действительно не бросает исключений. 9 | 10 | Если мы пометим функцию как `noexcept`, а она возьмет да кинет исключение, 11 | произойдет что-то странное, заканчивающееся внезапным `std::terminate`. 12 | 13 | Так, например, неожиданно [перестанут работать](https://godbolt.org/z/E9c9Ya) `try-catch` блоки. 14 | 15 | ```C++ 16 | void may_throw(){ 17 | throw std::runtime_error("wrong noexcept"); 18 | } 19 | 20 | struct WrongNoexcept { 21 | WrongNoexcept() noexcept { 22 | may_throw(); 23 | } 24 | }; 25 | 26 | // Попытки обернуть в try-catch эту функцию или любой код, 27 | // использующий ее — бесполезны. 28 | void throw_smth() { 29 | if (rand() % 2 == 0) { 30 | throw std::runtime_error("throw"); 31 | } else { 32 | WrongNoexcept w; 33 | } 34 | } 35 | ``` 36 | 37 | Может быть очень сложно понять почему это произошло, если код разнесен по разным единицам трансляции. 38 | 39 | ## Условный `noexcept` 40 | 41 | В С++ любят экономить на ключевых словах. 42 | 43 | - `= 0` для объявления чисто виртуальных методов 44 | - новый `requires` имеет два значения, порождая странные конструкции `requires(requires(...))` 45 | - `auto` и для автовывода, и для переключения на trailing return type 46 | - `decltype`, у которого разный смысл при применении к переменной и к выражению 47 | - и, конечно, `noexcept` — точно также два значения как у `requires`. 48 | 49 | Есть спецификатор `noexcept(condition)`. И просто `noexcept` — синтаксический сахар 50 | для конструкции `noexcept(true)`. 51 | 52 | А есть предикат `noexcept(expr)`, проверяющий, что выражение `expr` не кидает исключений по самой своей природе (сложение чисел, например) или же 53 | помечено как `noexcept`. 54 | 55 | И вместе они порождают конструкцию для условного навешивания noexcept: 56 | ```C++ 57 | void fun() noexcept(noexcept(used_expr)) 58 | ``` 59 | 60 | ```C++ 61 | void may_throw(){ 62 | throw std::runtime_error("wrong noexcept"); 63 | } 64 | 65 | struct ConditionalNoexcept { 66 | ConditionalNoexcept() noexcept(noexcept(may_throw())) { 67 | may_throw(); 68 | } 69 | }; 70 | 71 | // теперь с этой функцией все хорошо 72 | void throw_smth() { 73 | if (rand() % 2 == 0) { 74 | throw std::runtime_error("throw"); 75 | } else { 76 | ConditionalNoexcept w; 77 | } 78 | } 79 | ``` 80 | 81 | Чтобы избежать проблем, нужно всегда и везде использовать условный `noexcept` с аккуратной проверкой каждой используемой функции, либо вовсе не использовать `noexcept`. Но во втором случае стоит помнить, 82 | что операции перемещения, а также `swap`, должны помечаться как `noexcept` (и быть действительно `noexcept`!) для эффективной работы со стандартными контейнерами. 83 | 84 | Не забывайте писать негативные тесты. Без них 85 | можно проморгать появление ложного `noexcept` и получить `std::terminate` на боевом стенде. 86 | 87 | Также обратите внимание на тонкий и неприятный нюанс: если вам ну очень сильно надо кидать исключения из деструктора, обязательно явно пишите в его объявлении `noexcept(false)`. По умолчанию все ваши функции и методы помечены неявно `noexcept(false)`, но для деструкторов в C++ сделано исключение. Они неявно помечены `noexcept(true)`. [Так что](https://godbolt.org/z/5jo95d): 88 | 89 | ```C++ 90 | struct SoBad { 91 | // invoke std::terminate 92 | ~SoBad() { 93 | throw std::runtime_error("so bad dctor"); 94 | } 95 | }; 96 | 97 | struct NotSoBad { 98 | // OK 99 | ~NotSoBad() noexcept(false) { 100 | throw std::runtime_error("not so bad dctor"); 101 | } 102 | }; 103 | ``` 104 | 105 | ## Полезные ссылки 106 | 1. https://en.cppreference.com/w/cpp/language/noexcept 107 | 2. https://en.cppreference.com/w/cpp/language/noexcept_spec 108 | 3. https://www.modernescpp.com/index.php/c-core-guidelines-the-noexcept-specifier-and-operator 109 | -------------------------------------------------------------------------------- /runtime/recursion.md: -------------------------------------------------------------------------------- 1 | # Рекурсия 2 | 3 | Многие алгоритмы очень красиво и компактно записываются в рекурсивной форме. 4 | Сортировки, обходы графов, строковые алгоритмы. 5 | 6 | Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке. 7 | Конечно, есть хвостовая рекурсия, которая естественным образом может быть оптимизирована в цикл. Но это не гарантировано стандартом. Да и не всегда рекурсия именно хвостовая. 8 | 9 | Stack overflow не совсем неопределенное поведение, но точно не то, чего хочется видеть на боевом стенде. Потому в серьезных приложениях предпочитают итеративные алгоритмы рекурсивным. Если, конечно, нет гарантии, что глубина рекурсии мала. 10 | 11 | В деле искоренения рекурсии из своей программы нужно быть очень внимательным. И не только в корректной имплементации алгоритмов. 12 | Помимо алгоритмов, рекурсивными могут быть и структуры данных. И тут в игру вступает RAII, правила нуля, порядок вызовов деструкторов и `noexcept`. 13 | 14 | 15 | ```C++ 16 | struct Node { 17 | int value = 0; 18 | std::vector children; 19 | }; 20 | ``` 21 | 22 | Такая структура совершенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями. 23 | 24 | Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся «правилом нуля» и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего — красота! 25 | 26 | Однако, деструктор, сгенерированный компилятором, будет рекурсивным! И при слишком большой глубине дерева мы получим переполнение стека. 27 | 28 | Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева... А очередь это аллокация памяти. А аллокация памяти — операция, бросающая исключения. И вот у нас деструктор будет бросать исключения. Что совсем не хорошо. 29 | 30 | Можно написать деструктор без аллокаций и рекурсии. Но его алгоритмическая сложность будет квадратичной: 31 | 32 | 1. Находим вершину, у которой последний элемент в векторе потомков является листом. 33 | 2. Удаляем этот элемент из вектора. 34 | 3. Повторяем, пока дерево не закончится 35 | 36 | 37 | Для обычного связанного списка проблема также сохраняется 38 | ```C++ 39 | struct List { 40 | int value = 0; 41 | std::unique_ptr next; 42 | }; 43 | ``` 44 | Но в этом случае рекурсия является хвостовой и можно надеяться, что оптимизатор справится. Но вы же гоняете тесты и на дебажных сборках, верно? 45 | 46 | Так что пишем деструктор, а вместе с ним все остальные специальные методы (в данном случае только перемещающие операции) 47 | 48 | ```C++ 49 | struct List { 50 | int value = 0; 51 | std::unique_ptr next; 52 | 53 | ~List() { 54 | while (next) { 55 | // деструктор все также рекурсивен, 56 | // но теперь глубина рекурсии — 1 вызов 57 | next = std::move(next->next); 58 | } 59 | } 60 | 61 | List() noexcept = default; 62 | List(List&&) noexcept = default; 63 | List& operator=(List&&) noexcept = default; 64 | }; 65 | ``` 66 | 67 | --------- 68 | 69 | С рекурсивными структурами данных в C++ нужно быть очень аккуратными. Не просто так в 70 | Rust написать их «очевидным» способом тяжело. 71 | -------------------------------------------------------------------------------- /runtime/reserved_names.md: -------------------------------------------------------------------------------- 1 | # Зарезервированные имена 2 | 3 | Эта тема тесно связана с [ODR violation](odr_violation.md). 4 | 5 | В C/C++ невероятно много идентификаторов, использовать которые для своих переменных и типов запрещено под страхом неопределенного поведения. 6 | 7 | Некоторые имена запрещены самим стандартом C/C++. Некоторые — стандартами POSIX. Некоторые — платформоспецифическими библиотеками. В последнем случая обычно вам ничего не грозит, пока библиотека не подключена. 8 | 9 | Так в глобальной области видимости нельзя использовать имена функций из библиотеки C. Ни в C, ни в C++! 10 | Иначе вы можете столкнуться не только с ODR-violation, но еще и с удивительным поведением компиляторов, умеющих оптимизировать распространенные конструкции. 11 | 12 | Так, если определить свой собственный `memset`: 13 | 14 | ```C 15 | void *memset (void *destination, int c, unsigned long n) { 16 | for (unsigned long i = 0; i < n; ++i) { 17 | ((char*)(destination))[i] = c; 18 | } 19 | return destination; 20 | } 21 | ``` 22 | Заботливый оптимизирующий компилятор может запросто [превратить](https://godbolt.org/z/fKfeqza6a) его в 23 | ```C 24 | void *memset (void* destination, int c, unsigned long n) { 25 | return memset(destination, c, n); 26 | } 27 | ``` 28 | 29 | В C++, благодаря включенному по умолчанию декорированию имен, рекурсии не будет — вызовется стандартный `memset`, вместо нашего. 30 | 31 | Однако, декорирование не спасает, если объявлять не функции, а глобальные переменные. 32 | 33 | ```C++ 34 | #include 35 | int read; 36 | int main(){ 37 | std::ios_base::sync_with_stdio(false); 38 | std::cin >> read; 39 | } 40 | ``` 41 | При сборке такого примера со статически влинкованной стандартной библиотекой C, программа [упадет](https://godbolt.org/z/sq9bqhn46). 42 | Так как вместо адреса стандартной функции `read` будет подставлен адрес глобальной переменной `read`. Аналогичный пример с использованием имени `write` предлагается читателю воплотить самостоятельно в качестве упражнения. 43 | 44 | Запретных имен много. Например, все, что начинается с `is*`, `to*` или `_*` запрещено в глобальном пространстве. `_[A-Z]*` запрещены вообще везде. POSIX резервирует имена, заканчивающиеся на `_t`. И еще много всего неожиданного. 45 | С более полными списками можно ознакомиться по ссылкам. 46 | 47 | Если вы пользуетесь запрещенными именами, то сегодня может всё работать, но не завтра. 48 | 49 | Чтобы не жить в страхе во многих случаях достаточно использовать `static` или анонимные пространства имен. Или просто не использовать C/C++. 50 | 51 | 52 | ## Полезные ссылки 53 | 1. https://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html 54 | 2. https://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier 55 | 3. https://wiki.sei.cmu.edu/confluence/display/cplusplus/DCL51-CPP.+Do+not+declare+or+define+a+reserved+identifier -------------------------------------------------------------------------------- /runtime/rvo_vs_raii.md: -------------------------------------------------------------------------------- 1 | # (N)RVO vs RAII 2 | 3 | C++ восхитительный язык. В нем столько идиом, концепций, и каждая со своей замечательной, иногда невыговариваемой, аббревиатурой! А самое замечательное в них то, что они иногда конфликтуют. И от их конфликта страдать придется разработчику. А иногда они вступают в симбиоз и страдать приходится еще больше. 4 | 5 | В C++ есть конструкторы, деструкторы и приходящая с ними концепция RAII: 6 | Захватывай и инициализируй ресурс в конструкторе, очищай и отпускай в деструкторе. И будет тебе счастье. 7 | 8 | Ну что ж, давайте попробуем! 9 | 10 | Сделаем какой-нибудь простенький класс, выполняющую буферизированную запись: 11 | 12 | ```C++ 13 | struct Writer { 14 | public: 15 | static const size_t BufferLimit = 10; 16 | 17 | // захватываем устройство, в которое будет писать 18 | Writer(std::string& dev) : device_(dev) { 19 | buffer_.reserve(BufferLimit); 20 | } 21 | // в деструкторе отпускаем, записывая все, что набуферизировали 22 | ~Writer() { 23 | Flush(); 24 | } 25 | 26 | void Dump(int x) { 27 | if (buffer_.size() == BufferLimit){ 28 | Flush(); 29 | } 30 | buffer_.push_back(x); 31 | } 32 | private: 33 | void Flush() { 34 | for (auto x : buffer_) { 35 | device_.append(std::to_string(x)); 36 | } 37 | buffer_.clear(); 38 | } 39 | 40 | std::string& device_; 41 | std::vector buffer_; 42 | }; 43 | ``` 44 | 45 | И попробуем им красиво воспользоваться: 46 | 47 | ```C++ 48 | const auto text = []{ 49 | std::string out; 50 | Writer writer(out); 51 | writer.Dump(1); 52 | writer.Dump(2); 53 | writer.Dump(3); 54 | return out; 55 | }(); 56 | std::cout << text; 57 | ``` 58 | 59 | [Работает!](https://godbolt.org/z/szhvbM). Печатает `123`. Все как мы и ожидали. Как похорошел язык! 60 | 61 | Ага. Только работает оно исключительно потому что нам повезло. Тут, начиная с C++17, гарантированные NRVO (_named return value optimization_) и copy elision. А программа написана вообще-то с очень злобной ошибкой. И если мы возьмем, например, MSVC, который последним стандартам частенько забывает полностью соответствовать. То результат внезапно будет [иной](https://rextester.com/OKK46123). 62 | 63 | Если мы чуть-чуть модифицируем программу: 64 | ```C++ 65 | int x = 0; std::cin >> x; 66 | 67 | const auto text = [x]{ 68 | if (x < 1000) { 69 | std::string out; 70 | Writer writer(out); 71 | writer.Dump(1); 72 | writer.Dump(2); 73 | writer.Dump(3); 74 | return out; 75 | } else { 76 | return std::string("hello\n"); 77 | } 78 | }(); 79 | std::cout << text; 80 | ``` 81 | то под clang все еще работает, а под gcc — [нет](https://godbolt.org/z/5GWba8) 82 | 83 | И самое замечательное во всем этом безобразии, что никакое это не неопределенное поведение! 84 | 85 | Помните, мы обсуждали [не работающее перемещение](../syntax/move.md)? И выясняли, что в C++ нет деструктивного перемещения. А оно все-таки есть. Иногда. Когда срабатывает оптимизация возвращаемого значения и удаление лишних вызовов конструкторов копий/перемещений. 86 | 87 | Программы выше все неправильные. Они предполагают, что деструктор `Writer` будет вызван до возврата значения из функции. Чего никак быть не может. Деструкторы объектов вызываются всегда после возврата из функции. Иначе эти самые значения бы просто умирали, и вызывающий код всегда получал мертвый объект. 88 | 89 | Но как же тогда оно иногда работает и скрывает такую печальную ошибку? 90 | А вот как: 91 | 92 | ```C++ 93 | const auto text = []{ 94 | std::string out; 95 | Writer writer(out); // (2) адреса out и text одинаковые. 96 | // по сути это один и тот же объект 97 | writer.Dump(1); 98 | writer.Dump(2); 99 | writer.Dump(3); 100 | return out; // (1) это единственная точка возврата из функции 101 | // NRVO позволяет в качестве адреса временной 102 | // переменной out подложить адрес переменной, 103 | // в которую мы запишем результат — text 104 | }(); // (3) деструктор Writer пишет напрямую в text 105 | ``` 106 | 107 | Без всех хитроумных оптимизаций же происходит следующее: 108 | ```C++ 109 | const auto text = []{ 110 | std::string out; // (0) строка пуста 111 | Writer writer(out); // (1) адреса out и text разные. Это разные объекты 112 | writer.Dump(1); 113 | writer.Dump(2); 114 | writer.Dump(3); // (2) записи не происходило — буфер не заполнился 115 | return out; // (3) возвращаем копию out — пустую строку 116 | }(); // (3) деструктор Writer пишет в out, она умирает и не достается никому 117 | // text пуст 118 | ``` 119 | 120 | Никакого неопределенного поведения тут, повторяю, нет. Просто всякий деструктор/конструктор с побочными эффектами как бы «сломан» из-за разрешенных и описанных в стандарте (и даже иногда гарантированных) оптимизаций. 121 | 122 | Ну а в каком-нибудь Rust нам такую ерунду написать [просто не дадут](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c5c9b4edbf891d469214eae29a3ca1af). Такие дела. 123 | 124 | Исправляется проблема либо вытаскиванием `Flush` наружу и явным вызовом его. Либо добавлением еще одной вложенной области видимости: 125 | ```C++ 126 | const auto text = []{ 127 | std::string out; 128 | { 129 | Writer writer(out); 130 | writer.Dump(1); 131 | writer.Dump(2); 132 | writer.Dump(3); 133 | } // деструктор Writer вызывается здесь 134 | return out; 135 | }(); 136 | std::cout << text; 137 | ``` 138 | Не забудьте только оставить комментарий, чтобы ваши коллеги случайно не удалили такие «лишние» скобочки. И проверьте, что ваш автоформаттер кода также их не удаляет. 139 | 140 | ## Полезные ссылки: 141 | 1. https://en.cppreference.com/w/cpp/language/copy_elision 142 | 2. http://eel.is/c++draft/class.copy.elision -------------------------------------------------------------------------------- /runtime/trivial_types_and_ABI.md: -------------------------------------------------------------------------------- 1 | # Тривиальные типы и ABI 2 | 3 | Допустим, вы написали прекрасную библиотеку для работы с двумерными векторами. 4 | И там, конечно, же была структура `Point`. Вот такая: 5 | 6 | ```C++ 7 | template 8 | struct Point { 9 | ~Point() {} 10 | 11 | T x; 12 | T y; 13 | }; 14 | ``` 15 | 16 | И была функция 17 | 18 | ```C++ 19 | Point zero_point(); 20 | ``` 21 | 22 | имплементацию которой вы, как приличный разработчик, заботящийся о времени компиляции и размерах заголовочных файлов, поместили в компилируемый .cpp файл, а пользователю оставили только объявление. 23 | Ваша библиотека была такой хорошей, что быстро обрела популярность и от нее стали зависеть многие другие приложения. 24 | 25 | И все было хорошо. Но однажды вы заметили, что деструктор `Point` вам совершенно не нужен. Пусть его генерит компилятор самостоятельно. И вы его удалили. 26 | 27 | ```C++ 28 | template 29 | struct Point { 30 | T x; 31 | T y; 32 | }; 33 | ``` 34 | 35 | Пересобрали библиотеку и разослали пользователям новые готовые .dll/.so файлы. С новыми заголовками, конечно же. 36 | Но изменение такое незначительное, что пользователи просто подложили готовые бинари себе без перекомпиляции... И все упало со страшным memory corruption. 37 | 38 | Почему? 39 | 40 | Это изменение сломало ABI. 41 | 42 | В С++ все типы делятся на тривиальные и нетривиальный. Тривиальные, в свою очередь, бывают еще и в разных аспектах тривиальными. В общем случае тривиальность позволяет не генерировать дополнительный код, чтобы что-то сделать. 43 | 44 | - `trivially_constructible` — не надо ничего инициализировать 45 | - `trivially_destructible` — не нужно генерировать код для деструктора 46 | - `trivially_copyable` — не нужно ничего, кроме простого копирования байтов 47 | - `trivially_movable` — аналогично копированию, но при «перемещении» 48 | 49 | С объектами тривиальных типов выполнимы дополнительные оптимизации. Например, их можно передавать через регистры. Компилятор способен догадаться соптимизировать memcpy (использованный чтобы избежать неопределенного поведения) в `reinterpret_cast`. И другие подобные вещи. 50 | 51 | Вот, например: 52 | 53 | ```C++ 54 | struct TCopyable { 55 | int x; 56 | int y; 57 | }; 58 | static_assert(std::is_trivially_copyable_v); 59 | 60 | struct TNCopyable { 61 | int x; 62 | int y; 63 | 64 | TNCopyable(const TNCopyable& other) : x{other.x}, y{other.y} {} 65 | 66 | // вынуждены написать конструктор, так как aggregate initialization 67 | // отключился из-за конструктора копирования 68 | TNCopyable(int x, int y) : x{x}, y{y} {} 69 | }; 70 | 71 | static_assert(!std::is_trivially_copyable_v); 72 | 73 | // Здесь будет возврат через регистр rax. TCopyable в него как раз помещается 74 | extern TCopyable test_tcopy(const TCopyable& c) { 75 | return {c.x *5, c.y * 6}; 76 | } 77 | 78 | // Здесь возврат через указатель, передаваемый через регистр rdi 79 | extern TNCopyable test_tnocopy(const TNCopyable& c) { 80 | return {c.x *5, c.y * 6}; 81 | } 82 | ``` 83 | 84 | По ассемблерному листингу можно убедиться, что две «одинаковые» функции, возвращающие «одинаково» представленные в памяти структуры, делают это [по-разному](https://godbolt.org/z/Mz8srfdsc) 85 | 86 | ```asm 87 | test_tcopy(TCopyable const&): # @test_tcopy(TCopyable const&) 88 | mov eax, dword ptr [rdi] 89 | mov ecx, dword ptr [rdi + 4] 90 | lea eax, [rax + 4*rax] 91 | add ecx, ecx 92 | lea ecx, [rcx + 2*rcx] 93 | shl rcx, 32 94 | or rax, rcx #! 95 | ret 96 | test_tnocopy(TNCopyable const&): # @test_tnocopy(TNCopyable const&) 97 | mov rax, rdi 98 | mov ecx, dword ptr [rsi] 99 | mov edx, dword ptr [rsi + 4] 100 | lea ecx, [rcx + 4*rcx] 101 | add edx, edx 102 | lea edx, [rdx + 2*rdx] 103 | mov dword ptr [rdi], ecx #! 104 | mov dword ptr [rdi + 4], edx #! 105 | ret 106 | ``` 107 | 108 | [Аналогично](https://godbolt.org/z/KK1o5E168) и с вашей 2D точкой: 109 | 110 | ```C++ 111 | struct TPoint { 112 | float x; 113 | float y; 114 | }; 115 | static_assert(std::is_trivially_destructible_v); 116 | 117 | struct TNPoint { 118 | float x; 119 | float y; 120 | ~TNPoint() {} // user-provided деструктор делает тип нетривиальным 121 | // даже если деструктор ничего не делает 122 | }; 123 | 124 | static_assert(!std::is_trivially_destructible_v); 125 | 126 | // Возврат через регистр 127 | extern TPoint zero_point() { 128 | return {0,0}; 129 | } 130 | 131 | // Возврат через указатель 132 | extern TNPoint zero_npoint() { 133 | return {0,0}; 134 | } 135 | ``` 136 | 137 | ```asm 138 | zero_point(): # @zero_point() 139 | xorps xmm0, xmm0 140 | ret 141 | zero_npoint(): # @zero_npoint() 142 | mov rax, rdi 143 | mov qword ptr [rdi], 0 144 | ret 145 | ``` 146 | 147 | Особенно болезненно все становится с шаблонами, поскольку тривиальность шаблонной структуры может зависеть от тривиальности параметров. 148 | 149 | В реализациях стандартной библиотеки C++03, например, шаблона `pair` можно [увидеть](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_pair.h#L624) user-provided, ничего дополнительно не делающие, конструкторы копий. В C++11 и выше их уже [нет](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_pair.h#L210). Так что это еще одна точка бинарной несовместимости библиотек на старом C++ с библиотеками нового C++. 150 | 151 | Проблемы со сломом ABI вокруг тривиальных типов могут застать вас врасплох не только в самом C++, но и в любом другом языке, если вы попытаетесь через него общаться с плюсовыми библиотеками. Например, в утилите для генерации биндингов для Rust есть [баг](https://github.com/rust-lang/rust-bindgen/issues/778). 152 | 153 | Будьте внимательны с тривиальными типами. И если вы намерены предоставлять стабильный ABI своей библиотеки, то выстраивайте его вокруг чистых сишных структур и функций, а не вокруг зоопарка из мира C++. 154 | 155 | Также помните, что тривиальность 156 | 1. Легко ломается (достаточно [добавить](https://godbolt.org/z/b8T7T3Tbj) инициализатор, и тип уже не trivially_constructible) 157 | 2. Позволяет попадаться на [неинициализированные](https://godbolt.org/z/fW7sE9v37) переменные 158 | 3. Не всегда влияет на ABI (в основном влияют деструкторы и конструкторы копирования/перемещения) 159 | 160 | ## Полезные ссылки 161 | 1. https://en.cppreference.com/w/cpp/types/is_trivial 162 | 2. https://www.youtube.com/watch?v=ZxWjii99yao 163 | -------------------------------------------------------------------------------- /runtime/virtual_functions.md: -------------------------------------------------------------------------------- 1 | # Невиртуальные виртуальные функции 2 | 3 | Вы разрабатываете иерархию классов и хотите описать интерфейс для вычислителя, который можно запускать и останавливать. 4 | Скорее всего в первой итерации он будет выглядеть так 5 | 6 | ```C++ 7 | class Processor { 8 | public: 9 | virtual ~Processor() = default; 10 | virtual void start() = 0; 11 | // stops execution, returns `false` if already stopped 12 | virtual bool stop() = 0; 13 | }; 14 | ``` 15 | 16 | Пользователи интерфейса нареализовывали свойх имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада! 17 | 18 | Вы были уставши и злы. А быть может это были и не вы, а какой-то менее опытный коллега, которому поручили доработать интерфейс. В общем, на свет родилась правка 19 | 20 | ```C++ 21 | class Processor { 22 | public: 23 | virtual void start() = 0; 24 | // stops execution, returns `false` if already stopped 25 | virtual bool stop() = 0; 26 | virtual ~Processor() { 27 | stop(); 28 | } 29 | }; 30 | ``` 31 | 32 | Логично? — Да! 33 | 34 | Правильно? — Нет! 35 | 36 | Если вам повезет, то достаточно умный компилятор [сможет](https://godbolt.org/z/PGeob9bn1) сообщить о проблеме. 37 | В конструкторах и деструкторах в C++ виртуальная диспетчеризация методов не работает (В других языках — например, в C# или Java — наоборот, что доставляет свои проблемы). 38 | 39 | Почему так? При конструировании часть объекта-наследника, используемая в переопределенном методе, может быть еще не создана: конструкторы вызываются в порядке от базового класса к производному. 40 | При деструктурировании наоборот — часть объекта-наследника уже уничтожена, и если позволить динамический вызов, можно легко получить use-after-free. 41 | 42 | Радуйтесь! Это одно из немногих мест в C++, где вас защитили от неопределенного поведения со временами жизни! 43 | 44 | Хорошо. А если так? 45 | 46 | ```C++ 47 | // processor.hpp 48 | class Processor { 49 | public: 50 | void start(); 51 | // stops execution, returns `false` if already stopped 52 | bool stop(); 53 | 54 | virtual ~Processor(); 55 | 56 | protected: 57 | virtual bool stop_impl() = 0; 58 | virtual void start_impl() = 0; 59 | }; 60 | 61 | 62 | // processor.cpp 63 | Processor::~Processor() { 64 | stop(); 65 | } 66 | 67 | bool Processor::stop() { 68 | return stop_impl(); 69 | } 70 | void Processor::start() { 71 | start_impl(); 72 | } 73 | ``` 74 | 75 | Компиляторы уже [не выдают](https://godbolt.org/z/66fG6cjnd) замечательного предупреждения и подвох заметить стало сложнее. А ведь мы повысили уровень индирекции всего на один! А что будет если код нашего базового класса окажется сложнее?.. Наследование имплементаций — источник многих проблем, прячущихся за невинным желанием переиспользовать код. 76 | 77 | Вызов виртуальных функций класса в его конструкторах и деструкторах почти всегда является ошибкой сейчас или в будущем. 78 | Если же это не ошибка и так и задумывалось, то стоит использовать явный статический вызов с указанием имени класса (name qualified call). 79 | 80 | ```C++ 81 | // processor.cpp 82 | Processor::~Processor() { 83 | Processor::stop(); 84 | } 85 | ``` 86 | 87 | Также стоит отметить, что в C++ у pure virtual методов может быть имплементация, к которой можно обращаться. 88 | Иногда это даже полезно. Таким образом можно потребовать от пользователя обязательно явно принять решение: изменять поведение метода или использовать поведение по умолчанию. 89 | 90 | ```C++ 91 | class Processor { 92 | public: 93 | virtual void start() = 0; 94 | // stops execution, returns `false` if already stopped 95 | virtual bool stop() = 0; 96 | 97 | virtual ~Processor() = default; 98 | }; 99 | 100 | void Processor::start() { 101 | std::cout << "unsupported"; 102 | } 103 | 104 | class MyProcessor : public Processor { 105 | public: 106 | void start() override { 107 | // call default implementation 108 | Processor::start(); 109 | } 110 | }; 111 | ``` 112 | 113 | Вернемся опять к нашей остановке при вызове деструктора. Как же с ней быть? 114 | 115 | Есть два пути. 116 | 117 | Путь первый: потребовать, чтоб реализующий интерфейс обязательно предоставил свою версию деструктора, которая выполнит корректную остановку. 118 | 119 | Насильно, с проверкой на этапе компиляции, к этому никого, к сожалению никого не принудишь. Можно [попытаться](https://godbolt.org/z/cWrd7P89r) выразить намерение объявлением деструктора интерфейса чисто виртуальным, но это не поможет, поскольку деструктор, если не указан, всегда генерируется 120 | 121 | ```C++ 122 | class Processor { 123 | public: 124 | virtual void start() = 0; 125 | // stops execution, returns `false` if already stopped 126 | virtual bool stop() = 0; 127 | virtual ~Processor() = 0; 128 | }; 129 | 130 | // required! 131 | Processor::~Processor() = default; 132 | 133 | class MyProcessor : public Processor { 134 | public: 135 | void start() override { 136 | } 137 | bool stop() override { return false; } 138 | // ~MyProcessor() override = default; missing destructor does not trigger CE 139 | }; 140 | 141 | 142 | int main() { 143 | MyProcessor p; 144 | } 145 | ``` 146 | 147 | Путь второй — добавить еще один слой. И пользоваться им во всех публичных API 148 | 149 | ```C++ 150 | class GuardedProcessor { 151 | std::unique_ptr proc; 152 | // ... 153 | ~GuardedProcessor() { 154 | assert(proc != nullptr); 155 | proc->stop(); 156 | } 157 | }; 158 | ``` 159 | 160 | 161 | -------------------------------------------------------------------------------- /runtime/vla.md: -------------------------------------------------------------------------------- 1 | # Varialbe Length Arrays 2 | 3 | Память в C/C++ под наши объекты, как известно, можно выделять на стеке, а можно в куче. 4 | На стеке она обычнно выделяется автоматически и нам об этом сильно беспокоиться не надо. 5 | 6 | ```C 7 | int32_t foo(int32_t x, int32_t y) { 8 | int32_t z = x + y; 9 | return z; 10 | } 11 | ``` 12 | При вызове функции `foo` на стеке, после аргументов (хотя не факт что аргументы будут переданы через стек), будет выделено (просто вершина стека будет сдвинута) еще 4 байта (а может быть и больше, кто ж знает, что там настроено у компилятора!) под переменную z. А может быть и не будет выделено (например, если компилятор оптимизирует переменную и сложит результат сразу в регистр `rax`). Дикая природа удивительна, неправда ли? 13 | 14 | Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете `attribute ( ( naked ) )`. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобожается автоматически. Потому, как известно, вот такой код порождает висячий указатель 15 | 16 | ```C 17 | int32_t* foo(int32_t x, int32_t y) { 18 | int32_t z = x + y; 19 | return &z; 20 | } 21 | ``` 22 | 23 | Обычно для выделения чего-то на стеке размер этого чего-то должен быть заранее известен на этапе компиляции. Обычно, но не всегда... 24 | 25 | ---- 26 | Однажды один большой и сложный HTTP сервер внезапно упал. Упал он, как ни странно, с моим любимым сообщением segmentation fault (core dumped). К этому все впрочем уже привыкли, ведь HTTP сервер был написан на чистом и прекрасном C. Так что падение -- это что-то само собой разумеюшееся. 27 | 28 | Содержимое core файла было загадочным: строчка, на которую указывал dump, не делала ничего страшного. Она не разменовывала указатель, не писала в массив, не читала из массива, не освобождала память, не выделяля память... Ничего. Она просто пыталась вызвать функцию и передать в нее параметры. Но что-то пошло не так. 29 | 30 | У нее закончился стэк. 31 | 32 | Но как же так?! В core дампе было всего от силы 40 стэк фреймов! Как он мог закончится? Там же 10 мегабайт под Linux! 33 | 34 | Путешествуя по этому стэк трейсу, я поднялся на пять стэк фреймов выше. Все они были довольно небольшого размера. 40 байт, 180, килобайт... А вот шестой фрейм оказался невероятно большим! 8 мегабайт! 35 | 36 | Открывши соответствующий исходник, я обнаружил: 37 | 38 | ```C 39 | int encoded_len = request->content_len * 4 / 3 + 1; 40 | char encoded_buffer[encoded_len]; 41 | encoded_len = encode_base64(request->content, content_len, encoded_buffer, encoded_len); 42 | process(encoded_buffer, enconded_len); 43 | ``` 44 | 45 | Знакомьтесь, variable length array (VLA)! Прекрасная фича языка C. Существует в языке C++ как нестандартное расширение (MSVC не поддердживает, в GCC и Clang компилируется). 46 | 47 | Это массив переменной длины **на стеке**. Причина падения была [ясна](https://godbolt.org/z/Ps9deqnxj). 48 | 49 | ----- 50 | 51 | VLA -- концептуально, фича довольно полезная. Но крайне небезопасная. 52 | Вам нужен буффер, но его длину вы узнаете только в runtime? Пожалуйста, VLA! Не нужен никакой `malloc`/`new` -- просто объяви массив и укажи длину! К тому же это в среднем намного быстрее чем `malloc` и не утечет, автоматически освободится! Ну и конечно же, 53 | что может быть лучше чем получить segfault вместо out-of-memory? 54 | 55 | Лучше VLA может быть только прямое использование функции `alloca()`. Ведь в отличие от VLA, у нее намного больше вариативности по отрыванию ног 56 | 57 | ```C 58 | void fill(char* ptr, int n) { 59 | for (int i =0;i 90 | 91 | template 92 | void test_array(int (&arr)[N]) { 93 | std::cout << sizeof(arr) << "\n"; 94 | } 95 | 96 | int main(int argc, char* argv[]) { 97 | int fixed[15]; 98 | int vla[argc]; 99 | test_array(fixed); 100 | test_array(vla); // compilation error 101 | } 102 | ``` 103 | 104 | 105 | ## Полезные ссылки 106 | 1. https://lwn.net/Articles/749064/ 107 | 2. https://man7.org/linux/man-pages/man3/alloca.3.html 108 | 3. https://nullprogram.com/blog/2019/10/27/ 109 | 4. https://en.cppreference.com/w/c/language/array -------------------------------------------------------------------------------- /standard_lib/forward.md: -------------------------------------------------------------------------------- 1 | # Как неправильно использовать std::forward 2 | 3 | Однажды я увидел код, который демонстрирует красоту и удобство concepts в C++20/23 и мощь шаблонов. Код их действительно демонстрировал. Давайте я его покажу. 4 | 5 | ```C++ 6 | template 7 | struct Matrix { 8 | F generator; 9 | auto operator[](uint32_t r, uint32_t c) { 10 | return generator(r, c); 11 | } 12 | }; 13 | 14 | // Make matrix via 2 args generator function 15 | template 16 | auto MakeMatrix(std::invocable auto&& fn) { 17 | using F = std::remove_cvref_t; 18 | return Matrix(std::forward(fn)); 19 | } 20 | ``` 21 | 22 | Красиво, неправда ли? Давайте его протестируем 23 | 24 | ```C++ 25 | int main() { 26 | std::function generator = [](auto...) { 27 | return 42; 28 | }; 29 | auto m1 = MakeMatrix<3, 3>(generator); 30 | std::cout << "m1[1,1]=" << m1[1,1] << std::endl; 31 | auto m2 = MakeMatrix<2, 2>(generator); 32 | std::cout << "m2[1,1]=" << m2[1,1] << std::endl; 33 | } 34 | ``` 35 | 36 | Вы могли бы подумать, что обе операции вывода успешно напечатают число 42... 37 | Но произойдет кое-что [неожиданное](https://godbolt.org/z/6TKEP1caE)! 38 | 39 | ``` 40 | Program returned: 139 41 | terminate called after throwing an instance of 'std::bad_function_call' 42 | what(): bad_function_call 43 | Program terminated with signal: SIGSEGV 44 | m1[1,1]=42 45 | ``` 46 | 47 | ``` 48 | ==1==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x7a3c0ee28898 bp 0x7a3c0f01be90 sp 0x7ffe7360aa00 T0) 49 | ==1==The signal is caused by a READ memory access. 50 | ==1==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used. 51 | #0 0x7a3c0ee28898 in abort (/lib/x86_64-linux-gnu/libc.so.6+0x28898) (BuildId: 490fef8403240c91833978d494d39e537409b92e) 52 | #1 0x7a3c0f3abbfc (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xadbfc) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) 53 | #2 0x7a3c0f3bd169 (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xbf169) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) 54 | #3 0x7a3c0f3ab7a8 in std::terminate() (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xad7a8) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) 55 | #4 0x7a3c0f3bd3e6 in __cxa_throw (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xbf3e6) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) 56 | #5 0x7a3c0f3ae6c3 in std::__throw_bad_function_call() (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xb06c3) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) 57 | #6 0x401256 in std::function::operator()(unsigned int, unsigned int) const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/std_function.h:590 58 | #7 0x401256 in Matrix<2u, 2u, std::function >::operator[](unsigned int, unsigned int) /app/example.cpp:12 59 | #8 0x401256 in main /app/example.cpp:31 60 | ``` 61 | 62 | 63 | Все совершенно определенно и соответствует спецификации. `operator()` у `std::function` бросает исключение, если объект-функция оказался пустым... Да, если вы не знали, `std::function` это неявно nullable тип. 64 | Но какого черта объект оказался пустым?! 65 | 66 | Посмотрим еще раз на эти две прекрасные строчки 67 | 68 | ```C++ 69 | auto MakeMatrix(std::invocable auto&& fn) { 70 | using F = std::remove_cvref_t; 71 | return Matrix(std::forward(fn)); 72 | } 73 | ``` 74 | Шаблон принимает на вход так называемую «универсальную» ссылку. И использует `std::forward` чтобы передать ее дальше «универсальным» способом: передай rvalue как rvalue, lvalue как lvalue. 75 | Вот только программист решил для красоты и читаемости сэкономить на буквах и использовал его неправильно. 76 | 77 | Тип-параметр у шаблона `std::forward` — обязателен и его нужно указать правильно. Им должен быть тип ссылки, который мы хотим сохранить и прокинуть далее. 78 | 79 | Но разработчик подсунул туда `using F = std::remove_cvref_t;` То есть буквально отбросил ссылку. 80 | И если мы посмотрим на объявление `std::forward` 81 | 82 | ```C++ 83 | template< class T > 84 | constexpr T&& forward( typename std::remove_reference::type& t ) noexcept; 85 | template< class T > 86 | constexpr T&& forward( typename std::remove_reference::type&& t ) noexcept; 87 | ``` 88 | 89 | Станет понятно, что именно пошло не так: бессылочный `T` всегда превращается в `T&&` rvalue-ссылка! Вместо `std::forward` разработчик получил `std::move`. А мы получили use-after-move. 90 | 91 | Универсально корректное использование `std::forward` выглядит так: 92 | 93 | ```C++ 94 | std::forward(value) 95 | ``` 96 | 97 | Либо, конечно, можно использовать явный параметр шаблона 98 | 99 | ```C++ 100 | template Gen> 101 | auto MakeMatrix(Gen&& fn) { 102 | using F = std::remove_cvref_t; 103 | return Matrix(std::forward(fn)); 104 | } 105 | ``` 106 | Но такой вариант не защищен от ошибок при рефакторинге. 107 | 108 | ---- 109 | Я не большой фанат макросов, но конкретно для этого случая крайне рекомендую завести макрос 110 | ```C++ 111 | #define FORWARD(x) ::std::forward(x) 112 | ``` 113 | И никогда больше не допускать ошибку. 114 | 115 | В C++23 в стандартную библиотеку добавили еще функцию `std::forward_like`, чтобы навешивать на ваш объект ссылку той же категории, как у другого объекта. Соответствущий макрос может выглядеть так 116 | 117 | ```C++ 118 | #define FORWARD_LIKE(target, value) ::std::forward_like(value) 119 | ``` 120 | 121 | 122 | ## Полезные ссылки 123 | 1. https://en.cppreference.com/w/cpp/utility/forward 124 | 2. https://en.cppreference.com/w/cpp/utility/forward_like 125 | 126 | -------------------------------------------------------------------------------- /standard_lib/images/transform_filter_bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nekrolm/ubbook/5ee6434b9b741e4b7b06c76fa280a48b3a66e65a/standard_lib/images/transform_filter_bench.png -------------------------------------------------------------------------------- /standard_lib/map_subscript.md: -------------------------------------------------------------------------------- 1 | # `operator []` ассоциативных контейнеров 2 | 3 | Удивительное дело, но в этой заметке не будет ничего, связанного с неопределенным поведением. По крайней мере напрямую. 4 | 5 | В стандартной библиотеке C++ много неоднозначных решений. Одно из таких: объединить операцию вставки в ассоциативный контейнер и получения элемента. 6 | 7 | `operator []` для ассоциативных контейнеров, пытается вызвать конструктор по умолчанию для элемента, если не находит переданный ключ. 8 | 9 | С одной стороны это удобно: 10 | 11 | ```C++ 12 | std::map counts; 13 | for (Word c : text) { 14 | ++counts[word]; // ровно один поиск по ключу 15 | } 16 | ``` 17 | 18 | В иных языках придется постараться, чтобы записать то же самое и не допустить повторного поиска. 19 | 20 | ```Java 21 | map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1); // поиск трижды! 22 | map.put(key, map.getOrDefault(key, 0) + 1); // поиск дважды! 23 | ``` 24 | Оно, конечно, может быть, отоптимизируется JIT-компилятором... Но мы в C++ любим гарантии. 25 | 26 | 27 | С другой стороны, этот вызов конструктора, если элемент не найден, может выйти боком: 28 | 29 | ```C++ 30 | struct S { 31 | int x; 32 | explicit S (int x) : x {x} {} 33 | }; 34 | 35 | std::map m { { 1, S{2} }}; // Ok 36 | m[0] = S(5); // огромная трудночитаемая ошибка компиляции 37 | auto s = m[1]; // опять огромная трудночитаемая ошибка компиляции 38 | 39 | //------------------- 40 | struct Huge { 41 | Huge() { 42 | data.reserve(4096); 43 | } 44 | 45 | std::vector data; 46 | }; 47 | 48 | std::map m; 49 | Huge h; 50 | ... /* заполняем h */ 51 | m[0] = std::move(h); // бесполезный вызов default-конструктора, лишняя аллокация, 52 | // а потом перемещение 53 | ``` 54 | 55 | Чтобы выпутаться из этой неприятности, у ассоциативных контейнеров к C++17 (20) наплодили целую гору методов `insert_or_assign`, `try_emplace` и `insert` с непонятным для непосвященных возвращаемым значением `pair`. 56 | 57 | Всем этим добром, конечно же, пользоваться тяжело и неудобно. Про них пишут длинные статьи в блоги о том, как эффективно пользоваться поиском по контейнерам... 58 | 59 | С `operator[]`, конечно же, проще, «понятнее» и короче. Но и ловушка для невнимательных. 60 | А если еще и с мерзопакостными особенностями других объектов скрестить... 61 | 62 | ```C++ 63 | std::map options { 64 | {"max_value" : "1000"}; 65 | } 66 | .... 67 | const auto ParseInt = [](std::string s) { 68 | std::istringstream iss(s); 69 | int val; 70 | iss >> val; 71 | return val; 72 | }; 73 | 74 | const int value = ParseInt(options["min_value"]); //!перепутали !нет такого поля 75 | // value == 0. Все "ok". Счастливой отладки 76 | // operator[] вернул пустую строку 77 | // >> сфейлился и записал ноль в результат 78 | ``` 79 | 80 | Избежать неприятностей с `operator[]` для ассоциативных контейнеров можно, навесив `const`. 81 | И тогда вам этот оператор доступен не будет. И придется использовать либо `.at`, бросающий исключения. Либо всеми любимый: 82 | 83 | ```C++ 84 | if (auto it = m.find(key); it != m.end()) { 85 | // делай что хочешь с *it, it->second 86 | } 87 | ``` 88 | 89 | Все просто. 90 | -------------------------------------------------------------------------------- /standard_lib/null_terminated_string.md: -------------------------------------------------------------------------------- 1 | # NULL-терминированные строки 2 | 3 | В начале 70-х Кен Томпсон, Деннис Ритчи и Брайан Керниган, работая над первыми версиями C и Unix, приняли решение, которое отзывается болью, страданиями, багами и неэффективностью до сих пор, спустя 50 лет. 4 | Они решили, строки, как данные переменной длины, нужно представлять в виде последовательности, заканчивающейся терминирующим символом -- нулем. Так делали в ассемблере, а C ведь высокоуровневый ассемблер! Да и памяти у старенького PDP не много: лучше всего один байтик лишний на строку, чем 2, 4, а то и все 8 байтов для хранения размера в зависимости от платформы... Не, лучше байтик в конце! 5 | Но в других языках почему-то предпочли хранить размер и ссылку/указатель на данные... 6 | 7 | Ну что ж, посмотрим, к чему это привело 8 | 9 | ### Длина строки 10 | 11 | Единственный способ узнать длину NULL-термирированной строки -- пройтись по ней и посчитать символы. Это требует линейного времени от длины строки. 12 | 13 | ```C++ 14 | const char* str = ...; 15 | for (size_t i = 0 ; i < strlen(str); ++i) { 16 | ... 17 | } 18 | ``` 19 | 20 | И вот уже этот простенький цикл требует не линейного числа операций, а квадратичного. Это известный пример. Про него даже известно, что умный компилятор способен вынести вычисление длины строки из цикла 21 | 22 | ```C++ 23 | const char* str = ...; 24 | const size_t len = strlen(str); 25 | for (size_t i =0 ; i < len; ++i) { 26 | ... 27 | } 28 | ``` 29 | 30 | Но ведь пример может быть и посложнее. В коде одной там популярной игры про деньги, разборки мафии и угон автомобилей обнаружили занятный пример парсинга большого массива чисел из json-строки с помощью `sscanf` 31 | 32 | Выглядел он примерно (его получили путем реверс-инженеринга конечного бинарного файла) так 33 | 34 | ```C++ 35 | const char* config = ...; 36 | size_t N = ...; 37 | 38 | for (size_t i =0 ; i < N; ++i) { 39 | int value = 0; 40 | size_t parsed = sscanf(config, "%d", &value); 41 | if parsed > 0 { 42 | config += parsed; 43 | } 44 | } 45 | ``` 46 | 47 | Прекрасный и замечательный цикл! Тело его выполняется всего `N` раз, но на большинстве версий стандартной библиотеки C каждый раз требует `strlen(config)` операций на интерацию. Ведь `sscanf` должен посчитать длину строки, чтоб случайно не выйти за ее пределы! А строка NULL-терминированная. 48 | 49 | Вычисление длины строки -- невероятно часто всречающаяся операция. И один из самых первых кандидатов на оптимизацию -- посчитать ее один раз и хранить со строкою... Но зачем тогда NULL-терминатор? Только лишний байт в памяти! 50 | 51 | 52 | ### С++ и std::string 53 | 54 | C++ высокоуровневый язык! Уж повыше C, конечно. Стандартные строки в нем уже, учтя ошибку C, хранятся как размер + указатель на данные. Ура! 55 | 56 | Но не совсем ура. Ведь огромное число C библиотек никуда не денется, и у большинства из них в интерфейсах используются NULL-терминированные строки. 57 | Поэтому `std::string` тоже обязательно NULL-терминированные. Поздравляю, мы храним один лишний байт ради совместимости. А еще мы его храним неявно: `std::string::capacity()` на самом деле всегда на 1 меньше действительно выделенного блока памяти. 58 | 59 | ### C++ и std::string_view 60 | 61 | "Используйте `std::string_view` в своих API и вам не придется писать перегрузки для `const char*` и `const std::string&` чтобы избежать лишнего копирования!" 62 | 63 | Ага, конечно. 64 | 65 | `std::string_view` это тоже указатель + длина строки. Но уже, в отличие от `std::string`, указатель не обязательно на NULL-терминированную строку (Ура, мы можем использовать `std::vector` и не хранить лишний байт!). 66 | 67 | Но если вдруг за фасадом вашего удобного API со `string_view` скрывается обращение к какой-нибудь сишной библиотеке, требующей NULL-терминированную строку... 68 | 69 | ```C++ 70 | // Эта маленькая программа весело и задорно выведет 71 | 72 | // Hello 73 | // Hello World 74 | 75 | // Хотите вы этого или нет. 76 | 77 | void print_me(std::string_view s) { 78 | printf("%s\n", s.data()); 79 | } 80 | 81 | int main() { 82 | std::string_view hello = "Hello World"; 83 | std::string_view sub = hello.substr(0, 5); 84 | std::cout << sub << "\n"; 85 | print_me(sub); 86 | } 87 | ``` 88 | 89 | Чуть-чуть [изменим](https://godbolt.org/z/qPoeha4jc) аргументы 90 | 91 | ```C++ 92 | // Теперь эта маленькая программа весело и задорно выведет 93 | 94 | // Hello 95 | // Hello Worldnext (а может быть просто упадет с ошибкой сегментации) 96 | 97 | // Хотите вы этого или нет. 98 | 99 | 100 | void print_me(std::string_view s) { 101 | printf("%s\n", s.data()); 102 | } 103 | 104 | int main() { 105 | char next[] = {'n','e','x','t'}; 106 | char hello[] = {'H','e','l','l','o', ' ', 'W','o','r','l','d'}; 107 | std::string_view sub(hello, 5); 108 | std::cout << sub << "\n"; 109 | print_me(sub); 110 | } 111 | ``` 112 | 113 | 114 | Функция не менялась, мы просто передали другие параметры и всё совсем сломалось! А это всего лишь `print`. С какой-то другой функцией может случиться что-то совершенно немыслимое, когда она пойдет за границы диапазона, заданного в `string_view`. 115 | 116 | Что же делать?! 117 | 118 | Нужно гарантировать NULL-терминированность. А для этого надо скопировать строку... Но ведь `std::string_view` мы же специально использовали в API, чтобы не копировать?! 119 | 120 | Увы. Как только вы сталкиваетесь со старыми C API, оборачивая их, вы либо вынуждены писать две имплементации -- с сырым `char*` и c `const std::string&`. Либо соглашаться на копирование на каком-то из уровней. 121 | 122 | ### Как бороться 123 | 124 | Никак. 125 | 126 | NULL-терминированные строки -- унаследованная неэффективность и возможность для ошибок, от которых мы уже, вероятно, никогда не избавимся. В наших силах лишь постараться не продолжать плодить зло: 127 | В новых C-библиотеках стараться проектировать API, использующие пару указатель + длина, а не только лишь указатель на NULL-терминированную последовательность. 128 | 129 | От этого наследия страдают программы на всех языках, вынужденные взаимодействовать с C API. 130 | Rust, например, использует отдельные типы `CStr` и `CString` для подобных строк и переход к ним из нормального кода всегда сопровоздается мучительными тяжелыми преобразованиями. 131 | 132 | Использование NULL-терминатора встречается не только для текстовых строк. Так, например, библиотека [SRILM](http://www.speech.sri.com/projects/srilm/) активно использует 0-терминированные последовательности числовых идентификаторов, создавая этим дополнительные проблемы. 133 | Семейство функций exec в Linux принимают NULL-терминированные последовательности указателей. 134 | EGL использует для инициализации списки атрибутов, оканчивающиеся нулем. 135 | И многие многие другие. 136 | 137 | Не нужно дизайнить неудобные, уязвимые к ошибкам, API без великой надобности. Экономия в функции на одном параметре, размером в указатель, редко когда оправдана. 138 | 139 | 140 | -------------------------------------------------------------------------------- /standard_lib/stl_constructors.md: -------------------------------------------------------------------------------- 1 | # Перегруженные конструкторы стандартной библиотеки 2 | 3 | При проектировании стандартной библиотеки C++ было принято множество странных 4 | решений, из-за которых приходится страдать. И исправить их не представляется 5 | возможным из-за соображений обратной совместимости. 6 | 7 | Одним из таких странных решений являются перегрузки конструкторов с радикально 8 | различным поведением. 9 | 10 | Яркий [пример](https://gcc.godbolt.org/z/e3YnYx): 11 | ```C++ 12 | using namespace std::string_literals; 13 | std::string s1 { "Modern C++", 3 }; 14 | std::string s2 { "Modern C++"s, 3 }; 15 | 16 | std::cout << "S1: " << s1 << "\n"; 17 | std::cout << "S2: " << s2 << "\n"; 18 | ``` 19 | 20 | Этот код выведет 21 | ``` 22 | S1: Mod 23 | S2: ern C++ 24 | ``` 25 | 26 | Потому что у `std::basic_string` есть один конструктор, принимающий указатель и длину строки. 27 | А есть еще один конструктор, принимающий «что-то похожее на строку» и позицию, с которой надо из нее извлечь подстроку! 28 | 29 | На этом причуды не заканчиваются. 30 | 31 | ```C++ 32 | std::string s1 {'H', 3}; 33 | std::string s2 {3, 'H'}; 34 | std::string s3 (3, 'H'); 35 | 36 | std::cout << "S1: " << s1.size() << "\n"; 37 | std::cout << "S2: " << s2.size() << "\n"; 38 | std::cout << "S3: " << s3.size() << "\n"; 39 | ``` 40 | 41 | Этот [пример](https://gcc.godbolt.org/z/rrP67s) выведет 42 | ``` 43 | S1: 2 44 | S2: 2 45 | S3: 3 46 | ``` 47 | 48 | Потому что у строки есть конструктор, принимающий число `n` и символ `c`, который 49 | нужно повторить `n` раз. А еще есть конструктор, принимающий список инициализации (`std::initializer_list`), состоящий из символов. И существование этого конструктора взаимодействует с неявным приведением типов! 50 | 51 | - `std::string s1 {'H', 3};` — строка "H\3" 52 | - `std::string s2 {3, 'H'};` — строка "\3H" 53 | - `std::string s3 (3, 'H');` — строка "HHH" 54 | 55 | Аналогичной проблемой страдает `std::vector` 56 | 57 | ```C++ 58 | std::vector v1 {3, 2}; // v1 == {3, 2} 59 | std::vector v2 (3, 2); // v2 == {2,2,2} 60 | ``` 61 | 62 | А еще у контейнеров есть конструктор, принимающий пару итераторов. И, казалось бы, с ними уж проблем-то не будет, но у нас есть указатели, которые также являются итераторами. А еще есть тип `bool`: 63 | 64 | ```C++ 65 | bool array[5] = {true, false, true, false, true}; 66 | std::vector vector {array, array + 5}; 67 | std::cout << vector.size() << "\n"; 68 | ``` 69 | 70 | [Будет выведено](https://gcc.godbolt.org/z/jobeh6) 2, а не 5. Потому что указатели неявно приводятся к `bool`! 71 | 72 | Собственно, эти прекрасные примеры показывают, почему «универсальная» инициализация не универсальная. 73 | 74 | Чтобы не множить хаос в своих проектах, нужно быть осторожнее с объявлениями перегруженных конструкторов для своих типов. Лучше ввести статическую функцию, чем создавать перегруженные конструкторы, неожиданно взаимодействующие с неявным приведением типов и списками инициализации. 75 | 76 | ## Полезные ссылки 77 | 1. https://habr.com/ru/post/330402/ -------------------------------------------------------------------------------- /standard_lib/uniform_int_distribution.md: -------------------------------------------------------------------------------- 1 | # Нельзя так просто взять и сгенерировать случайную последовательность байт 2 | 3 | В C++11 стандартная библиотека пополнилась многими отличными штуками, в том числе функциями и классами для работы с генераторами случайных чисел удобным и, как это ни странно, предсказуемым способом: 4 | 5 | В наследство от C, С++ досталась функция `rand()`, которой на сегодняшний день не рекомендуется пользоваться нигде, кроме как совершенно игрушечных проектах: 6 | - Если ее `seed` не инициализировать через `srand()`, он будет инициализирован, внезапно, единицей (а могли бы взять 42) 7 | - Естественно она использует некое глобальное, возможно, thread local состояние. Это implementation defined 8 | - В многопоточной среде thread safety тоже implementation defined. 9 | В общем, очень непредсказуемая штука! 10 | 11 | Другое дело функционал из `#include `! 12 | 13 | Вот вам и разные генераторы, и преобразования функций распределения, и вы их можете друг от друга отделять и иметь свой собственный, особенно инициализированный генератор для каждой отдельной сущности в вашем проекте, и многопоточный доступ организовывайте как хотите. Красота! 14 | 15 | А давайте сгенерируем последовательность случайных, равномерно распределенных байт. 16 | 17 | ```C++ 18 | #include 19 | #include 20 | 21 | int main() 22 | { 23 | std::random_device rd; // a seed source for the random number engine 24 | std::mt19937 gen(rd()); // mersenne_twister_engine seeded with rd() 25 | std::uniform_int_distribution distrib{}; // don't try to use std::byte! it won't compile 26 | 27 | // Use distrib to generate random byte 28 | for (int n = 0; n != 10; ++n) 29 | std::cout << int32_t(distrib(gen)) << ' '; // we need a cast to print numeric values instead of characters 30 | std::cout << '\n'; 31 | } 32 | ``` 33 | Компилируем, запускаем, все [отлично работает](https://godbolt.org/z/xh1bY7PTq)!.. 34 | По крайней мере c GCC и Clang. 35 | 36 | А что если нам нужна кроссплатформенная сборка в том числе под Windows с помощью MSVC?... А давайте попробуем просто взять и скомпилировать как есть... 37 | 38 | [Ой](https://godbolt.org/z/Ka93s7sW6) 39 | 40 | ``` 41 | example.cpp 42 | C:/data/msvc/14.39.33321-Pre/include\random(2107): error C2338: static_assert failed: 'invalid template argument for uniform_int_distribution: N4950 [rand.req.genl]/1.5 requires one of short, int, long, long long, unsigned short, unsigned int, unsigned long, or unsigned long long' 43 | C:/data/msvc/14.39.33321-Pre/include\random(2107): note: the template instantiation context (the oldest one first) is 44 | (9): note: see reference to class template instantiation 'std::uniform_int_distribution' being compiled 45 | C:/data/msvc/14.39.33321-Pre/include\random(2107): error C2338: static_assert failed: 'note: char, signed char, unsigned char, char8_t, int8_t, and uint8_t are not allowed' 46 | Compiler returned: 2 47 | ``` 48 | 49 | Ну-у, матерый разработчик, знакомый с причудами старых версий MSVC, который с по умолчанию включенным `\permissive+` мог компилировать совершенно безумные вещи, не сильно удивится и скажет, что опять Microsoft какую-то ерунду придумали просто... 50 | 51 | Удивительно, [но нет](https://eel.is/c++draft/rand#req.genl-1.6)! 52 | 53 | ``` 54 | Throughout this subclause [rand], the effect of instantiating a template: 55 | ... 56 | that has a template type parameter named UIntType is undefined unless the corresponding template argument is cv-unqualified and is one of unsigned short, unsigned int, unsigned long, or unsigned long long. 57 | ``` 58 | 59 | Подставлять `uint8_t` в `std::uniform_int_distribution` стандартом не разрешается под страхом неопределенных эффектов. 60 | 61 | Это, конечно, совершенно нелепо, и в 2013 году даже поднимался вопрос о принятии [defect report](https://cplusplus.github.io/LWG/issue2326), но его отклонили как not a defect и предложили написать proposal в стандарт, чтобы как-то это дело исправить... В общем по состоянию на 2024 год не исправили. Возможно, исправят в C++26. Или в C++29. 62 | 63 | Кстати, на всякий случай, `__int128_t` тоже использовать как бы нельзя. [Хотя с GCC работает.](https://godbolt.org/z/9Mxrx6aEr) 64 | 65 | # Полезные ссылки 66 | 1. [Обсуждение на reddit](https://www.reddit.com/r/cpp/comments/1czwa5h/is_instantiating_stduniform_int_distributionuint8/) 67 | 2. [cppreference uniform_int_distribution](https://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution) 68 | 69 | -------------------------------------------------------------------------------- /syntax/assume.md: -------------------------------------------------------------------------------- 1 | # Атрибут [[assume]] 2 | 3 | "Есть некоротая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!" 4 | 5 | Да, С++23 наконец-то дал простым пользователям инструмент целенаправленного **внедрения** неопределенного поведения в их код. Такой инструмент, правда, давно уже был и так, но специфичный для конкретного компилятора. C++23 же всего лишь стандартизировал его. Так что радуйтесь, никаких больше уродливых `__builtin_assume`! 6 | 7 | - Зачем вообще такая возможность существует?! — первый же вопрос, который возникает после прочтения абзаца выше. Неужели недостаточно ужасов самого языка, нужно еще пользователям позволить создавать новые?! 8 | 9 | На самом деле, конечно, причина есть: компиляторы глупые, быстрый и оптимальный код получить хочется, а на ассемблере писать не очень хочется. Хотя, конечно, разработчики ffmpeg с этим не согласятся — они поэтому целенаправленно делают ассемблерные вставки, не доверяя компиляторам С. 10 | 11 | Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику новвовведения С++23. 12 | 13 | Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианну величину 14 | 15 | ```Rust 16 | use std::num::NonZeroUsize; 17 | 18 | pub fn medians(data: &[f32], group: NonZeroUsize) -> Vec { 19 | let n = group.get(); 20 | data.chunks_exact(n) // разбиваем на группы по n, 21 | // последняя группа если в ней меньше n -- игнорируется 22 | .map(move |chunk| chunk[n/2]) // берем медиану 23 | .collect() // собираем результат 24 | } 25 | ``` 26 | 27 | Если мы [скомпилируем](https://godbolt.org/z/vYezzv9ba) эту функцию довольно старой версией Rustc 1.51 с opt-level=3, мы обнаружим, что код получился так себе 28 | 29 | 1. Мы видим в начале функции 30 | ``` 31 | sub rsp, 120 32 | mov qword ptr [rsp + 96], rcx 33 | test rcx, rcx 34 | je .LBB4_33 35 | ... 36 | .LBB4_33: 37 | ... 38 | call qword ptr [rip + core::panicking::panic_fmt::hcd56f7f635f62c74@GOTPCREL] 39 | ud2 40 | ``` 41 | 42 | Это проверка что `n` не ноль. Но мы же и так знаем что `n` не ноль ­— это четко указано в типе входного параметра! 43 | 44 | 2. При обработке каждой группы мы находим 45 | ``` 46 | shr rdi 47 | cmp rdi, r15 48 | jae .LBB4_27 49 | ... 50 | .LBB4_27: 51 | lea rdx, [rip + .L__unnamed_4] 52 | mov rsi, r15 53 | call qword ptr [rip + core::panicking::panic_bounds_check::h16537cfb53a1364b@GOTPCREL] 54 | ``` 55 | Каждый раз проверяется что индекс `n/2` в границах группы. Но ведь это всегда так! 56 | 57 | Очень бы хотелось донести до компилятора такие очевидные факты. Собственно `[[assume(condition)]]` для того в C++23 и добавили. Если компилятор не смог догадаться до чего-то самостоятельно и сгенерировать оптимальный код, мы теперь можем ему подсказать... 58 | 59 | Так c GCC14 и C++26 та же самая функция (используя безопасные методы, как в Rust) 60 | 61 | ```C++ 62 | struct NonZero { 63 | public: 64 | explicit NonZero(size_t v) : value { 65 | v > 0 ? v : throw std::runtime_error("Zero value") 66 | } {} 67 | 68 | size_t get() const { 69 | return value; 70 | } 71 | private: 72 | size_t value; 73 | }; 74 | 75 | template 76 | auto chunks_exact(std::span data, size_t n) { 77 | if (n == 0) { 78 | throw std::runtime_error("zero chunk len"); 79 | } 80 | return data.subspan(0, data.size() - data.size() % n) 81 | | std::views::chunk(n) 82 | | std::views::transform([](auto chunk){ return std::span(&chunk.front(), chunk.size()); }); // remap into spans 83 | } 84 | 85 | 86 | __attribute__((noinline)) 87 | std::vector medians(std::span data, NonZero group) { 88 | size_t n = group.get(); 89 | // [[assume(n>0)]]; 90 | return chunks_exact(data, n) 91 | | std::views::transform([n](auto chunk) { return chunk.at(n/2); }) 92 | | std::ranges::to(); 93 | } 94 | ``` 95 | также компилируется со всеми ненужными проверками. Но стоит нам только лишь добавить `[[assume(n>0)]]`, как ситуация [меняется](https://godbolt.org/z/Yvez1WjeK) и все избыточные проверки на ноль и на границы групп могут быть успешно выброшены компилятором! 96 | 97 | --- 98 | 99 | Но что если мы подсказали неправильно? Неопределенное поведение, конечно же! 100 | Из ложной посылки следует что угодно. 101 | 102 | А если мы подсказывали правильно, но только на допустимом множестве входных данных? Отлично, все хорошо, только не забудьте включить в документацию упоминание неопределенного поведения на недопустимом входе. 103 | 104 | Но прежде чем начинать пользоваться такой замечательной возможностью языка, стоит понимать: 105 | 106 | 1. Правильная подсказка ничего не гарантирует. А ложная невероятно опасна 107 | 2. Новые версии компиляторов и сами могут догадаться. Так, например, rustc 1.80 на рассмотренном примере [оптимизирует](https://godbolt.org/z/c5Kc9hP8r) уже все как надо. 108 | 109 | -------------------------------------------------------------------------------- /syntax/c_variadic.md: -------------------------------------------------------------------------------- 1 | # Эллипсис и функции с произвольным числом аргументов 2 | 3 | Наверняка все С++ (а уж просто C тем более) программисты знакомы с семейством функций 4 | `printf`. Одной из удивительных особенностей этих функций является возможность принимать произвольное число аргументов. А также на `printf` можно писать [полноценные программы!](https://github.com/carlini/printf-tac-toe). Исследованию и описанию этого безумия даже посвящены отдельные [статьи](https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-carlini.pdf). 5 | 6 | Мы же остановимся только на произвольном числе аргументов. Но для начала я расскажу одну занимательную историю. 7 | 8 | Какая-то замечательная библиотека предоставляла красивую функцию 9 | ```C++ 10 | template 11 | void ProcessBy(HandlerFunc&& fun) 12 | requires std::is_invocable_v; 13 | ``` 14 | 15 | И программист думал вызвать эту восхитительную функцию. В качестве `HandlerFunc` подсунуть лямбду, в которой ему было совершенно наплевать на передаваемые аргументы `T1, T2, T3, T4, T5`. 16 | Что же он мог сделать? 17 | 18 | Вариант первый: честно перечислить пять аргументов с их типами. Как деды делали. 19 | 20 | ```C++ 21 | ProcessBy([](T1, T2, T3, T4, T5) { do_something(); }); 22 | ``` 23 | Если имена типов короткие, почему бы и нет. Но все равно как-то слишком подробно. Не удобно. Да и добавится новый аргумент — придется и тут править. Не очень современный C++-подход. 24 | 25 | Вариант второй: воспользоваться функциями с произвольным числом аргументов. 26 | 27 | ```C++ 28 | ProcessBy([](...){ do_something(); }); 29 | ``` 30 | 31 | Вау, красота! Компактно и здорово. До чего прогресс дошел! 32 | И оно скомпилировалось. И даже работало. И так программист и оставил. 33 | 34 | Но однажды замечательная библиотека обновилась, стала лучше и безопаснее. И начались странные, необъяснимые падения. SIGILL, SIGABRT, SIGSEGV. Все наши любимые друзья хлынули в проект. 35 | 36 | Что произошло? Кто виноват? Что делать? Без опытного сыщика тут не обойтись... 37 | 38 | ------ 39 | Дайте разбираться. 40 | 41 | В C можно определять собственные функции, принимающие сколь угодно много аргументов. 42 | И сделать это можно двумя способами: 43 | 44 | 1. Пустой список аргументов. 45 | ```C 46 | void foo() { 47 | printf("foo"); 48 | } 49 | 50 | foo(1,2,4,5,6); 51 | ``` 52 | Казалось бы, функция `foo` не должна в принципе принимать аргументы. [Но нет](https://godbolt.org/z/MPv6E4). В C функции, объявленные с пустым списком аргументов, на самом деле являются функциями с произвольным числом аргументов. Действительно ничего не принимающая функция объявляется так 53 | ```C 54 | void foo(void); 55 | ``` 56 | В C++ это безобразие исправили. 57 | 58 | 2. Эллипсис и `va_list` 59 | 60 | ```C 61 | #include 62 | 63 | void sum(int count, /* чтобы получить доступ к списку аргументов, 64 | нужен хотя бы один явный */ 65 | ...) { 66 | int result = 0; 67 | va_list args; 68 | va_start(args, count); 69 | for (int i = 0; i < count; ++i) // причем функция не знает, 70 | // сколько аргументов передали 71 | { 72 | result += va_arg(args, int); // запрашиваем очередной аргумент 73 | // функция не знает какой у него тип 74 | // указываем самостоятельно — int 75 | } 76 | va_end(args); 77 | return result; 78 | } 79 | ``` 80 | Если явного аргумента не будет, то получить доступ к списку остальных нельзя. 81 | Более того, мы уйдем в область implementation defined поведения. 82 | 83 | Также на этот явный аргумент, предшествующий вариативной части, налагаются ограничения: 84 | - Он не может быть помечен спецификатором `register`. Но это мало кому надо 85 | - Он не может иметь «повышаемый» тип. Привет нашим любимым integer/float promotion. `float`, `short`, `char` нельзя. 86 | 87 | Нарушаем ограничения явного аргумента — получаем неопределенное поведение. 88 | Запрашиваем у `va_arg` повышаемый тип — снова неопределенное поведение. 89 | Передаем не тот тип, что запрашиваем... Правильно, неопределенное поведение. 90 | 91 | Невероятные возможности по отстрелу рук и ног себе и пользователям кода! Собственно, на этих 92 | возможностях и идет игра, при атаках на `printf`. 93 | 94 | И в C++, конечно же, эта прелесть осталась. И не просто осталась, но и значительно усилилась! 95 | 96 | ------- 97 | 98 | C простой, маленький язык. В нем не так много типов. Примитивы, указатели, да пользовательские структуры. 99 | 100 | В C++ есть ссылки. Есть объекты с интересными конструкторами и деструкторами. И вы уже наверняка догадались о том, что будет неопеределенное поведение, если засунуть ссылку или такой объект в качестве аргумента вариативной функции. Еще больше возможностей для веселой отладки! 101 | 102 | Но C++ не был бы самим собой, если бы в нем эту проблему не «решили». И так у нас есть C++-style вариадики: 103 | 104 | ```C++ 105 | template 106 | int avg(ArgT... arg) { 107 | // доступно число аргументов 108 | const size_t args_cnt = sizeof...(ArgT); 109 | // доступны их типы 110 | 111 | // итерироваться по аргументам нельзя 112 | // нужно писать рекурсивные вызовы для обработки, 113 | // либо использовать fold expressions 114 | return (arg + ... + 0) / ((args_cnt == 0) ? 1 : args_cnt); 115 | } 116 | ``` 117 | Не очень удобно, но намного лучше и безопаснее. 118 | 119 | ------ 120 | 121 | Ну что ж, теперь когда все карты вскрыты, вернемся к нашему детективу. 122 | 123 | Убийца — C-вариадик! 124 | ```C++ 125 | ProcessBy([](...){ do_something(); }); 126 | ``` 127 | 128 | Когда библиотека обновилась. В ней, незначительно на первый взгляд, поменялся один из типов `T`, которые передавались функцией `ProcessBy` в `HandlerFunc`. Но это изменение привело к неопределенному поведению. 129 | 130 | А программисту же нужно было использовать C++-вариадик. 131 | 132 | ```C++ 133 | ProcessBy([](auto...){ do_something(); }); 134 | ``` 135 | 136 | Все. Всего одно слово `auto` и никто бы не погиб. Удобно. 137 | 138 | И конечно, чтобы не было лишних копирований, надо дописать два амперсанда: 139 | 140 | ```C++ 141 | ProcessBy([](auto&&...){ do_something(); }); 142 | ``` 143 | Вот теперь все. Прекрасный способ принять и проигнорировать сколь угодно много аргументов. 144 | Ну, а тем программистом был когда-то я сам. 145 | 146 | # Полезные ссылки 147 | 1. https://habr.com/ru/post/430064/ 148 | 2. https://en.cppreference.com/w/c/variadic/va_list -------------------------------------------------------------------------------- /syntax/comma_operator.md: -------------------------------------------------------------------------------- 1 | # Оператор запятая 2 | 3 | Если вы начинали свое знакомство с программированием с языков Pascal или C#, то, наверное, знаете, что 4 | в них обращение к элементам двумерного массива (а также массивов большей размерности) осуществляется перечислением индексов через запятую внутри квадратных скобок 5 | 6 | ```C# 7 | double [,] array = new double[10, 10]; 8 | double x = array[1,1]; 9 | ``` 10 | 11 | Также в записи на псевдокоде или в специализированных языках для математических вычислений (MatLab, MathCAD) часто используют именно такой или похожий (круглые скобки) способы. 12 | 13 | В C/C++ же на каждую размерность должны быть свои квадратные скобки 14 | 15 | ```C++ 16 | double array[10][10]; 17 | double x = array[1][1]; 18 | ``` 19 | 20 | Однако, написать «неправильно» нам никто не запрещает и, более того, компилятор обязан это [скомпилировать](https://godbolt.org/z/G4zhYdnd1)! 21 | 22 | ```C++ 23 | int array[5][5] = {}; 24 | std::cout << array[1,4]; // oops! 25 | ``` 26 | 27 | В комбинации с неявным приведением типов и выходами за границы массивов, можно наиграть множество неприятностей при невнимательном переносе кода. 28 | 29 | Почему это вообще компилируется? 30 | 31 | Все дело в операторе «запятая» (`,`). Она последовательно вычисляет оба своих аргумента и возвращает второй (правый). 32 | 33 | ```C++ 34 | int array[2][5] = {} 35 | auto x = array[1, 4]; // Oops! Это array[4]. 36 | // Но для первой размерности максимальное значение = 1. Неопределенное поведение! 37 | ``` 38 | 39 | В C++20, на наше счастье, использование оператора `,` при индексировании массивов пометили как deprecated и 40 | теперь компиляторы будут [сыпать предупреждениями](https://godbolt.org/z/976Gsad1o) (вы всегда можете их превратить в ошибки). 41 | 42 | На этом можно было бы и закончить, если бы не один нюанс. 43 | 44 | ## Перегрузки оператора `,` 45 | 46 | Запятую можно перегрузить. И посеять немного хаоса. 47 | 48 | ```C++ 49 | return f1(), f2(), f3(); 50 | ``` 51 | Если `,` не перегружена, стандарт гарантирует, что функции будут вызваны последовательно. 52 | Если же тут вызывается перегруженная запятая, то до C++17 такой гарантии нет. 53 | 54 | В случае встроенной запятой тут гарантируется, что тип результата совпадает с последним аргументов в цепочке. 55 | Если же оператор перегружен — тип может быть [каким угодно](https://godbolt.org/z/qW5Gsfcbs). 56 | 57 | ```C++ 58 | auto test() { 59 | return f1(), f2(), f3(); 60 | } 61 | 62 | int main() { 63 | test(); 64 | static_assert(!std::is_same_v); 65 | static_assert(std::is_same_v); // ??! 66 | return 0; 67 | } 68 | ``` 69 | 70 | Запятой часто пользуются в различных шаблонах, чтобы раскрывать пачки аргументов произвольной длины, или чтобы проверять несколько условий, триггерящих SFINAE. 71 | 72 | Из-за потенциальной возможности влететь в перегруженную запятую, в выражениях с ней авторы библиотек 73 | прибегают к касту каждого аргумента к `void` — перегрузку, принимающую `void` невозможно написать. 74 | 75 | ```C++ 76 | template 77 | void invoke_all(F&&... f) { 78 | (static_cast(f()), ...); 79 | } 80 | 81 | int main() { 82 | invoke_all([]{ 83 | std::cout << "hello!\n"; 84 | }, 85 | []{ 86 | std::cout << "World!\n"; 87 | }); 88 | return 0; 89 | } 90 | ``` 91 | 92 | Зачем вообще может понадобиться перегружать запятую? 93 | 94 | Может быть, для какого-нибудь DSL (domain-specific language). 95 | 96 | Или вдруг вам все-таки захочется сделать так, чтоб индексация через запятую [работала](https://godbolt.org/z/bjjrr6nd3). 97 | 98 | ```C++ 99 | 100 | struct Index { size_t idx; }; 101 | 102 | template 103 | struct MultiIndex : std::array {}; 104 | 105 | template 106 | auto operator , (MultiIndex i1, MultiIndex i2) { ... } 107 | 108 | template 109 | auto operator , (Index i1, MultiIndex i2) { ... } 110 | 111 | template 112 | auto operator , (MultiIndex i1, Index i2) { ... } 113 | 114 | auto operator , (Index i1, Index i2) { ... } 115 | 116 | Index operator "" _i (unsigned long long x) { 117 | return Index { static_cast(x) }; 118 | } 119 | 120 | template 121 | struct Array2D { 122 | T arr[N][M]; 123 | 124 | T& operator [] (MultiIndex<2> idx) { 125 | return arr[idx[0].idx][idx[1].idx]; 126 | } 127 | }; 128 | 129 | int main() { 130 | Array2D arr; 131 | 132 | arr[1_i, 2_i] = 5; 133 | std::cout << arr[1_i, 2_i]; // Ok 134 | std::cout << arr[1_i, 2_i, 3_i]; // Compilation error 135 | } 136 | 137 | ``` 138 | -------------------------------------------------------------------------------- /syntax/comparison_operator_rewrite.md: -------------------------------------------------------------------------------- 1 | # Пользовательские операторы сравнения в C++20 2 | 3 | С++, конечно, развивается медленным циклом в три года. Чтобы ничего не сломать. Большой успех -- большая ответственность... 4 | Но несмотря на всю медлительность и осторожность, новые стандарты C++ все равно умудряются подложить мину там, где никто не ожидает. 5 | 6 | С++20 добавил долгожданный "операторо НЛО" `<=>`, *three-way-comparison*, позволяющим существенно сократить однообразный код для определения операций сравнения над пользовательскими типами. 7 | 8 | ```C++ 9 | struct Pair { 10 | int x; 11 | int y; 12 | 13 | auto operator<=>(const Pair&) const = default; 14 | // все операции <, >, ==, !=, <=, >= -- выведены автоматически. Покомпонентное сравнение в порядке объявления полей! 15 | }; 16 | 17 | // И вот мы уже можем сравнивать точки! 18 | Pair {1, 2} < Pair { 2, 3 }; 19 | ``` 20 | 21 | Ну [почти](https://godbolt.org/z/1n4Mx6j3e). 22 | ``` 23 | :8:10: error: 'strong_ordering' is not a member of 'std' 24 | 8 | auto operator <=> (const Pair&) const = default; 25 | | ^~~~~~~~ 26 | :1:1: note: 'std::strong_ordering' is defined in header ''; this is probably fixable by adding '#include ' 27 | ``` 28 | 29 | Внезапно, вы обязаны подключить заголовок, если хотите использовать новую синтаксическую конструкцию 30 | c автоматической реализацией (через `=default`). Причем узнаете вы об этом только, когда попробуете использовать сравнение. 31 | 32 | Добавим заголовок и все будет [работать](https://godbolt.org/z/5E4csxK6b). Здорово? Конечно же! 33 | Но есть кое-что еще. 34 | 35 | Не все типы можно осмысленно упорядочивать. Иногда достаточно только равенства. 36 | В C++20 можно определить `operator ==` и `operator !=` будет выведен автоматически. 37 | Более того, реализация через `= default` также [работает](https://godbolt.org/z/Mc9YEsGWK). 38 | 39 | ```C++ 40 | struct Pair { 41 | int x; 42 | int y; 43 | 44 | bool operator==(const Pair&) const = default; 45 | 46 | }; 47 | 48 | // operator != выведен автоматически 49 | Pair {1, 2} != Pair{3, 4}; 50 | ``` 51 | 52 | Другие операторы сравнения тоже можно автоматически определять по-умолчанию. 53 | 54 | Прекрасно. Но сравнениями для одного и того же типа все не заканчивается. Иногда нам нужно уметь сравнивать 55 | объекты разных типов. Например `std::string` и `std::string_view`. 56 | 57 | Как разработчики справлялись с этой задачей до C++20? 58 | 59 | Ну, например, эксплуатируя неявное приведение типов, когда это уместно. 60 | ```C++ 61 | struct String; 62 | 63 | struct StringView { 64 | // Разрешаем неявное приведение, ведь это удобно 65 | StringView(const String&) {} 66 | bool operator==(const StringView &) const { return true; } 67 | }; 68 | 69 | struct String { 70 | bool operator==(const StringView &sv) const { 71 | // а тут меняем порядок местами, ведь у StringView уже есть operator == 72 | // overload resolution наедет его по первому аргументу 73 | // а ко второму (*this: String) можно применить неявное приведение типа. Все отлично! 74 | return sv == *this; 75 | } 76 | }; 77 | 78 | String{} == String{}; 79 | ``` 80 | [Работает, компилируется c С++17, все отлично!](https://godbolt.org/z/Ynzo54sYe) 81 | 82 | А без трюков 4 перегрузки нужно... 83 | 84 | Ну хорошо. [Переходим](https://godbolt.org/z/Yn8M34d7o) в C++20. 85 | 86 | ``` 87 | Program returned: 139 88 | Program terminated with signal: SIGSEGV 89 | ``` 90 | 91 | Шикарно! Надеюсь, если вы компилировали без `-Werror`, у вас были хотя бы тесты. 92 | 93 | ``` 94 | :14:19: warning: in C++20 this comparison calls the current function recursively with reversed arguments 95 | 14 | return sv == *this; 96 | | ~~~^~~~~~~~ 97 | : In function 'int main()': 98 | :19:31: warning: C++20 says that these are ambiguous, even though the second is reversed: 99 | 19 | return String{} == String{}; 100 | ``` 101 | 102 | С++20 позаботился о разработчиках. И теперь им не нужно выдумывать странные перестановки аргументов местами чтоб писать поменьше перегрузок операторов сравнения. 103 | C++20 ввел правила переписывания [всех операторов сравнения](https://en.cppreference.com/w/cpp/language/overload_resolution#Call_to_an_overloaded_operator), так что компиляторы выполнят перестановку за вас. Даже если в ней нет надобности. И разумеется веселые и находчивые разработчики старых кодовых баз получат бесконечную рекурсию. А c ней и неопределенное поведение. И SIGSEGV от переполнения стека, если повезет. 104 | 105 | Эти изменения в правила поиска перегрузок для операций сравнения имели и другие побочные эффекты. Их постарались исправить в предложении [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. 106 | 107 | Но внезапную рекурсию все равно не убрали! Полагайтесь на предупреждения компилятора. 108 | 109 | В С++20/23, если у вас есть неявное приведение между типами, не нужно больше ничего выдумывать -- определяйте операции для одного и того же типа, а комбинации будут получены автоматическими перестановками. 110 | 111 | ```C++ 112 | struct String { 113 | bool operator==(const String&) const = default; 114 | }; 115 | struct StringView { 116 | StringView(const String&) {} 117 | bool operator==(const StringView &) const = default; 118 | }; 119 | 120 | String{} == StringView{String{}}; 121 | StringView{String{}} == String{}; 122 | ``` 123 | 124 | Если неявного приведения нет, то достаточно определить только одну дополнительную перегрузку 125 | 126 | ```C++ 127 | struct String { 128 | bool operator==(const String&) const = default; 129 | }; 130 | struct StringView { 131 | explicit StringView(const String&) {} 132 | bool operator==(const StringView &) const = default; 133 | }; 134 | 135 | bool operator == (const String& s, const StringView& sv) { 136 | return sv == StringView{s}; 137 | } 138 | 139 | // Обе перестановки работают 140 | String{} == StringView{String{}}; 141 | StringView{String{}} == String{}; 142 | ``` 143 | -------------------------------------------------------------------------------- /syntax/default_default_constructor.md: -------------------------------------------------------------------------------- 1 | # Конструктор по умолчанию и = default 2 | 3 | Гайдлайны по современному C++ всячески намекают, а иногда напрямую советуют: [следуйте "правилу нуля" (rule of zero)](https://en.cppreference.com/w/cpp/language/rule_of_three) для ваших классов, и структур и будет вам счастье! Используйте инициализаторы по умолчанию! C++20 улучшил поддержку структур-аггрегатов, так что не надо писать вручную конструкторы там где это не надо... Но legacy код существует, его затратно переписывать... А также существуют legacy разработчики, которые застряли в C++98... 4 | 5 | Так что в старых кодовых базах можно встретить что-нибудь такое: 6 | 7 | ```C++ 8 | 9 | // Point.hpp 10 | class Point2D { 11 | public: 12 | Point2D(int _x, int _y); 13 | // Раз добавили какой-то конструктор, 14 | // нужно добавить и конструктор по умолчанию 15 | Point2D(); 16 | 17 | int x; 18 | int y; 19 | }; 20 | 21 | // Некоторые разработчики как мантру твердят, что 22 | // определение любых функций всегда нужно выносить 23 | // в компилируемый .cpp файл. Даже коротких. 24 | // Point.cpp 25 | Point2D::Point2D(int _x, int _y) : x {_x}, y {_y} {} 26 | // И даже такие! 27 | Point2D::Point2D() = default; 28 | ``` 29 | 30 | Делать так в современном C++ крайне не рекомендуется. Не только из-за обилия бессмысленного бойлерплейта, но и из-за риска получить неинициализированные поля и неопределенное поведение вместе с ними. 31 | 32 | Инициализация в C++ -- невероятно сложная тема из-за обилия терминологии, переопределенного синтаксиса и вариативности, чтоб удовлетворить все мыслимые и немыслимые возможности. А также из-за множества особых случаев и исключений. И с подобным устаревшим подходом к описанию конструкторов как раз связано одно из таких исключений. 33 | 34 | Пусть нам все-таки очень нужно иметь конструкторы для точки 35 | 36 | И мы их определили в составе объявления класса 37 | ```C++ 38 | class Point2D { 39 | public: 40 | Point2D(int _x, int _y) : x {_x }, y {_y} {} 41 | // Раз добавили какой-то конструктор, 42 | // нужно добавить и конструктор по умолчанию 43 | Point2D() = default; 44 | 45 | int x; 46 | int y; 47 | }; 48 | ``` 49 | И мы создаем точку, инициализированную по умолчанию 50 | с помощью фигурных скобок, как рекомендуется в современном C++ 51 | ```C++ 52 | int main() { 53 | 54 | Point2D a {}; 55 | return a.x; 56 | } 57 | ``` 58 | Стандарт гарантирует, что произойдет zero initialization. 59 | Потому как в классе из тривиальных типов без инициализаторов `Point2D() = default` определил тривиальный конструктор по умолчанию. Так что все здорово. Никаких неинициализированных полей. 60 | 61 | Но стоит нам вынести определение конструктора по умолчанию за пределы объявления класса 62 | 63 | ```C++ 64 | class Point2D { 65 | public: 66 | Point2D(int _x, int _y) : x {_x }, y {_y} {} 67 | Point2D(); 68 | 69 | int x; 70 | int y; 71 | }; 72 | 73 | Point2D::Point2D() = default; 74 | ``` 75 | 76 | Как все резко поменяется! Теперь это уже нетривиальный конструктор. А значит инициализация фигурными скобками должна вызвать его вместо zero initialization. 77 | И поля `x`, `y` останутся неинициализированными. Ведь мы их не инициализировали. 78 | 79 | ```C++ 80 | struct Bad { 81 | int x; 82 | Bad(); 83 | }; 84 | Bad::Bad() = default; 85 | 86 | struct Good { 87 | int x; 88 | Good() = default; 89 | }; 90 | 91 | int main() { 92 | Bad a {}; 93 | Good b {}; 94 | return a.x + b.x; 95 | } 96 | ``` 97 | При компиляции GCC c `-std=c++26 -O3 -Wall -Wextra -Wpedantic -Wuninitialized` 98 | Мы получим предупреждение 99 | ``` 100 | :15:14: warning: 'a.Bad::x' is used uninitialized [-Wuninitialized] 101 | 15 | return a.x + b.x; 102 | ``` 103 | Стоит отметить, что без оптимизаций, ни GCC 14, ни Clang 18 предупреждений [не выдают](https://godbolt.org/z/z1v3bEPEq). 104 | 105 | Ну хорошо. Класс для 2D точки это все-таки отличный кандидат, чтоб просто использовать аггрегаты и списки инициализации и не думать. 106 | 107 | Да. Делайте так! 108 | ```C++ 109 | struct Point2D { 110 | int x = 0; 111 | int y = 0; 112 | }; 113 | ``` 114 | 115 | Я также встречал эту проблему и в более сложных случаях: 116 | 117 | Был класс для логгирования: 118 | ```C++ 119 | class Logger { 120 | public: 121 | Logger(std::string log_group) 122 | Logger(); // определен как Logger::Logger() = default в .cpp файле 123 | private: 124 | // Это поле было в классе давно. У строк есть конструктор по умолчанию 125 | // инициализатор не обязателен 126 | std::string log_group; 127 | }; 128 | ``` 129 | 130 | В какой-то момент было решено добавить поле для контроля максимальной длины строки 131 | 132 | ```C++ 133 | class Logger { 134 | public: 135 | Logger(std::string log_group, size_t limit) 136 | Logger(); 137 | private: 138 | std::string log_group; 139 | size_t limit; // Неопытный программист, 140 | // которому поручили задачу, по аналогии добавил поле без инициализатора 141 | }; 142 | ``` 143 | 144 | Все компилируется, но логгер по умолчанию перестает работать, а `= default` сбивает программиста с толку. 145 | 146 | Инициализируйте поля явно! Всегда, кроме случаев, когда инициализация действительно становится проблемой для производительности. 147 | -------------------------------------------------------------------------------- /syntax/function-try-catch.md: -------------------------------------------------------------------------------- 1 | # Function-try-block 2 | 3 | В C++ существует альтернативный синтаксис для определения тела функции, позволяющий навесить на него целиком перехват и обработку исключений 4 | 5 | ```C++ 6 | // Стандартный способ 7 | void f() { 8 | try { 9 | may_throw(); 10 | } catch (...) { 11 | handle_error(); 12 | } 13 | } 14 | 15 | // Альтернативный синтаксис 16 | void f() try { 17 | may_throw(); 18 | } catch (...) { 19 | handle_error(); 20 | } 21 | ``` 22 | 23 | Во-первых, запись становится короче, с меньшим уровнем вложенности. 24 | Во-вторых, эта фича позволяет нам ловить исключения там, где стандартным способом это сделать невозможно — в списке инициализации класса, при инициализации подобъекта базового класса и подобном. 25 | 26 | ```C++ 27 | struct ThrowInCtor { 28 | ThrowInCtor() { 29 | throw std::runtime_error("err1"); 30 | } 31 | }; 32 | 33 | 34 | struct TryStruct1 { 35 | TryStruct1() try { 36 | 37 | } catch (const std::exception& e) { 38 | // будет поймано исключение из конструктора `c` 39 | std::cout << e.what() << "\n"; 40 | } 41 | ThrowInCtor c; 42 | }; 43 | 44 | struct TryStruct2 { 45 | TryStruct2() { 46 | try { 47 | 48 | } catch (const std::exception& e) { 49 | // исключение не будет поймано, поскольку тело конструктора 50 | // исполняется после инициализации полей 51 | std::cout << e.what() << "\n"; 52 | } 53 | } 54 | ThrowInCtor c; 55 | }; 56 | ``` 57 | 58 | На [примере](https://godbolt.org/z/6Yf5cE7W4) с `try-block` для конструктора мы сталкиваемся с, на первый взгляд странной, неожиданностью: несмотря на блок `catch`, исключение вылетает в код, вызывающий конструктор. 59 | Это логично, ведь если при инициализации полей класса вылетело исключение, мы никак не можем исправить ситуацию и починить объект. 60 | 61 | Потому можно иногда встретить такие страшные нагромождения 62 | ```C++ 63 | struct S { 64 | 65 | S(...) try : 66 | a(...), 67 | b(...) { 68 | try { 69 | init(); 70 | } catch (const std::exception& e) { 71 | log(e); 72 | try_repair(); 73 | } 74 | } catch (const std::exeption& e) { 75 | // не получилось починить или неисправимая ошибка в полях 76 | log(e); 77 | // implicit rethrow 78 | } 79 | 80 | A a; 81 | B b; 82 | }; 83 | ``` 84 | 85 | Ну хорошо. А как насчет деструкторов? Ведь из деструкторов крайне нежелательно выкидывать исключения, и возможность красиво и просто поставить `catch`, который бы гарантированно перехватил все, весьма недурна. 86 | 87 | ```C++ 88 | struct DctorThrowTry { 89 | ~DctorThrowTry() try { 90 | throw std::runtime_error("err"); 91 | } catch (const std::exception& e) { 92 | std::cout << e.what() << "\n"; 93 | } 94 | }; 95 | ``` 96 | 97 | Выглядит неплохо. Но у нас C++, так что это [не работает](https://godbolt.org/z/vMhb8nWvq)! 98 | 99 | Кто-то очень доброжелательный решил, что в случае с деструкторами поведение по умолчанию должно быть таким же как и с конструкторами. То есть **`catch` блок деструктора неявно прокидывает исключение дальше**. И привет всем возможным проблемам с исключениями из деструкторов, в том числе нарушению неявного `noexcept(true)`. 100 | 101 | Однако, в отличие от конструкторов, для деструкторов добавили возможность подавить неявное пробрасывание пойманного исключения. Для этого нужно всего лишь... добавить `return`! 102 | 103 | ```C++ 104 | struct DctorThrowTry { 105 | ~DctorThrowTry() try { 106 | throw std::runtime_error("err"); 107 | } catch (const std::exception& e) { 108 | std::cout << e.what() << "\n"; 109 | return; // исключение не будет перевыброшено! 110 | } 111 | }; 112 | ``` 113 | 114 | Удивительно, но таким образом в C++ есть случай, в котором `return` последней командой в void-функции меняет ее поведение. 115 | 116 | Также нужно добавить, что в catch блоке деструкторов и конструкторов нельзя обращаться к нестатическим полям и методам класса — будет неопределенное поведение. По понятным причинам. В момент входа в `catch` блок они все уже мертвы. 117 | 118 | ```C++ 119 | struct S { 120 | A a; 121 | B b; 122 | 123 | S() try { 124 | ... 125 | } catch (...) { 126 | do_something(a); // UB! 127 | } 128 | 129 | ~S() try { 130 | ... 131 | } catch (...) { 132 | do_something(b); // UB! 133 | return; 134 | } 135 | }; 136 | 137 | // Но при этом 138 | 139 | bool fun(T1 a, T2 b) try { 140 | ... 141 | return true; 142 | } catch (...) { 143 | // важно: этот блок не ловит исключения, возникающие при инициализации a и b 144 | do_something(a); // Ok! 145 | return false; 146 | } 147 | ``` 148 | 149 | **Итого** 150 | 151 | 1. Для обычных функций и `main()` с помощью альтернативного синтаксиса можно удобно и красиво перехватывать все исключения, которые могли бы вылететь. И поведение по умолчанию — именно перехват. [Дальше не летит.](https://godbolt.org/z/eYGevjeco) 152 | 2. Для конструкторов можно ловить исключения из конструкторов полей, обрабатывать их (печатать в лог), но подавить нельзя. Либо кидаете свое новое исключение, либо пойманное неявно будет проброшено дальше. 153 | 3. Для деструкторов также будет неявный проброс, но его можно подавить, добавив `return`. 154 | 155 | Если что, в Rust нет исключений (но есть очень похожие паники). Живите с этим. 156 | 157 | ### Полезные ссылки 158 | 1. https://en.cppreference.com/w/cpp/language/function-try-block 159 | 2. https://mariusbancila.ro/blog/2019/03/13/little-known-cpp-function-try-block/ -------------------------------------------------------------------------------- /syntax/implicit_bool.md: -------------------------------------------------------------------------------- 1 | # Неявное приведение к bool 2 | 3 | Вы пишете новую восхитительную библиотеку сериализации в JSON. Для этого у вас уже написано много своих версий функции `stringify` для поддерживаемых JSON типов. Их там немного... 4 | 5 | И вот у вас есть 6 | 7 | ```C++ 8 | auto stringify(bool b) -> std::string_view { 9 | return b ? "true" : "false"; 10 | } 11 | 12 | auto stringify(std::string_view s) -> std::string_view { 13 | return s; 14 | } 15 | ``` 16 | 17 | Выглядит хорошо и логично. 18 | 19 | Вы тестируете эти функции 20 | 21 | ```C++ 22 | int main() { 23 | std::cout << stringify(true) << "\n"; 24 | std::cout << stringify("string") << "\n"; 25 | } 26 | ``` 27 | И [получаете](https://gcc.godbolt.org/z/hjE1Kzvxf) 28 | ``` 29 | true 30 | true 31 | ``` 32 | 33 | Удивлены? Но тут нет ничего удивительного! Просто строковый литерал, который имеет тип `const char[7]` неявно приводится к `const char*`, который неявно приводится к `bool`. А поскольку это все built-in преобразования они имеют приоритет перед user-defined преобразованием к `std::string_view` через его конструктор. 34 | 35 | 36 | C неявным приведением указателей к `bool` есть еще известный дефект в инициализаторе через фигурные скобки 37 | 38 | ```C++ 39 | bool array[5] = {true, false, true, false, true}; 40 | std::vector vector {array, array + 5}; 41 | std::cout << vector.size() << "\n"; 42 | ``` 43 | [Будет выведено](https://gcc.godbolt.org/z/jobeh6) 2, а не 5. Потому что указатели неявно приводятся к `bool`! 44 | Дефект кое-как [исправили](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1957r2.html) в C++20. [Теперь Clang отказывается это компилировать, а GCC просто выдает предупреждение](https://gcc.godbolt.org/z/h3YzzEMz3). 45 | 46 | ``` 47 | :9:32: error: type 'bool[5]' cannot be narrowed to 'bool' in initializer list [-Wc++11-narrowing] 48 | 9 | std::vector vector { array, array + 5}; 49 | | ^~~~~ 50 | :9:32: note: insert an explicit cast to silence this issue 51 | 9 | std::vector vector { array, array + 5}; 52 | | 53 | ``` 54 | 55 | ------- 56 | 57 | А вы знаете как определить для вашего типа все возможные арифметические операторы и операторы сравнения разом? Нужно всего лишь определить неявный оператор приведения к `bool`, конечно же! 58 | 59 | ```C++ 60 | struct OptionalPositive { 61 | int x; 62 | 63 | operator bool() const { 64 | return x >= 0; 65 | } 66 | }; 67 | 68 | int main() { 69 | std::cout << 5 + OptionalPositive { 5 }; 70 | std::cout << (5 < OptionalPositive { 5 }); 71 | std::cout << (5 == OptionalPositive { 5 }); 72 | std::cout << (5 * OptionalPositive { 5 }); 73 | } 74 | ``` 75 | Это компилируется и выдает результат `6005`. Потому как выполняется user-defined неявное приведенине к `bool`, который далее неявно приводится к `int`. Все правильно. 76 | 77 | Последние версии Clang хотя бы вывают частично [предупреждения](https://gcc.godbolt.org/z/fs98G3o3f) 78 | ``` 79 | :13:21: warning: result of comparison of constant 5 with expression of type 'bool' is always false [-Wtautological-constant-out-of-range-compare] 80 | 13 | std::cout << (5 < OptionalPositive { 5 }); 81 | | ~ ^ ~~~~~~~~~~~~~~~~~~~~~~ 82 | :14:21: warning: result of comparison of constant 5 with expression of type 'bool' is always false [-Wtautological-constant-out-of-range-compare] 83 | 14 | std::cout << (5 == OptionalPositive { 5 }); 84 | | 85 | ``` 86 | Правда, если поменять тип константы слева на `double`, в Clang 19. предупреждение исчезнет. Но компилироваться оно [не перестанет](https://gcc.godbolt.org/z/MhafPcTvv). 87 | 88 | Никогда, если только у вас не C++98, не определяйте неявный `operator bool`! Он всегда должен быть `explicit`. Если вы боитесь, что это заставит вас делать `static_cast` там, где этого не хочется делать, то не переживайте! 89 | С++ определяет несколько контекстов, в которых `explicit operator bool` все равно может быть вызван неявно: в условиях `if`, `for` и `while`, а также в логических операциях. Этого достаточно для большинства использнований `operator bool`. 90 | 91 | Если у вас C++98... Я вам очень соболезную. Но и даже в вашем печальном случае есть решение. Чудовищно громоздкое, но решение — можете ознакомиться с устаревшей [Safe Bool Idiom](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool) в свободное время в качестве домашнего задания. Если коротко, вместо `operator bool` предгалалось определить 92 | 93 | ```C++ 94 | // Указатель на метод в приватном классе! 95 | typedef void (SomePrivateClass::*bool_type) () const; 96 | operator bool_type(); // неявное приведение к этому указателю 97 | ``` 98 | И тогда бы ваш объект в условных операциях неявно приводился бы к указателю, а указатель бы далее неявно приводился к `bool`. -------------------------------------------------------------------------------- /syntax/missing_return.md: -------------------------------------------------------------------------------- 1 | # Забытый `return` 2 | 3 | Про C/C++ иногда говорят, что это языки, в которых есть специальный синтаксис для написания невалидных программ. 4 | 5 | В C/C++ в функции, возвращающей что-то, отличное от `void`, необязательно должен быть `return что-то`. 6 | 7 | ```C++ 8 | int add(int x, int y) { 9 | x + y; 10 | } 11 | ``` 12 | 13 | Это синтаксически корректная функция, которая приведет к неопределенному поведению. Может быть мусор, может быть провал в код следующей далее по коду функции, а может быть и [«все нормально»](https://gcc.godbolt.org/z/6Y4T66). 14 | 15 | А с современными версиями компиляторов, например, Clang 18, можно получить совершенно безумный по своей красоте [результат](https://godbolt.org/z/Y1qo49Tnn) сравнимый с работой популярных ИИ-ассистентов 16 | 17 | ```C++ 18 | __attribute__((noinline)) 19 | bool is_even(int x) { 20 | switch (x) { 21 | case 0: return true; 22 | case 1: return false; 23 | case 2: return true; 24 | case 3: return false; 25 | // Undefined behaviour, do your job! 26 | } 27 | } 28 | 29 | int main() { 30 | std::cout << is_even(19) << "\n"; 31 | std::cout << is_even(200) << "\n"; 32 | } 33 | ``` 34 | 35 | ``` 36 | # Сгенерированный код совершенно точно делает то, что мы и подразумевали! 37 | is_even(int): # @is_even(int) 38 | test dil, 1 39 | sete al 40 | ret 41 | ``` 42 | 43 | 44 | Особенную боль недоразумение с забытым `return` может доставить тем, кто пришел в C++ после какого-нибудь ориентированного на выражения языка, в котором похожий код абсолютно нормален: 45 | 46 | ```Rust 47 | fn add(x: i32, y: i32) -> i32 { 48 | x + y 49 | } 50 | ``` 51 | 52 | Обоснования, почему не обязательно писать в конце функции `return`, следующие: 53 | 1. В функции может быть ветвление логики. В одной из веток может вызываться код, который не предполагает возврата: бесконечный цикл, исключение, `std::exit`, `std::longjmp` или что-то иное, помеченное аттрибутом `[[noreturn]]`. Проверить на наличие такого кода не всегда возможно. 54 | 2. Функция может содержать ассемблерную вставку со специальным кодом финализации и инструкцией `ret`. 55 | 56 | Проверить наличие формального `return`, конечно, можно. Но нам разрешили не писать иногда (очень иногда!) чисто формальную строчку, а компиляторам разрешили не считать это ошибкой. 57 | 58 | ------ 59 | 60 | С флагом `-Wreturn-type` GCC и clang во многих случаях сообщают о проблеме. 61 | 62 | ----- 63 | Единственным исключением, начиная с C++11, является функция `main`. В ней отсутствующий `return` к неопределенному поведению не приводит и трактуется как возврат 0. 64 | 65 | ## Полезные ссылки 66 | 1. https://stackoverflow.com/questions/1610030/why-does-flowing-off-the-end-of-a-non-void-function-without-returning-a-value-no 67 | 2. https://en.cppreference.com/w/cpp/language/attributes/noreturn 68 | -------------------------------------------------------------------------------- /syntax/most_vexing_parse.md: -------------------------------------------------------------------------------- 1 | # Most Vexing Parse 2 | 3 | Помимо неопределенного поведения, в C++ есть неожиданное поведение, 4 | произрастающее из следующих фантастических возможностей языка. 5 | 6 | Пользовательские типы и функции можно *объявлять* [где попало и как попало](https://godbolt.org/z/MWszrj). 7 | 8 | ```C++ 9 | template 10 | struct STagged {}; 11 | 12 | 13 | using S1 = STagged; // преобъявление струкруты Tag1 14 | using S2 = STagged; // преобъявление струкруты Tag2 15 | 16 | void fun(struct Tag3*); // предобъявление структуры Tag3 17 | 18 | void external_fun() { 19 | int internal_fun(); // предобъявление функции! 20 | internal_fun(); 21 | } 22 | 23 | int internal_fun() { // определение предобъявленой функции 24 | std::cout << "hello internal\n"; 25 | return 0; 26 | } 27 | 28 | int main() { 29 | external_fun(); 30 | } 31 | ``` 32 | 33 | При этом *определять* сущности можно не везде. 34 | Типы можно определять локально — внутри функции. А функции определять нельзя. 35 | 36 | ```C++ 37 | void fun() { 38 | struct LocalS { 39 | int x, y; 40 | }; // OK 41 | 42 | void local_f() { 43 | std::cout << "local_f"; 44 | } // Compilation Error 45 | } 46 | ``` 47 | 48 | И все могло бы быть хорошо, если бы не одно: в C++ есть конструкторы, вызов которых [похож](https://godbolt.org/z/h6zTor) на объявление функции 49 | 50 | ```C++ 51 | struct Timer { 52 | int val; 53 | explicit Timer(int v = 0) : val(v) {} 54 | }; 55 | 56 | struct Worker { 57 | int time_to_work; 58 | 59 | explicit Worker(Timer t) : time_to_work(t.val) {} 60 | 61 | friend std::ostream& operator << (std::ostream& os, const Worker& w) { 62 | return os << "Time to work=" << w.time_to_work; 63 | } 64 | }; 65 | 66 | int main() { 67 | // ЭТО НЕ ВЫЗОВ КОНСТРУКТОРА! 68 | Worker w(Timer()); // предобъявление функции, которая возвращает Worker и принимает функцию, возвращающую Timer и не принимающую ничего! 69 | 70 | std::cout << w; // имя функции неявно преобразуется к указателю, который неявно преобразуется к bool 71 | // будет выведено 1 (true) 72 | } 73 | ``` 74 | 75 | Подобная ошибка может быть труднообнаружима, если случайно предобъявленная функция используется в контексте приведения к `bool` или если объект, который хотели сконструировать, сам является вызываемым (у него перегружен `operator()`). 76 | 77 | Может показаться, что виноват именно конструктор по умолчанию класса `Timer`. На самом деле, виноват C++. 78 | В нем можно объявлять функции вот так: 79 | 80 | ```C++ 81 | void fun(int (val)); // скобки вокруг имени параметра допустимы! 82 | ``` 83 | 84 | И потому получать [более отвратительный](https://godbolt.org/z/dhz6nK) и труднопонимаемый вариант ошибки: 85 | 86 | ```C++ 87 | int main() { 88 | const int time_to_work = 10; 89 | Worker w(Timer(time_to_work)); // предобъявление функции, которая возвращает Worker 90 | // и принимает параметр типа Timer. time_to_work — имя этого параметра! 91 | 92 | std::cout << w; 93 | } 94 | ``` 95 | 96 | Clang способен предепреждать о подобном. 97 | 98 | С++11 и новее предлагают *universal initialization* (через `{}`), которая не совсем *universal* и имеет свои проблемы. 99 | C++20 предлагает еще одну *universal* инициализацию, но уже снова через `()`... 100 | 101 | Избежать проблемы можно, используя *Almost Always Auto* подход с инициализацией вида 102 | `auto w = Worker(Timer())`. Круглые или фигурные скобки здесь — не так важно (хотя, на самом деле, важно, но в другой ситуации). 103 | 104 | Возможно, когда-нибудь объявление функций в старом сишном стиле запретят в пользу 105 | *trailing return type* (`auto fun(args) -> ret`). И вляпаться в рассмотренную прелесть станет значительно сложнее (но все равно [можно](https://www.youtube.com/watch?v=tsG95Y-C14k)!). 106 | 107 | 1. https://www.fluentcpp.com/2018/01/30/most-vexing-parse/ 108 | 2. http://cginternals.github.io/guidelines/articles/almost-always-auto/ 109 | -------------------------------------------------------------------------------- /syntax/move.md: -------------------------------------------------------------------------------- 1 | # Семантика перемещения 2 | 3 | Начиная с C++11, у нас есть rvalue-ссылки и семантика перемещения. Причем 4 | перемещение не деструктивно: исходный объект остается жив, что порождает 5 | множество ошибок. 6 | Еще есть проблемы с тем, как избегать накладных расходов при использовании 7 | перемещаемых объектов, но с этим можно жить. 8 | 9 | ## Накладные расходы 10 | 11 | Несмотря на все громкие заявления, абстракции в C++ имеют далеко не нулевую стоимость. 12 | Занятным примером является `std::unique_ptr`, завязанный на семантику перемещения. 13 | 14 | ```C++ 15 | void run_task(std::unique_ptr ptask) { 16 | // do something 17 | ptask->go(); 18 | } 19 | 20 | void run(...){ 21 | auto ptask = std::make_unique(...); 22 | ... 23 | run_task(std::move(ptask)); 24 | } 25 | ``` 26 | При вызове `run_task` параметр передается по значению: создается новый объект `unique_ptr`, а старый останется, 27 | но окажется пустым. Раз два объекта, то и два вызова деструктора. С деструктивной 28 | семантикой перемещения (например, в Rust) вызов деструктора будет только один. 29 | 30 | Можно исправить ситуацию — передать по rvalue-ссылке: 31 | 32 | ```C++ 33 | void run_task(std::unique_ptr&& ptask) { 34 | // do something 35 | ptask->go(); 36 | } 37 | ``` 38 | 39 | Тогда дополнительного объекта не будет. И произойдет только один вызов деструктора. 40 | При этом, из-за ссылки, имеется дополнительный уровень индирекции и обращение к памяти. 41 | 42 | Но самое главное: никакого перемещения [на самом деле не будет](https://godbolt.org/z/4bbrh1), что может скрыть ошибку в логике программы: 43 | 44 | ```C++ 45 | void consume_v1(std::unique_ptr p) {} 46 | void consume_v2(std::unique_ptr&& p) {} 47 | 48 | void test_v1(){ 49 | auto x = std::make_unique(5); 50 | consume_v1(std::move(x)); 51 | assert(!x); // ok 52 | } 53 | 54 | void test_v2(){ 55 | auto x = std::make_unique(5); 56 | consume_v2(std::move(x)); 57 | assert(!x); // fire! 58 | } 59 | ``` 60 | 61 | И мы переходим к основной проблеме. 62 | 63 | ## Use-after-move 64 | 65 | Во-первых, функция `std::move` ничего не делает. 66 | Это всего лишь явное преобразование lvalue-ссылки в rvalue. Оно никак не влияет на состояние объкта. 67 | Обозреваемые эффекты от перемещения могут давать функции, работающие с этой самой rvalue-ссылкой. В основном это конструкторы и операторы перемещения. 68 | 69 | Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение. 70 | Оно должно быть валидным в смысле вызова деструктора. Но более ничего не требуется. Объект не обязан быть пустым после перемещения. Его поля не обязаны быть зануленными. Так у `std::thread` после перемещения нельзя вызывать ни один из методов. А `std::unique_ptr` гарантированно становится пустым (`nullptr`). 71 | 72 | Чаще всего и проще всего натолкнуться на use-after-move можно при реализации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам. 73 | 74 | ```C++ 75 | struct Person { 76 | public: 77 | Person(std::string first_name, 78 | std::string last_name) : first_name_(std::move(first_name)), 79 | last_name_(std::move(last_name)) { 80 | std::cerr << first_name; // wrong, use-after-move 81 | } 82 | private: 83 | std::string first_name_; 84 | std::string last_name_; 85 | }; 86 | ``` 87 | 88 | Конечно, в таком случае ошибка будет быстро найдена — для `std::string` есть гарантия, что после перемещения объект окажется пустым. Но если сделать конструктор шаблонным и передавать в него тривиально перемещаемые типы, ошибка долго может не проявляться. 89 | 90 | ```C++ 91 | template 92 | Person(T1 first_name, 93 | T2 last_name) : first_name_(std::move(first_name)), 94 | last_name_(std::move(last_name)) { 95 | std::cerr << first_name; // wrong, use-after-move 96 | } 97 | ... 98 | 99 | Person p("John", "Smith"); // T1, T2 = const char* 100 | ``` 101 | 102 | Другой интересный случай использования после перемещения — self-move-assignment. 103 | В результате которого из объекта могут внезапно пропадать данные. А могут и не пропадать. В зависимости от того, как 104 | реализовали перемещение для конкретного типа. 105 | 106 | Так, например, вот такая наивная реализация алгоритма `remove_if` содержит ошибку: 107 | 108 | ```C++ 109 | template 110 | void remove_if(std::vector& v, P&& predicate) { 111 | size_t new_size = 0; 112 | for (auto&& x : v) { 113 | if (!predicate(x)) { 114 | v[new_size] = std::move(x); // self-move-assignment! 115 | ++new_size; 116 | } 117 | } 118 | v.resize(new_size); 119 | } 120 | ``` 121 | 122 | [Ошибка](https://godbolt.org/z/qY5MMn) не даст о себе знать до тех пор, пока элементы контейнера не будут содержать 123 | полей, не учитывающих возможность самоприсваивания. 124 | 125 | ```C++ 126 | struct Person { 127 | std::string name; 128 | int age; 129 | }; 130 | 131 | std::vector persons = { 132 | Person { "John", 30 }, Person { "Mary", 25 } 133 | }; 134 | remove_if(persons, [](const Person& p) { return p.age < 20; }); 135 | 136 | for (const auto& p : persons){ 137 | std::cout << p.name << " " << p.age << "\n"; // все name пустые! 138 | } 139 | ``` 140 | 141 | Отследить использование после перемещения способны некоторые статические анализаторы. 142 | Для clang-tidy тоже [есть проверки](https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-after-move.html). 143 | 144 | Если вы реализуете перемещаемые классы и хотите учесть возможность самоприсваивания/самоперемещения, либо используйте [идиому copy/move-and-swap](https://mropert.github.io/2019/01/07/copy_swap_20_years/), либо не забывайте проверить совпадение адресов текущего и перемещаемого объектов: 145 | 146 | ```C++ 147 | MyType& operator=(MyType&& other) noexcept { 148 | if (this == std::addressof(other)) { // addressof сработает, 149 | // если у вас перегружен & 150 | return *this; 151 | } 152 | ... 153 | } 154 | ``` 155 | 156 | 157 | ## Полезные ссылки 158 | 1. https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-after-move.html 159 | 2. https://youtu.be/rHIkrotSwcc?t=1065 160 | 3. https://stackoverflow.com/questions/7027523/what-can-i-do-with-a-moved-from-object 161 | 4. https://herbsutter.com/2020/02/17/move-simply/ 162 | 5. https://mropert.github.io/2019/01/07/copy_swap_20_years/ -------------------------------------------------------------------------------- /syntax/multidimensional_subscript.md: -------------------------------------------------------------------------------- 1 | # Многомерный operator[] 2 | 3 | C++23 подарил нам долгожданную возможность перегружать `operator[]` с более чем одним параметром. Любители матриц и numpy ликуют. C++ очень похорошел за последние годы! 4 | 5 | Вместе с долгожданной фичей разумеется поставляются новые грабли, которые любезно разложены под разработчиков крупных проектов со смесью стандартов разных версий, от которых бизнес требует релизить фичи как можно быстрее, так что на предупреждения компилятора они могут иногда и подзабить... 6 | 7 | У вас была библиотека с классом матриц, поддерживающих обращение к отдельным строкам 8 | 9 | ```C++ 10 | 11 | 12 | #if __has_include() && __cplusplus >= 202002L 13 | #include 14 | 15 | template 16 | using Span = std::span; 17 | 18 | #else 19 | 20 | template 21 | struct Span { 22 | T* data; 23 | size_t size; 24 | 25 | auto begin() { return data; } 26 | auto end() { return data + size; } 27 | 28 | auto subspan(size_t ofs, size_t len) { 29 | return Span { 30 | data + ofs, 31 | len 32 | }; 33 | } 34 | }; 35 | 36 | #endif 37 | 38 | struct Row { 39 | Span data; 40 | 41 | void operator = (int c) { 42 | for (auto& x: data) x = c; 43 | } 44 | }; 45 | 46 | struct Matrix { 47 | std::vector data; 48 | size_t cols; 49 | 50 | // Эта перегрузка была у вас 20 лет 51 | Row operator[](size_t row_idx) { 52 | return { 53 | Span{data.data(), data.size()} 54 | .subspan(row_idx * cols, cols) 55 | }; 56 | } 57 | 58 | // И вот месяц назад вы добавили восхитетельную 59 | // перегрузку для доступа к элементу. 60 | // Библиотека используется с разными версиями C++, так что 61 | // перегрузка под feature-control флагом -- все отлично 62 | #ifdef __cpp_multidimensional_subscript 63 | int& operator[](size_t row_idx, size_t col_idx) { 64 | return data[row_idx * cols + col_idx]; 65 | } 66 | #endif 67 | }; 68 | ``` 69 | 70 | [Some horrible misguided fool](https://x.com/ericniebler/status/1734997577274380681) из соседней команды использует вашу библиотеку и пишет свой прикладной модуль на C++23, не сильно задумываясь о feature-flags стандартов (вы удивитесь, но это невероятно распространенный сценарий!). И он совершает чудовищную ошибку: помещает определение функции в заголовочный файл 71 | 72 | ```C++ 73 | auto compute() -> Matrix { 74 | Matrix m { 75 | std::vector(12, 0), 76 | 4, // matrix 3 x 4 77 | }; 78 | 79 | for (int i = 0; i < 3; ++i) { 80 | for (int j = 0; j < 4; ++j) { 81 | m[i, j] = i * j; 82 | } 83 | } 84 | return m; 85 | } 86 | ``` 87 | 88 | У него [все работает, все отлично](https://godbolt.org/z/hsfcTaqse). 89 | 90 | К ниму приходит коллега из еще одной команды и берет его пакет к себе, в проект с C++17 и вызывает функцию `compute()` 91 | 92 | У него все компилируется без каких-либо предупреждений. Запускается. Иногда работает. А иногда валится с [segmentation fault](https://godbolt.org/z/hTWjqE3WM). 93 | 94 | Они идут смотреть вывод санитайзеров и valgrind, смотреть core dump, подключаться отладчиком и развлекаться прочими интересными способами, 95 | чтобы обнаружить: 96 | 97 | ``` 98 | ================================================================= 99 | ==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x604000000040 at pc 0x000000401923 bp 0x7ffe36f379e0 sp 0x7ffe36f379d8 100 | WRITE of size 4 at 0x604000000040 thread T0 101 | #0 0x401922 in Row::operator=(int) /app/example.cpp:38 102 | #1 0x401922 in compute() /app/example.cpp:68 103 | #2 0x40116b in main /app/example.cpp:75 104 | #3 0x749978829d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 962015aa9d133c6cbcfb31ec300596d7f44d3348) 105 | #4 0x749978829e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 962015aa9d133c6cbcfb31ec300596d7f44d3348) 106 | #5 0x401274 in _start (/app/output.s+0x401274) (BuildId: 18a2ccdd2845a7b183f1249366db2569b4a1d038) 107 | 108 | ``` 109 | 110 | Ну да, ведь до C++23 была только одна перегрузка `operator[]` 111 | и в строке `m[i, j] = i * j;` вместо многомерного оператора вызывается одномерный, в который передается результат `operator ,`, то есть второй элемент. 112 | Вот и приплыли с выходом за границы буфера, если у матрицы столбцов больше чем строк. Не говоря уже о том, что код в принципе теперь сломан и делает что-то другое. 113 | 114 | Ах да. Обещанное предупреждения компилятора... Есть предупреждение, да. Но по умолчанию только в [C++20](https://godbolt.org/z/nq4eenaaM). 115 | 116 | ``` 117 | : In function 'Matrix compute()': 118 | :68:16: warning: top-level comma expression in array subscript is deprecated [-Wcomma-subscript] 119 | 68 | m[i, j] = i * j; 120 | ``` 121 | 122 | В C++17 и ранее нет. А c `-Wall` есть, но другое 123 | 124 | ``` 125 | :68:15: warning: left operand of comma operator has no effect [-Wunused-value] 126 | 68 | m[i, j] = i * j; 127 | ``` 128 | 129 | Но `-Wunused-value` бывает слишком "шумным", особенно в легаси проектах, и его часто отключают. 130 | 131 | Веселого вам перехода с C++17 на C++23, минуя C++20! 132 | 133 | 134 | -------------------------------------------------------------------------------- /what_is_ub.md: -------------------------------------------------------------------------------- 1 | # Что такое неопределенное поведение и как оно проявляется 2 | 3 | Неопределенное поведение (undefined behavior, UB) — это удивительная особенность некоторых языков программирования, 4 | позволяющая написать синтаксически корректную программу, работающую совершенно непредсказуемо при переносе ее с одной платформы на другую, изменении опций компиляции/интерпретации, и замене одного компилятора/интерпретатора другим. И главное — помимо синтаксической корректности, программа **выглядит** корректной семантически. 5 | 6 | Состоит эта особенность в том, что в спецификации языка программирования сознательно 7 | не определяют поведение программы в каких-то особых условиях. Делается это из соображений производительности: не надо генерировать дополнительные инструкции с проверками. Или из соображений обеспечения гибкости при реализации каких-то фич. 8 | В спецификации пишут просто: «Если код делает что-то нехорошее, то поведение не определено». Например: 9 | - Если обратиться по нулевому указателю, поведение не определено. 10 | - Если дважды захватить блокировку в одном и том же потоке, поведение не определено. 11 | - Если поделить на ноль, поведение не определено. 12 | - Если прочитать неинициализированную память, поведение не определено. 13 | - И так далее, и тому подобное. 14 | 15 | Важно, что это «поведение не определено» означает, что произойти может что угодно: форматирование диска, ошибка компиляции, исключение, а может и все будет хорошо. Никаких гарантий не дается. Отсюда и происходят веселые, неожиданные и очень печальные 16 | в production-коде последствия. 17 | 18 | И, конечно же, именно C и C++ наиболее печально известны своим неопределенным поведением. 19 | Однако надо понимать, что эта особенность присуща и другим языкам. Во многих языках можно найти какой-нибудь редкий особенный пример с неопределенным поведением. Но именно в C и C++ оно встречается при написании почти любой программы. Слишком много фич языка содержат пункты с неопределенным поведением. 20 | 21 | ---- 22 | 23 | И так, по каким же признакам можно заподозрить UB в программе и насколько неопределенное поведение действительно неопределенное? 24 | 25 | Когда-то давно UB в коде могло повлечь действительно что угодно. Например, `gcc 1.17` [начинал запускать игрушечки](https://feross.org/gcc-ownage/). 26 | 27 | Сегодня, если вы поделите что-то на ноль, подобного почти наверное не произойдет. Однако неприятности все же бывают разные: 28 | 29 | 1. Для данной конкретной платформы и компилятора в документации сказано что именно произойдет, несмотря на страшные слова «_undefined behavior_» в стандарте. И все будет хорошо. Вы знаете что делаете. Никакой неопределенности. Все классно. 30 | 2. UB при работе с памятью чаще всего заканчиваются ошибкой сегментации и получением прекрасного сигнала SIGSEGV от операционной системы. Программа падает. 31 | 3. Программа работает и штатно завершается. Но дает разные или неадекватные результаты от запуска к запуску. Также результаты меняются от сборки к сборке при изменении опций компилятора или самого компилятора. Никаких генераторов случайных чисел вы не использовали. 32 | 4. Программа ведет себя неправильно, несмотря на то, что в коде наставлено огромное множество проверок, `assert`'ов, `try-catch` блоков, каждый из которых «подтверждает» что все корректно. В отладчике видно, что вычисления идут корректно, но совершенно внезапно все ломается. 33 | 5. Программа выполняет код, который в ней есть, но не вызывался. Отрабатывают ни разу не вызываемые функции. 34 | 6. Компилятор «без причины» и без падения отказывается собирать код. Линковщик выдает «невозможные и бессмысленные» ошибки. 35 | 7. Проверки в коде перестают исполняться. Под отладчиком видно, что исполнение не заходит внутрь веток `if` или `catch`, хотя по значениям переменных заход должен быть выполнен. 36 | 8. Внезапный необоснованный вызов `std::terminate`. 37 | 9. Бесконечные циклы становятся конечными и наоборот. 38 | 39 | --- 40 | 41 | С неопределенным поведением часто путают другие понятия. 42 | 1. Еще одна страшная аббревиатура UB — неуточненное (_unspecified_) поведение. Стандарт не уточняет, что именно может произойти, но описывает варианты. Так, например, порядок вычисления аргументов функции — поведение неуточненное. 43 | 2. Поведение, определяемое реализацией (_implementation-defined_) — надо смотреть документацию для вашей платформы и вашего компилятора. 44 | 3. Ошибочное поведения (_erroneous_) — новинка C++26. Часть неопределенного поведения будет, возможно, переквалифицированна в эту категорию. Например, так поступили с чтением неинициализированных переменных. Разница с неопределенным — компилятору *очень рекомендуется* выдавать диагностики и запрещается выполнять умные оптимизации с неожиданными побочными эффектами. 45 | 46 | Эта тройка намного лучше неопределенного, хотя и имеет с ним одну общую черту: программа, полагающаяся на любое из них, вообще говоря, непереносима. 47 | 48 | Также выделяют два класса неопределенного поведения: 49 | 50 | - Неопределенное поведение на уровне библиотеки 51 | (*Library Undefined Behavior*): Вы сделали что-то что не предусматривается конкретной библиотекой (в том числе и стандартной, но не всегда). Например, библиотека [gmock](https://google.github.io/googletest/gmock_for_dummies.html) под страхом неопределенного поведения не допускает донастраивать mock-объект после начала его использования. 52 | - Неопределенное поведение на уровне языка (*Language Undefined Behavior*): Вы сделали что-то что фундаментально не определено спецификацией языка программирования. Например, разыменовани нулевой указатель. 53 | 54 | Если вы столкнулись с первым — у вас проблемы, но если работает, то с очень большим шансом и продолжит работать пока вы не обновите библиотеку/не смените платформу. А побочные эффекты часто могут быть лишь локальными. Очень похоже на implementation defined поведение. 55 | 56 | Если вы столкнулись со вторым — у вас большие проблемы. Код может перестать работать корректно совершенно внезапно при малейших изменениях. А также могут быть серьезные угрозы безопасности для пользователей вашего приложения. 57 | 58 | ## Полезные ссылки 59 | 1. https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior 60 | --------------------------------------------------------------------------------