diff --git a/README.md b/README.md index a3956ef..7a20f89 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ -------------- -"Преждевременная оптимизация -- корень всех зол" (Д. Кнут или Э. Хоар -- в зависимости от того, какой источник смотрите). Язык С++, пожалуй, наиболее яркая тому демонстрация: огромное количество ошибок в C++ программах свзязаны +"Преждевременная оптимизация — корень всех зол" (Д. Кнут или Э. Хоар — в зависимости от того, какой источник смотрите). Язык С++, пожалуй, наиболее яркая тому демонстрация: огромное количество ошибок в C++ программах свзязаны с неопределенным поведением, заложенным в фундаменте языка просто для того, чтобы дать простор оптимизациям на этапе компиляции. Если вы собираетесь писать на C++ код, в работоспособности которого хотите быть хоть немного уверенными, стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка, его библиотеке, и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая. @@ -36,7 +36,7 @@ 1. [Переполнение знаковых целых чисел](numeric/overflow.md) 2. [Числа с плавающей точкой](numeric/floats.md) 3. Нарушения lifetime объектов - 1. [Висячие ссылки -- общие случаи](lifetime/use_after_free_in_general.md) + 1. [Висячие ссылки — общие случаи](lifetime/use_after_free_in_general.md) 2. [Автовывод типов и висячие ссылки](lifetime/decltype_auto_and_explicit_types.md) 3. [std::string_view](lifetime/string_view.md) 4. [Range-based for](lifetime/for_loop.md) diff --git a/how_to_find_ub.md b/how_to_find_ub.md index 5ea167a..65dffd1 100644 --- a/how_to_find_ub.md +++ b/how_to_find_ub.md @@ -2,7 +2,7 @@ Очень частный вопрос, который задавали мне, задавал я сам себе и другим. Да и каждый C++ разработчик, к сожалению, должен его задавать. -Ответ на него в общем случае -- никак. Это алгоритмически неразрешимая задача практически ничем не отличающаяся от задачи останова. Но программистов как палками ни гоняй, все равно будут решать неразрешимые задачи, так что +Ответ на него в общем случае — никак. Это алгоритмически неразрешимая задача практически ничем не отличающаяся от задачи останова. Но программистов как палками ни гоняй, все равно будут решать неразрешимые задачи, так что для конкретного кода и для конкретных входных данных иногда есть способы дать ответ. ---- @@ -63,7 +63,7 @@ static_assert((div_test(A, B), true)); // Compliation error, zero division ---- -Тесты, различные сборки, статический и динамический анализ --- способы немного поднять уверенность в том, что в вашем коде нет UB. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять. И даже этого может быть недостаточно. +Тесты, различные сборки, статический и динамический анализ — способы немного поднять уверенность в том, что в вашем коде нет UB. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять. И даже этого может быть недостаточно. Еще есть путь отключения каких-либо оптимизаций флагами компилятора, флаги включащие различные нарушения стандарта (знаменитый `-fpermissive`), превращающие язык C++ во что-то совершенно иное. Но призываю вас никогда не идти этим путем. Ваш код станет непереносимым. Ваш код перестанет быть кодом на C++. Лучше сразу возьмите другой язык программирования. diff --git a/lifetime/decltype_auto_and_explicit_types.md b/lifetime/decltype_auto_and_explicit_types.md index b041a22..7e51963 100644 --- a/lifetime/decltype_auto_and_explicit_types.md +++ b/lifetime/decltype_auto_and_explicit_types.md @@ -21,9 +21,9 @@ auto find(const std::vector& v, const T& x) { --- ## Проблема явного указания типа -Длинно и много писать -- решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах. +Длинно и много писать — решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах. -И все могло быть хорошо: поменяли где-то в объявлении, получили ошибки компиляции -- исправили во всех местах, на которые указали ошибки. Но в C++ есть неявное приведение типов, которое особенно жестоко наказывает при использовании ссылок. +И все могло быть хорошо: поменяли где-то в объявлении, получили ошибки компиляции — исправили во всех местах, на которые указали ошибки. Но в C++ есть неявное приведение типов, которое особенно жестоко наказывает при использовании ссылок. ```C++ std::map counters = { {"hello", 5}, {"world", 5} }; @@ -41,14 +41,14 @@ for (std::string_view k : keys) { } ``` -Мы немного ошиблись в аргументе лямбда-функции и получили ссылку на временный объект, а вместе с ней -- [неопределенное поведение](https://godbolt.org/z/EKcob3). +Мы немного ошиблись в аргументе лямбда-функции и получили ссылку на временный объект, а вместе с ней — [неопределенное поведение](https://godbolt.org/z/EKcob3). [Исправляем](https://godbolt.org/z/E6evof) ошибку, добавляя `const` перед `string`: ```C++ [](const std::pair& item) -> std::string_view ``` -Проходят недели, код рефакторится. `counters` отъезжают в поле какого-нибудь класса. Получение и обработка ключей -- в его второстепенный метод. А потом внезапно выясняется, что тип значений в ассоциативном массиве надо бы поменять на меньший -- пусть `short`. +Проходят недели, код рефакторится. `counters` отъезжают в поле какого-нибудь класса. Получение и обработка ключей — в его второстепенный метод. А потом внезапно выясняется, что тип значений в ассоциативном массиве надо бы поменять на меньший — пусть `short`. Вы меняете его. Уже не помните про метод обработки ключей. Компилятор не ругается. Запускаете тестирование и возвращаетесь к той же самой [ошибке](https://godbolt.org/z/7Mzcs3). @@ -71,7 +71,7 @@ public: } ``` Опять ошиблись. Опять надо исправлять. -`std::map` может в будущем поменяться на что-то другое, у чего итератор возвращает уже не настоящий `pair`, а прокси-объект. Универсальным решением будет в этом случае -- `decltype(auto)` в качестве возвращаемого значения. +`std::map` может в будущем поменяться на что-то другое, у чего итератор возвращает уже не настоящий `pair`, а прокси-объект. Универсальным решением будет в этом случае — `decltype(auto)` в качестве возвращаемого значения. ## Проблемы автоматического вывода типа @@ -89,28 +89,28 @@ private: T name; }; ``` -2. `const auto&` -- может забиндиться к висячей ссылке: +2. `const auto&` — может забиндиться к висячей ссылке: ```C++ const auto& x = std::min(1, 2); - // x -- dangling reference + // x — dangling reference ``` В качестве возвращаемого значения функции `const auto&` использовать в 90% случаев [не выйдет](https://godbolt.org/z/hqf3nK) при правильно настроенных предупреждениях компилятора. -3. `auto&&` -- universal/forwarding reference. Точно также может забиндиться к висячей ссылке: +3. `auto&&` — universal/forwarding reference. Точно также может забиндиться к висячей ссылке: ```C++ auto&& x = std::min(1, 2); - // x -- dangling reference + // x — dangling reference ``` -С возвращаемым значением -- [аналогично](https://godbolt.org/z/qx8e1M) варианту `const auto&` +С возвращаемым значением — [аналогично](https://godbolt.org/z/qx8e1M) варианту `const auto&` -4. `decltype(auto)` -- автовывод "как объявлено": справа ссылка -- слева ссылка. Справа нет ссылки -- слева нет ссылки. В каком-то смысле то же самое, что и `auto&&` при объявлении переменных, но [не совсем](https://godbolt.org/z/Yorrjo): +4. `decltype(auto)` — автовывод "как объявлено": справа ссылка — слева ссылка. Справа нет ссылки — слева нет ссылки. В каком-то смысле то же самое, что и `auto&&` при объявлении переменных, но [не совсем](https://godbolt.org/z/Yorrjo): ```C++ auto&& x = 5; static_assert(std::is_same_v); decltype(auto) y = 5; static_assert(std::is_same_v); ``` -Разница в том, что `auto&&` -- всегда ссылка, а `decltype(auto)` -- "как объявлено в возвращаемом значении". Что может быть важно при дальнейших вычислениях над типами. +Разница в том, что `auto&&` — всегда ссылка, а `decltype(auto)` — "как объявлено в возвращаемом значении". Что может быть важно при дальнейших вычислениях над типами. `decltype(auto)` начинает стрелять при использовании его в качестве возвращаемого значения, требуя дополнительной внимательности при написании [кода](https://godbolt.org/z/PPcPYK): ```C++ @@ -122,7 +122,7 @@ public: decltype(auto) Name2() const { return (name); // ссылка. Выражение (name) имеет тип const T&: - // само по себе (name) -- T&, но this помечен const, поэтому + // само по себе (name) — T&, но this помечен const, поэтому // получается const T& } @@ -138,12 +138,12 @@ public: decltype(auto) DanglingName1() const { auto&& local_name = Name1(); // возвращает копию. Копия прибивается к rvalue ссылке - return local_name; // local_name -- ссылка на локальную переменную + return local_name; // local_name — ссылка на локальную переменную } decltype(auto) DanglingName2() const { auto local_name = Name1(); // возвращает копию. - return (local_name); // (local_name) -- ссылка на локальную переменную + return (local_name); // (local_name) — ссылка на локальную переменную } decltype(auto) NonDanglingName() const { @@ -157,7 +157,7 @@ private: }; ``` -`decltype(auto)` это хрупкий и тонкий механизм, способный перевернуть все с ног на голову с помощью минимального изменения в коде -- "лишних" скобок или `&&`. +`decltype(auto)` это хрупкий и тонкий механизм, способный перевернуть все с ног на голову с помощью минимального изменения в коде — "лишних" скобок или `&&`. --- ## Полезные ссылки diff --git a/lifetime/for_loop.md b/lifetime/for_loop.md index c220ec8..0f22d48 100644 --- a/lifetime/for_loop.md +++ b/lifetime/for_loop.md @@ -4,7 +4,7 @@ Правило хитрое и состоит не только в том, что `const&&` или `&&` продляют жизнь временному объекту (но только парвая такая ссылка). На самом деле правило такое: -все временные объекты живут до окончания выполнения всего включающего их выражения (statement) -- грубо говоря, до ближайшей точки с запятой(`;`). +все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(`;`). ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта `const&` или `&&` ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта То есть: @@ -16,7 +16,7 @@ const int& x = 1 + 2; // временные объекты 1, 2, // Но мы присваиваем 3 константной ссылке, // Ее область видимости простирается ниже, дальше ; // Так что время жизни продлевается. -// Таким образом: 1, 2 -- умирают. 3 -- продолжает жить +// Таким образом: 1, 2 — умирают. 3 — продолжает жить const int& y = std::max([](const int& a, const int& b) -> const int& { @@ -24,14 +24,14 @@ const int& y = std::max([](const int& a, const int& b) -> const int& { }(1 + 2, 4), 5); // временные объекты 1,2, 3(сумма), 4, 5 живут до ЭТОЙ ; // 3, 4 присваиваются константным ссылкам в аргументах лямбда-функии. // область видимости этих ссылок заканчивается после return -// -- она МЕНЬШЕ времени жизни временного объекта. +// — она МЕНЬШЕ времени жизни временного объекта. // ссылки ничего не продлили, но лишили временных объект будущего. // 5 прибивается к константной ссылке в аргументе std::max -// Со ссылками на 4, 5 успешно отрабатывает std::max -- +// Со ссылками на 4, 5 успешно отрабатывает std::max — // их время жизни еще не закончилось. Ссылки валидны. -// Ссылка-результат присваевается `y`. Продлений жизни не происходит -- +// Ссылка-результат присваевается `y`. Продлений жизни не происходит — // все временные объекты уже безуспешно попытали счастья на аргументах функций. // Дело доходит до ; Время жизни всех объектов 1,2,3,4,5 заканчивается. // `y` становится висячей. Занавес. @@ -64,7 +64,7 @@ int main() { ``` Все [работает](https://godbolt.org/z/r1zbzK), как и ожидается -Повысим инкапсуляцию, проведем минимальный рефакторинг -- сделаем `vertexes` приватным полем с read-only доступом: +Повысим инкапсуляцию, проведем минимальный рефакторинг — сделаем `vertexes` приватным полем с read-only доступом: ```C++ struct Shape { @@ -115,7 +115,7 @@ for (; begin_ != end_; ++begin_) { auto&& container_ = MakeShape().vertexes; // временный объект Shape живет до ;. Он не встретил еще ни одной const& или && // ссылки -// Подобъект vertexes -- считается таким же временным. +// Подобъект vertexes — считается таким же временным. // Его время жизни закончится на ; // Но он встречает && ссылку, область видимости которой простирается ниже // и продлевает ему жизнь. Причем придлевается жизнь не только лишь подобъекту @@ -135,7 +135,7 @@ auto&& container_ = MakeShape().Vertexes(); Вот так все просто и сломано. -Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект -- очень неочевидная штука. И, если, например, ваш код полагся на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o). +Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагся на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o). --- diff --git a/lifetime/lambda_capture.md b/lifetime/lambda_capture.md index 869abb9..4a3b17e 100644 --- a/lifetime/lambda_capture.md +++ b/lifetime/lambda_capture.md @@ -7,7 +7,7 @@ C++11 подарил нам лямбда-функции и, вместе с ни ```C++ auto make_add_n(int n) { return [&](int x) { - return x + n; // n -- станет висячей ссылкой! + return x + n; // n — станет висячей ссылкой! }; } @@ -16,10 +16,10 @@ auto add5 = make_add_n(5); std::cout << add5(5); // UB! ``` -Ничего принципиально новго -- тут все те же проблемы, что и с возвратом ссылки из функции. +Ничего принципиально новго — тут все те же проблемы, что и с возвратом ссылки из функции. clang иногда [способен выдать предупреждение](https://godbolt.org/z/rsq8hM). -Но стоит нам принять аргумент `make_add_n` по ссылке -- и [никаких предупреждений не будет](https://godbolt.org/z/1K89z9). +Но стоит нам принять аргумент `make_add_n` по ссылке — и [никаких предупреждений не будет](https://godbolt.org/z/1K89z9). Аналогично проблему [можно наиграть](https://godbolt.org/z/31KdTj) и для методов объектов: ```C++ @@ -28,7 +28,7 @@ struct Task { std::function GetNotifier() { return [this]{ - // this -- может стать висячей ссылкой! + // this — может стать висячей ссылкой! std::cout << "notify " << id << "\n"; }; } @@ -47,7 +47,7 @@ struct Task { std::function GetNotifier() { return [=]{ - // this -- может стать висячей ссылкой! + // this — может стать висячей ссылкой! std::cout << "notify " << id << "\n"; }; } diff --git a/lifetime/self_init.md b/lifetime/self_init.md index f660eed..5e950a7 100644 --- a/lifetime/self_init.md +++ b/lifetime/self_init.md @@ -38,11 +38,11 @@ void fun(int y) { } ``` -Код, уходящий в область неопределенного поведения при добавлении лишь одного символа -- все как мы любим. +Код, уходящий в область неопределенного поведения при добавлении лишь одного символа — все как мы любим. --- Такой код синтаксически валиден и никто не собирается его запрещать. Более того, он еще и [не всегда](https://godbolt.org/z/7jqo61) приводит к UB. -К UB приводит только использование с, грубо говоря, разыменованием ссылки на этот объект. Почему грубо? Потому что правила такие же, как и с разыменованием `nullptr` -- то есть довольно [путанные](https://habr.com/ru/post/513058/), а не просто лишь "никогда нельзя -- всегда UB". Хотя использование такой радикальной трактовки уберет вас от многих бед. +К UB приводит только использование с, грубо говоря, разыменованием ссылки на этот объект. Почему грубо? Потому что правила такие же, как и с разыменованием `nullptr` — то есть довольно [путанные](https://habr.com/ru/post/513058/), а не просто лишь "никогда нельзя — всегда UB". Хотя использование такой радикальной трактовки уберет вас от многих бед. ```C++ struct ExtremelyLongClassName { diff --git a/lifetime/string_view.md b/lifetime/string_view.md index bbb56ae..f0c0c80 100644 --- a/lifetime/string_view.md +++ b/lifetime/string_view.md @@ -1,4 +1,4 @@ -# string_view -- тот же const& только больнее +# string_view — тот же const& только больнее С++17 подарил нам тип `std::string_view`, призванный убить сразу двух зайцев: - Проблемы с перегрузками для функций, которые должны работать хорошо как с C, так и с C++ строками @@ -13,7 +13,7 @@ int count_char(const std::string& s, char c) { count_char("hello world", 'l'); // создастся временный объект std::string, // выделится память, скопируется строка, а потом строка умрет и память -// деаллоцируется --- плохо, много лишних операций +// деаллоцируется — плохо, много лишних операций ``` Так что нам нужна перегрузка для С-строк @@ -99,7 +99,7 @@ int main() { Сутация такая же, как с [ранее рассмотренным](use_after_free_in_general.md) `std::min`. Только защититься от такой функции `common_prefix`, обернув ее в шаблон с помощью анализа rvalue/lvalue, -намного сложнее: нам нужно разобрать случаи `const char*` и `std::string` для каждого аргумента -- в общем, все то, от чего нас введение `std::string_view` "избавило". +намного сложнее: нам нужно разобрать случаи `const char*` и `std::string` для каждого аргумента — в общем, все то, от чего нас введение `std::string_view` "избавило". Влететь в `string_view` можно еще изящнее: @@ -112,13 +112,13 @@ struct Person { if (name.length() <= 2) { return name; } - return name.substr(0, 2); // copy -- dangling reference! + return name.substr(0, 2); // copy — dangling reference! } }; ``` Причем [видно](https://godbolt.org/z/TPc4zq), что Clang хотя бы выдает предупреждение. -А gcc -- нет. +А gcc — нет. Все потому что `std::string_view` настолько легендарный, что в clang сделали хоть какой-то lifetime checker сперва для него. diff --git a/lifetime/use_after_free_in_general.md b/lifetime/use_after_free_in_general.md index 11583e4..5c49d20 100644 --- a/lifetime/use_after_free_in_general.md +++ b/lifetime/use_after_free_in_general.md @@ -2,10 +2,10 @@ 80% случаев неопределенного поведения в C++ связаны с ними. -Объект жил на стеке и умер. Или объект жил в куче и умер. Разница по сути не очень большая: обобщенный сценарий воспроизведения ошибки один и тот же -- где-то остались указатель или ссылка на уже мертвый объект. А потом этой ссылкой (или указателем) воспользовались, чтобы обратиться к мертвому объекту. Такой спиритический сеанс заканчивается неопределенным поведением. Если повезет -- будет ошибка сегментации с возможностью узнать, кто именно обратился. +Объект жил на стеке и умер. Или объект жил в куче и умер. Разница по сути не очень большая: обобщенный сценарий воспроизведения ошибки один и тот же — где-то остались указатель или ссылка на уже мертвый объект. А потом этой ссылкой (или указателем) воспользовались, чтобы обратиться к мертвому объекту. Такой спиритический сеанс заканчивается неопределенным поведением. Если повезет — будет ошибка сегментации с возможностью узнать, кто именно обратился. Но все же между мервым объектом со стека или мервым объектом из кучи есть разница в возможности обнаружения методами динамического анализа: -Для инструментации стека санитайзерами вообще говоря нужно перекомпилировать программу. Для инструментации кучи -- можно подменить библиотеку с аллокатором. +Для инструментации стека санитайзерами вообще говоря нужно перекомпилировать программу. Для инструментации кучи — можно подменить библиотеку с аллокатором. ------ Конечно, в жизни почти никто и никогда явно не пишет некорректный код вида @@ -121,10 +121,10 @@ decltype(auto) // выводим тип без отбрасывания ссыл safe_min(T1&& a, T2&& b) { // forwarding reference на каждый аргумент. if constexpr (std::is_lvalue_reference_v && std::is_lvalue_reference_v) { - // оба аргумента были lvalue -- можно безопасно вернуть ссылку + // оба аргумента были lvalue — можно безопасно вернуть ссылку return std::min(a, b); } else { - // один из аргументов -- временный объект. + // один из аргументов — временный объект. // возвращаем по значению. // для этого делаем копию auto temp = std::min(a,b); // auto&& нельзя! @@ -150,7 +150,7 @@ auto&& y = std::min({x, 10, 15, y}); // OK Все вышеописанное рассматривало только свободные функции и, что то же самое, статическим методы классов. -Но с методами классов возврат ссылок -- обычное дело. И проблемы с ними те же, но менее явные. Неявность связана с передачей указателя `this` на текущий объект. +Но с методами классов возврат ссылок — обычное дело. И проблемы с ними те же, но менее явные. Неявность связана с передачей указателя `this` на текущий объект. Так, например, безопасная реализация условного _Builder_ с поддержкой вызовов методов по цепочке оказывается весьма нетривиальной. @@ -222,13 +222,13 @@ class VectorBuilder { auto&& builder = VectorBuilder{}.Append(1).Append(2).Append(3); ``` -Опять получим висячую ссылку, но уже на сам объект `VectorBuilder`. Добавленная перегрузка `Append` тут ни при чем -- неявный `this` и в исходном случае успевал прибиваться к временному объекту, и единоразово продлевать ему жизнь. +Опять получим висячую ссылку, но уже на сам объект `VectorBuilder`. Добавленная перегрузка `Append` тут ни при чем — неявный `this` и в исходном случае успевал прибиваться к временному объекту, и единоразово продлевать ему жизнь. Чтобы этого избежать, нам нужно: Либо настраивать линтер, запрещающий использовать `auto&&` и `const auto&` c этим классом в правой части. -Либо жертвовать производительностью, и в rvalue версии `Append` возвращать по значению (+ move) -- при большом количестве примитивных, всегда копируемых, объектов внутри, просадка будет заметной. +Либо жертвовать производительностью, и в rvalue версии `Append` возвращать по значению (+ move) — при большом количестве примитивных, всегда копируемых, объектов внутри, просадка будет заметной. Либо в принципе запретить использовать `VectorBuilder` в rvalue контексте: ```C++ diff --git a/lifetime/vector_invalidation.md b/lifetime/vector_invalidation.md index 92a0b64..e9d2dbc 100644 --- a/lifetime/vector_invalidation.md +++ b/lifetime/vector_invalidation.md @@ -6,7 +6,7 @@ - std::deque - std::vector -Из них `std::vector` используется в большинстве случаев. А остальные -- только если их особенности становятся действительно необходимыми и дают заметную разницу в улучшении производительности. Так, например, возможность вставки в произвольную позицию за константное число операций в `std::list` [не дает преимущества](https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vector-vs-list.html) в сравнении с `std::vector` (требует линейного времени), пока контейнеры не достаточно большие или размер элементов мал. +Из них `std::vector` используется в большинстве случаев. А остальные — только если их особенности становятся действительно необходимыми и дают заметную разницу в улучшении производительности. Так, например, возможность вставки в произвольную позицию за константное число операций в `std::list` [не дает преимущества](https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vector-vs-list.html) в сравнении с `std::vector` (требует линейного времени), пока контейнеры не достаточно большие или размер элементов мал. `std::vector`, будучи самым эффективным контейнером, является еще и самым небезопасным. Из-за инвалидации ссылок и итераторов. @@ -26,7 +26,7 @@ void run_actions(std::vector actions) { ``` Красиво, коротко, с неопределенным поведением и неправильно. -- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end ивалидируются -- цикл продолжится по уничтоженным данным. +- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end ивалидируются — цикл продолжится по уничтоженным данным. - Если реаллокации не произойдет, цикл пройдет только по тому набору элементов, что были в векторе изначально. До добавленных в процессе дело не дойдет. Корректый код: @@ -53,7 +53,7 @@ void run_actions(std::vector actions) { } } ``` -И у нас опять неопределенное поведение -- `push_back` может вызвать реаллокацию вектора +И у нас опять неопределенное поведение — `push_back` может вызвать реаллокацию вектора и тогда ссылка `act` станет висячей. Корректный код: @@ -68,14 +68,14 @@ void run_actions(std::vector actions) { } ``` -Этот простой паттерн с инвалидацией ссылок в векторе может очень легко спрятаться под слоем абстракций. Например -- цикл обработки явялется публичным методом класса `TaskQueue`, а обработка одной задачи -- его приватный метод. В таком случае изменение в одном методе, совершенно корректное в рамках него, приведен к UB из-за неявного влияния на другой метод. +Этот простой паттерн с инвалидацией ссылок в векторе может очень легко спрятаться под слоем абстракций. Например — цикл обработки явялется публичным методом класса `TaskQueue`, а обработка одной задачи — его приватный метод. В таком случае изменение в одном методе, совершенно корректное в рамках него, приведен к UB из-за неявного влияния на другой метод. Кое-как защититься от подобной неприятности можно с помощью статических анализаторов, работающих с потоком исполнения программы. Также проблема точно ловится санитайзерами или утилитами проверки памяти (например, valgrind). Если, конечно, у вас достаточно хорошие тесты. В языке Rust проблема ловится на этапе компиляции с помощью borrow checker'а. Если вы можете позволить себе просадку производительности, лучше использовать специализированные контейнеры (или адапторы контейнеров) для специфичных задач. -Так `std::queue` по умолчанию использует `std::deque` и не инвалидирует ссылки при добавлении новых элементов. А также ее нельзя неосторожно использовать в range-based-for -- у нее нет итераторов begin/end +Так `std::queue` по умолчанию использует `std::deque` и не инвалидирует ссылки при добавлении новых элементов. А также ее нельзя неосторожно использовать в range-based-for — у нее нет итераторов begin/end ## Полезные ссылки 1. https://baptiste-wicht.com/posts/2012/11/cpp-benchmark-vector-vs-list.html diff --git a/numeric/floats.md b/numeric/floats.md index f156997..a66bd73 100644 --- a/numeric/floats.md +++ b/numeric/floats.md @@ -5,14 +5,14 @@ Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что по IEEE 754 выражение `x/0.0` определяется как `-INF`, `NaN`, или `INF` в зависимости от знака числа `x` (`NaN` для нуля). -Сравнение вещественных чисел -- излюбленная головная боль. -Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же -- [не стоит](https://godbolt.org/z/q55891). +Сравнение вещественных чисел — излюбленная головная боль. +Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же — [не стоит](https://godbolt.org/z/q55891). Выражение `x == y` фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями `-0.0` и `+0.0`, и `NaN`. О существовании этого и `!=` операторов для вещественных чисел стоит забыть и никогда не вспонимать. Для побитового сравнения нужно использовать `memcmp`. -Для сравнения чисел -- приближенные варианты вида `std::abs(x - y) < EPS`, где `EPS` -- какое-то абсолютное или вычисляемое на основе `x` и `y` значение. А также различные манипуляции с [`ULP`](https://en.wikipedia.org/wiki/Unit_in_the_last_place) сравниваемых чисел. +Для сравнения чисел — приближенные варианты вида `std::abs(x - y) < EPS`, где `EPS` — какое-то абсолютное или вычисляемое на основе `x` и `y` значение. А также различные манипуляции с [`ULP`](https://en.wikipedia.org/wiki/Unit_in_the_last_place) сравниваемых чисел. Так как стандарт C++ не форсирует IEEE 754, проверки на `x == NaN` через его свойство `(x != x) == true` могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого diff --git a/numeric/narrowing.md b/numeric/narrowing.md index ca9f97e..c06b4cf 100644 --- a/numeric/narrowing.md +++ b/numeric/narrowing.md @@ -30,18 +30,18 @@ int main() { В этом коде нет неопределенного поведение (по крайней мере на используемых входных данных). Но есть неявное приведение типов, делающее результат неожиданным. -1. Тип возвращаемого значения `std::accumulate` определяется третьим аргументом. В данном случае это целочисленный знаковый ноль -- тип по умолчанию для всех числовых литералов. -2. Тип возвращаемого значения операции деления определяется наибольшим из участвующих типов аргументов, а также правилами [integer promotion](https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules). В примере тип левого арумента -- `int`, а правого -- `size_t` -- достаточно широкое беззнакое целое, Более широкое чем `int`. Потому, по правилам integer promotion, результатом будет `size_t` -3. `-3` неявно преобразуется к типу `size_t` -- такое преобразование вполне определено. Результатом будет беззнаковое число `2^N - 3`. +1. Тип возвращаемого значения `std::accumulate` определяется третьим аргументом. В данном случае это целочисленный знаковый ноль — тип по умолчанию для всех числовых литералов. +2. Тип возвращаемого значения операции деления определяется наибольшим из участвующих типов аргументов, а также правилами [integer promotion](https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules). В примере тип левого арумента — `int`, а правого — `size_t` — достаточно широкое беззнакое целое, Более широкое чем `int`. Потому, по правилам integer promotion, результатом будет `size_t` +3. `-3` неявно преобразуется к типу `size_t` — такое преобразование вполне определено. Результатом будет беззнаковое число `2^N - 3`. 4. Далее будет проиведено деление беззнаковых числел. `(2^N - 3) / 3`. Старший бит результата окажется нулевым. 5. Возвращаемым типом функции `average` объявлен `int`. Так что нужно выполнить еще одно неявное преобразование. 6. В общем случае преобразование unsigned -> signed определяется реализацией (implementation defined). - 1. Если размеры типов `int` и `size_t` одинаковые, то, поскольку старший бит нулевой, положительное число укладывается в допустимый диапазон значений для типа `int` -- стандарт гарантирует, что никаких проблем нет + 1. Если размеры типов `int` и `size_t` одинаковые, то, поскольку старший бит нулевой, положительное число укладывается в допустимый диапазон значений для типа `int` — стандарт гарантирует, что никаких проблем нет 2. Если размеры не совпадают, то произойдет сужающее преобразование (_narrowing conversion_), которое как раз таки отдано на откуп деталям реализации. Так, вместо ожидаемой обрезки старших, не поместившихся, битов, на некоторых платформах может произойти замена на `std::numeric_limits::max` 3. Для примера сборки под 64-битную платформу с помощью `gcc` сужающее преобразование определено, как и ожидается, через обрезку старших битов. Поэтому итоговым результатом оказывается (`(2^64 -3) / 3 % 2^32` ) -Неявные приведения типов касаются не только встроенных примитивов, но и более сложных типов. И самое неприятное -- они вмешиваются в выбор подходящей перегрузки функции, приводя к различным, часто неприятным, казусам. +Неявные приведения типов касаются не только встроенных примитивов, но и более сложных типов. И самое неприятное — они вмешиваются в выбор подходящей перегрузки функции, приводя к различным, часто неприятным, казусам. Пример с [`abs`](https://godbolt.org/z/KbTza4) ```C++ @@ -51,10 +51,10 @@ int main() { int main() { std::cout << abs(3.5) << "\n"; // функция библиотеки С, // принимает на вход тип long - // результат -- 3 + // результат — 3 std::cout << std::abs(3.5); // функция библиотеки С++ // перегружена для double - // результат -- 3.5 + // результат — 3.5 } ``` @@ -81,9 +81,9 @@ int main() { Обязателько включайте предупреждения компилятора обо всех неявных преобразованиях. Очень желательно трактовать их как ошибки. -Не привносите невные преобразования для своих типов -- всегда помечайте однопараметрические конструкторы как `explicit`. +Не привносите невные преобразования для своих типов — всегда помечайте однопараметрические конструкторы как `explicit`. -Если перегружаете операторы приведения (`operator T()`) для своих типов -- также делайте их `explicit`. +Если перегружаете операторы приведения (`operator T()`) для своих типов — также делайте их `explicit`. Если ваши функции/методы рассчитаны на работу только с определенным примитивным типом, навешивайте на них ограничения с помощью шаблонов, SFINAE, концептов, или, что [очень просто](https://godbolt.org/z/Yx1e3d), механизма явного удаления перегрузок (`= delete`): diff --git a/numeric/overflow.md b/numeric/overflow.md index 278d461..778d005 100644 --- a/numeric/overflow.md +++ b/numeric/overflow.md @@ -1,9 +1,9 @@ # Переполнение целых знаковых чисел -Большая часть написанного и еще не написанного кода любой программы так или иначе работает с числами. Вычисление по каким-либо формулам, увеличение или уменьшение счетчиков итераций циклов, рекурсивных вызовов, элементов контейнеров -- работа с числами везде. +Большая часть написанного и еще не написанного кода любой программы так или иначе работает с числами. Вычисление по каким-либо формулам, увеличение или уменьшение счетчиков итераций циклов, рекурсивных вызовов, элементов контейнеров — работа с числами везде. -Компьютер не может напрямую работать с бесконечно "длинными" числами -- хранить все их цифры. Как бы много оперативной памяти у нас ни было -- все же она конечна. Да и хранить, и обрабатывать величины, сопоставимые с числом атомов в видимой части вселенной -- безнадежное занятие. Так что ограничения типов `int64` или `int128` не очень нас-то и ограничивают +Компьютер не может напрямую работать с бесконечно "длинными" числами — хранить все их цифры. Как бы много оперативной памяти у нас ни было — все же она конечна. Да и хранить, и обрабатывать величины, сопоставимые с числом атомов в видимой части вселенной — безнадежное занятие. Так что ограничения типов `int64` или `int128` не очень нас-то и ограничивают Тем не менее при выполнении операций над целыми числами мы все же имеем шанс выпасть за пределы допустимого диапазона (например, `[-2^31, 2^31-1]` для `int32`), и тут в игру вступают особенности поддержки целых чисел для того или иного языка программирования, а также, быть может, особенности реализации конкретной платформы. @@ -16,12 +16,12 @@ iadd x 5 В реализации конкретного языка программирования может быть проверка флага переполнения и сообщение об ошибке. А может и не быть. Может быть гарантия "цикличности" значений (после `2^31-1` идет `-2^31`), а может и не быть. -Проверки и гарантии -- это дополнительные инструкции, которые нужно генерировать компилятору, а процессору потом исполнять. +Проверки и гарантии — это дополнительные инструкции, которые нужно генерировать компилятору, а процессору потом исполнять. В языке C++ решили не жертвовать производительностью и заставлять компиляторы генерировать код проверки, а объявили переполнение целых знаковых (`signed`) чисел неопределенным, открывая простор для оптимизаций. Компилятор может генерировать любой код, какой ему вздумается, ориентируясь лишь на одно правило: переполнения не бывает. -Многие программисты свято верят, что переполнение чисел работает, как ожидается, "циклично" -- и пишут проверки вида +Многие программисты свято верят, что переполнение чисел работает, как ожидается, "циклично" — и пишут проверки вида ```C++ if (x > 0 && a > 0 && x + a <= 0) { @@ -122,7 +122,7 @@ ErrorOrInteger div(I a, std::type_identity_t b) { if (a == std::numeric_limits::min && b == -1) { // диапазон [min, max] несимметричный относительно 0. - // abs(min) > max -- будет переполнение + // abs(min) > max — будет переполнение return ArithmeticError::Overflow; } return a / b; @@ -157,7 +157,7 @@ ErrorOrInteger mod(I a, std::type_identity_t b) { Однако, если ваша программа только и делает, что ожидает и выполняет IO операции, то траты в два раза большего числа тактов на сложение или умножение никто и не заметит. Да и язык C++ для таких программ чаще всего не лучший выбор. -Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет -- все определено как вычисления по модулю `2^N` (N -- количество бит для выбранного типа чисел). +Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел). Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные програмы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да? @@ -165,7 +165,7 @@ ErrorOrInteger mod(I a, std::type_identity_t b) { А также тестовые `constexpr` вычисления. -Также проблемы неопределенного поведения при переполнении касаются битовых сдвигов влево для отрицательных чисел (или при сдвиге положительного числа с залезанием в знаковый бит). Начиная с C++20, стандарт требует фиксированной единой реализации отрицательных чисел -- через дополнительный код ([_two's complement_](https://en.wikipedia.org/wiki/Two%27s_complement)), и многие проблемы сдвигов сняты. Тем не менее все равно стоит следовать общей рекомендации: любые битовые операции выполнять только в `unsigned` типах. +Также проблемы неопределенного поведения при переполнении касаются битовых сдвигов влево для отрицательных чисел (или при сдвиге положительного числа с залезанием в знаковый бит). Начиная с C++20, стандарт требует фиксированной единой реализации отрицательных чисел — через дополнительный код ([_two's complement_](https://en.wikipedia.org/wiki/Two%27s_complement)), и многие проблемы сдвигов сняты. Тем не менее все равно стоит следовать общей рекомендации: любые битовые операции выполнять только в `unsigned` типах. Стоит заметить, что сужающее преобразование из целочисленного типа в другой целочисленный тип к неопределенному поведению не приводит, и выполнять побитовое `и` с маской перед присваиванием переменной меньшего типа не обязательно. Но желательно, чтобы избежать предупреждений компилятора @@ -192,7 +192,7 @@ static_assert(IntegerPromotionUB(65535) == 1); // won't compile ```C++ x *= x; // переписывается как x = x * x; -// тип uint16 меньше чем тип int -- для * выполняется неявное приведение к int. +// тип uint16 меньше чем тип int — для * выполняется неявное приведение к int. ``` diff --git a/pointer_prominence/invalid_pointer.md b/pointer_prominence/invalid_pointer.md index 35195cb..036b3e5 100644 --- a/pointer_prominence/invalid_pointer.md +++ b/pointer_prominence/invalid_pointer.md @@ -3,10 +3,10 @@ Что вообще такое указатель? Когда их пытаются объяснить новичкам в C++, часто говорять, что это число, адрес, указывающий на номер ячейки в памяти, где что-то лежит. -Это в каком-то смысле справедливо на очень низком уровне -- в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется. -Более того, в C++ (не в C), есть указатели, которые вообще не являются адресами в памяти -- указатели на поля и методы классов. Но о них мы говорить сейчас не будем. +Это в каком-то смысле справедливо на очень низком уровне — в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется. +Более того, в C++ (не в C), есть указатели, которые вообще не являются адресами в памяти — указатели на поля и методы классов. Но о них мы говорить сейчас не будем. -Указатель -- это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации. +Указатель — это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации. Для указателей в стандарте C++ подробно расписано, откуда они могут появляться. Если коротко, то: @@ -14,13 +14,13 @@ 2. Как результат вызова оператора `new` (возможно, _placement new_) 3. Как результат неявного преобразования имени массива или имени функции в указатель. 4. Как результат некоторой _допустимой_ операции над другим указателем. -5. Копирование существующего указателя. В частности -- копирование `nullptr`. +5. Копирование существующего указателя. В частности — копирование `nullptr`. -Все остальные источники указателей -- implementation defined или вообще undefined. +Все остальные источники указателей — implementation defined или вообще undefined. -Главная операция, выполняемая над указателями, -- разыменование -- то есть получение доступа -к объекту, на который этот указатель ссылается. И вместе с этой операцией приходит главная проблема -- ее не ко всем указателям применять можно. Есть и другие операции, которые также применимы не к любому указателю. -Но, конечно, есть eдинственная операция, допустимая (почти) всегда -- сравнение на равенство (не равенство). +Главная операция, выполняемая над указателями, — разыменование — то есть получение доступа +к объекту, на который этот указатель ссылается. И вместе с этой операцией приходит главная проблема — ее не ко всем указателям применять можно. Есть и другие операции, которые также применимы не к любому указателю. +Но, конечно, есть eдинственная операция, допустимая (почти) всегда — сравнение на равенство (не равенство). В идеальном светлом мире, от типа объекта зависит множество допустимых над ним операций. Но в случае указателей, и это очень печально, применимость или неприменимость зависит от значения указателя, но еще и от того, откуда этот указатель взялся. А также откуда взялись другие указатели! @@ -35,10 +35,10 @@ auto x_invalid_ptr = (&x) + 2; // невалидный указатель, ``` Сравнение указателей на больше или меньше определено только для указателей на элементы одного и того же массива. -Для произвольных указателей -- unspecified. +Для произвольных указателей — unspecified. -Арифметика указателей допустима только в пределах одного и того же массива (от указателя на первый элемент до указателя на элемент за последним) Иначе -- undefinded behavior. -Особый только случай `(&x) + 1` -- любой объект считается массивом из одного элемента. +Арифметика указателей допустима только в пределах одного и того же массива (от указателя на первый элемент до указателя на элемент за последним) Иначе — undefinded behavior. +Особый только случай `(&x) + 1` — любой объект считается массивом из одного элемента. Пример кода, который валится с UB именно на арифметике указателей найти сложно, зато можно привести пример с итераторами (которые разворачиваются в указатели). @@ -47,10 +47,10 @@ std::string str = "hell"; str.erase(str.begin() + 4 + 1 - 3); ``` -Этот код [упадет](https://rextester.com/GPVRKM58250) в отладочной сборке под msvc. `str.begin() + 4` -- указатель на элемент за последним. И еще `+1` +Этот код [упадет](https://rextester.com/GPVRKM58250) в отладочной сборке под msvc. `str.begin() + 4` — указатель на элемент за последним. И еще `+1` выводит за пределы строки. Это UB. И не важно, что дальше вычитание возвращает внутренний указатель обратно в границы строки. -Не стоит выполнять сложные вычисления с указателями. Прибавлять к ним или вычитать лучше всегда конечный числовой результат. В данном конкретном примере рассчет отступа (4 + 1 - 3) нужно выполнить отдельно -- расставить скобки или (еще лучше) безопаснее и понятнее, вынести в отдельную переменную. +Не стоит выполнять сложные вычисления с указателями. Прибавлять к ним или вычитать лучше всегда конечный числовой результат. В данном конкретном примере рассчет отступа (4 + 1 - 3) нужно выполнить отдельно — расставить скобки или (еще лучше) безопаснее и понятнее, вынести в отдельную переменную. ----------- @@ -98,7 +98,7 @@ private: const auto old_size = size(); // !access to invalidated data_! data_ = ndata; end_ = data_ + old_size; - } // else -- "ok", noop + } // else — "ok", noop } } ``` diff --git a/runtime/endless_loop.md b/runtime/endless_loop.md index 595c35b..d63a476 100644 --- a/runtime/endless_loop.md +++ b/runtime/endless_loop.md @@ -1,12 +1,12 @@ # Бесконечные циклы и проблема останова -Определить, завершается или не завершается программа на конкретном наборе данных -- алгоритимически невозможно в общем случае. +Определить, завершается или не завершается программа на конкретном наборе данных — алгоритимически невозможно в общем случае. Но в стандартах C и C++ зачем-то сказано, что валидная программа должна либо гарантированно завершаться, либо гарантированно производить обозреваемые эффекты: запрашивать ввод-вывод, взаимодействовать с `volatile` переменными и подобное. А иначе поведение программы неопределенное. Так что "правильные" компиляторы C++ настолько суровы, что способны решать алгоритмически неразрешимые задачи. Если в программе есть бесконечный цикл, и компилятор решил, что этот цикл не имеет обозреваемых эффектов, то цикл не имеет смысла и может быть выброшен. -Занятный пример -- таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/nE7oWf) +Занятный пример — таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/nE7oWf) ```C++ #include @@ -40,11 +40,11 @@ int main () { return 0; } ``` -Комилятор увидел, что единственный выход из цикла -- `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1` +Комилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1` -Если же попытаться узнать, что за тройку "нашла" программа -- цикл вернется. +Если же попытаться узнать, что за тройку "нашла" программа — цикл вернется. -В `constexpr` контексте -- получим [ошибку компиляции](https://godbolt.org/z/98MYzd). +В `constexpr` контексте — получим [ошибку компиляции](https://godbolt.org/z/98MYzd). Может показаться, что проблема в том, что условие продолжения цикла не зависит от его тела. Но и в [исправленной](https://godbolt.org/z/o1Gcqc) версии цикл исчезает diff --git a/runtime/noexcept.md b/runtime/noexcept.md index 0538b39..39dfd74 100644 --- a/runtime/noexcept.md +++ b/runtime/noexcept.md @@ -24,7 +24,7 @@ struct WrongNoexcept { }; // Попытки обернуть в try-catch эту функцию или любой код, -// использующий ее -- бесполезны. +// использующий ее — бесполезны. void throw_smth() { if (rand() % 2 == 0) { throw std::runtime_error("throw"); @@ -44,9 +44,9 @@ void throw_smth() { - новый `requires` имеет два значения, порождая странные конструкции `requires(requires(...))` - `auto` и для автовывода, и для переключения на trailing return type - `decltype`, у которого разный смысл при примении к переменной и к выражению -- и, конечно, `noexcept` -- точно также два значения как у `requires`. +- и, конечно, `noexcept` — точно также два значения как у `requires`. -Есть спецификатор `noexcept(condition)`. И просто `noexcept` -- синтаксический сахар +Есть спецификатор `noexcept(condition)`. И просто `noexcept` — синтаксический сахар для конструкции `noexcept(true)`. А есть предикат `noexcept(expr)`, проверяющий, что выражение `expr` не кидает исключений по самой своей природе (сложение чисел, например) или же diff --git a/runtime/recursion.md b/runtime/recursion.md index b189442..ebb26c7 100644 --- a/runtime/recursion.md +++ b/runtime/recursion.md @@ -3,7 +3,7 @@ Многие алгоритмы очень красиво и компактно записываются в рекурсивной форме. Сортировки, обходы графов, строковые алгоритмы. -Однако рекурсия требует места для хранения промежуточного состояния -- на куче или в стеке. +Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке. Конечно, есть хвостовая рекурсия, которая естественным образом может быть оптимизирована в цикл. Но это не гарантировано стандартом. Да и не всегда рекурсия именно хвостовая. Stack overflow не совсем неопределенное поведение, но точно не то, чего хочется видеть на боевом стенде. Потому в серьезных приложениях предпочитают итеративные алгоритмы рекурсивным. Если, конечно, нет гарантии, что глубина рекурсии мала. @@ -21,11 +21,11 @@ struct Node { Такая структура совешенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями. -Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся "правилом нуля" и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего -- красота! +Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся "правилом нуля" и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего — красота! Однако, деструктор, сгенерированный компилятором, будет рекурсивным! И при слишком большой глубине дерева мы получим переполнение стека. -Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева... А очередь это аллокация памяти. А аллокация памяти -- операция, бросающая исключения. И вот у нас деструктор будет бросать исключения. Что совсем не хорошо. +Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева... А очередь это аллокация памяти. А аллокация памяти — операция, бросающая исключения. И вот у нас деструктор будет бросать исключения. Что совсем не хорошо. Можно написать деструктор без аллокаций и рекурсии. Но его алгоритмическая сложность будет квадратичной: @@ -53,7 +53,7 @@ struct List { ~List() { while (next) { // деструктор все также рекурсивен, - // но теперь глубина рекурсии -- 1 вызов + // но теперь глубина рекурсии — 1 вызов next = std::move(next->next); } } diff --git a/syntax/const_launder.md b/syntax/const_launder.md index bf8d35c..03d854c 100644 --- a/syntax/const_launder.md +++ b/syntax/const_launder.md @@ -4,7 +4,7 @@ Также в C++ есть `const_cast`, позволяющий этот `const` игнорировать. И иногда за это вам ничего не будет. А иногда будет неопределенное поведение, segfault, и прочие радости жизни. -Разница между этими "иногда" в том, что есть настоящие константы, попытка модификации которых -- UB. +Разница между этими "иногда" в том, что есть настоящие константы, попытка модификации которых — UB. А есть ссылки на константы, ссылающиеся не на константы. И раз на самом деле объект неконстантен, то модифицировать его можно без проблем. @@ -29,7 +29,7 @@ public: int& get_for_val_or_abs_val(int val) { return const_cast( // отбрасываем const с результата. // находясь в неконстантном методе, мы знаем, что результат - // в действительности не является константой -- проблем не будет. + // в действительности не является константой — проблем не будет. std::as_const(*this) // навешиваем const, чтобы вызвать const-метод, // а не уйти в бесконечную рекурсию .get_for_val_or_abs_val(val)); @@ -43,7 +43,7 @@ private: }; ``` -По возможности, стоит избегать такого кода. Видно, что он очень хрупок -- забытый или случайно удаленный +По возможности, стоит избегать такого кода. Видно, что он очень хрупок — забытый или случайно удаленный `std::as_const` ломает его. И без настройки предупреждений, компиляторы об этом сообщать [не торопятся](https://godbolt.org/z/17fc31). Вместо использования `const_cast` и привнесения в мир C++ еще большей нестабильности, @@ -53,11 +53,11 @@ class MyMap { public: // какой-то метод с длинной реализацией: const int& get_for_val_or_abs_val(int val) const { - return get_for_val_or_abs_val_impl(*this, val); // *this -- const& + return get_for_val_or_abs_val_impl(*this, val); // *this — const& } int& get_for_val_or_abs_val(int val) { - return get_for_val_or_abs_val_impl(*this, val); // *this -- & + return get_for_val_or_abs_val_impl(*this, val); // *this — & } void set_val(int val, int x) { @@ -160,7 +160,7 @@ void find_last_zero_pos(const std::vector& v, for (size_t i = 0; i < v.size(); ++i) { // мы опять не можем один раз // сохранить значение v.size() if (v[i] == 0) { - // внутри вектора есть поля типа int* -- begin, end + // внутри вектора есть поля типа int* — begin, end // что если pointer_to_last_zero указывает на один из них?!? *poiner_to_last_zero = (v.data() + i); } @@ -170,7 +170,7 @@ void find_last_zero_pos(const std::vector& v, ``` Оставаясь в рамках рекомендуемых практик написания C++ программ, -мы не можем соорудить пример, который бы демонстрировал неприменимость оптимизации -- нам мешает инкапсуляция. +мы не можем соорудить пример, который бы демонстрировал неприменимость оптимизации — нам мешает инкапсуляция. До приватных полей вектора мы не можем законно добраться. Но ненормальный код не запрещен! Применим грубую силу: @@ -191,7 +191,7 @@ int main() { ## Сonst, время жизни и происхождение указателей. -Неизменяемые объекты всем хороши, кроме одного -- это константные объекты в C++. +Неизменяемые объекты всем хороши, кроме одного — это константные объекты в C++. Если они где-то засели, то их оттуда по-нормальному не выгонишь. Что имеется в виду: @@ -206,7 +206,7 @@ struct Unit { ``` Из-за константного поля объекты `Unit` теряют операцию присваивания. -Их нелья менять местами -- `std::swap` не работает больше. +Их нелья менять местами — `std::swap` не работает больше. `std::vector` нельзя больше просто так отсортировать... В общем, сплошное удобство. Но самое интересное начинается, если сделать что-то такое @@ -261,7 +261,7 @@ std::cout << unit.back().id << ""; Но поддерживать указатель, взвращенный оператором `new`, не всегда возмножно. Он занимает место, его надо хранить, что неэффективно при реализации `optional`: для `int32_t` будет нужно в три раза больше места на 64хбитной системе (4 байта на `storage` + 8 байт на указатель)! -Поэтому в стандартной библиотеке с C++17 есть функция "отмывания" невесть откуда взявшихся указателей -- `std::launder`. +Поэтому в стандартной библиотеке с C++17 есть функция "отмывания" невесть откуда взявшихся указателей — `std::launder`. ```C++ using storage = std::aligned_storage_t; diff --git a/syntax/most_vexing_parse.md b/syntax/most_vexing_parse.md index 59ff4f0..4df2f32 100644 --- a/syntax/most_vexing_parse.md +++ b/syntax/most_vexing_parse.md @@ -31,7 +31,7 @@ int main() { ``` При этом *определять* сущности можно не везде. -Типы можно определять локально -- внутри функции. А функции определять нельзя. +Типы можно определять локально — внутри функции. А функции определять нельзя. ```C++ void fun() { @@ -87,7 +87,7 @@ void fun(int (val)); // скобки вокруг имени параметра int main() { const int time_to_work = 10; Worker w(Timer(time_to_work)); // предобъявление функции, которая возвращает Worker - // и принимает параметр типа Timer. time_to_work -- имя этого параметра! + // и принимает параметр типа Timer. time_to_work — имя этого параметра! std::cout << w; } @@ -99,7 +99,7 @@ Clang способен предепреждать о подобном. C++20 предлагает еще одну *uneversal* инициализацию, но уже снова через `()`... Избежать проблемы можно, используя *Almost Always Auto* подход с инициализацией вида -`auto w = Worker(Timer())`. Круглые или фигурные скобки здесь -- не так важно (хотя, на самом деле, важно, но в другой ситуации). +`auto w = Worker(Timer())`. Круглые или фигурные скобки здесь — не так важно (хотя, на самом деле, важно, но в другой ситуации). Возможно, когда-нибудь объявление функций в старом сишном стиле запретять в пользу *trailing return type* (`auto fun(args) -> ret`). И вляпаться в рассмотренную прелесть станет значительно сложнее (но все равно [можно](https://www.youtube.com/watch?v=tsG95Y-C14k)!) diff --git a/syntax/move.md b/syntax/move.md index 3dbf5d1..c5ba680 100644 --- a/syntax/move.md +++ b/syntax/move.md @@ -27,7 +27,7 @@ void run(...){ но окажется пустым. Раз два объекта, то и два вызова деструктора. С деструктивной семантикой перемещения (например, в Rust) вызов деструктора будет только один. -Можно исправить ситуацию -- передать по rvalue-ссылке: +Можно исправить ситуацию — передать по rvalue-ссылке: ```C++ void run_task(std::unique_ptr&& ptask) { @@ -69,7 +69,7 @@ void test_v2(){ Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение. Оно должно быть валидным в смысле вызова деструктора. Но более ничего не требуется. Объект не обязан быть пустым после перемещения. Его поля не обязаны быть зануленными. Так у `std::thread` после перемещения нельзя вызывать ни один из методов. А `std::unique_ptr` гарантированно становится пустым (`nullptr`). -Чаще всего и проще всего натолкнуться на use-after-move можно при реализцации конструкторов, заполняющих поля переданными аргументами -- достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам. +Чаще всего и проще всего натолкнуться на use-after-move можно при реализцации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам. ```C++ struct Person { @@ -85,7 +85,7 @@ private: }; ``` -Конечно, в таком случае ошибка будет быстро найдена -- для `std::string` есть гарантия, что после перемещения объект окажется пустым. Но если сделать конструктор шаблонным и передавать в него тривиально перемещаемые типы, ошибка долго может не проявляться. +Конечно, в таком случае ошибка будет быстро найдена — для `std::string` есть гарантия, что после перемещения объект окажется пустым. Но если сделать конструктор шаблонным и передавать в него тривиально перемещаемые типы, ошибка долго может не проявляться. ```C++ template @@ -99,7 +99,7 @@ private: Person p("John", "Smith"); // T1, T2 = const char* ``` -Другой интересный случай использования после перемещения -- self-move-assignment. +Другой интересный случай использования после перемещения — self-move-assignment. В результате которого из объекта могут внезапно пропадать данные. А могут и не пропадать. В зависимости от того, как реализовали перемещение для конкретного типа. diff --git a/syntax/stl_constructors.md b/syntax/stl_constructors.md index c590f1a..51ffd74 100644 --- a/syntax/stl_constructors.md +++ b/syntax/stl_constructors.md @@ -48,9 +48,9 @@ S3: 3 Потому что у строки есть конструктор, принимающий число `n` и символ `c`, который нужно повторить `n` раз. А еще есть конструктор, принимающий список инициализации (`std::initializer_list`), состоящий из символов. И существование этого конструктора взаимодействует с неявным приведением типов! -- `std::string s1 {'H', 3};` -- строка "H\3" -- `std::string s2 {3, 'H'};` -- строка "\3H" -- `std::string s3 (3, 'H');` -- строка "HHH" +- `std::string s1 {'H', 3};` — строка "H\3" +- `std::string s2 {3, 'H'};` — строка "\3H" +- `std::string s3 (3, 'H');` — строка "HHH" Аналогичной проблемой страдает `std::vector`