From 54cdebec3519cce7c9dfe9e573d7e0679caa4130 Mon Sep 17 00:00:00 2001 From: "Lapshin Dmitry (LDVSOFT)" Date: Mon, 25 Jan 2021 17:34:48 +0300 Subject: [PATCH] Remove trailing spaces --- README.md | 2 +- how_to_find_ub.md | 2 +- lifetime/decltype_auto_and_explicit_types.md | 14 +++--- lifetime/for_loop.md | 30 +++++------ lifetime/lambda_capture.md | 6 +-- lifetime/self_init.md | 22 ++++----- lifetime/string_view.md | 10 ++-- lifetime/use_after_free_in_general.md | 24 ++++----- lifetime/vector_invalidation.md | 2 +- numeric/floats.md | 4 +- numeric/narrowing.md | 6 +-- numeric/overflow.md | 24 ++++----- pointer_prominence/invalid_pointer.md | 6 +-- runtime/endless_loop.md | 10 ++-- runtime/noexcept.md | 18 +++---- runtime/recursion.md | 8 +-- syntax/const_launder.md | 52 ++++++++++---------- syntax/most_vexing_parse.md | 10 ++-- syntax/move.md | 20 ++++---- syntax/stl_constructors.md | 2 +- 20 files changed, 136 insertions(+), 136 deletions(-) diff --git a/README.md b/README.md index 7a20f89..4836437 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ # Содержание 0. [Как искать UB?](how_to_find_ub.md) -1. [Сужающие преобразования](numeric/narrowing.md) +1. [Сужающие преобразования](numeric/narrowing.md) 2. Целые и вещественные числа 1. [Переполнение знаковых целых чисел](numeric/overflow.md) 2. [Числа с плавающей точкой](numeric/floats.md) diff --git a/how_to_find_ub.md b/how_to_find_ub.md index 65dffd1..1f3b4ef 100644 --- a/how_to_find_ub.md +++ b/how_to_find_ub.md @@ -13,7 +13,7 @@ - PVS Studio - И другие -Достаточно умный анализатор, работающий с графом потока выполнения программы, знающий сотни ловушек стандарта, способен найти многие проблемы и привлечь внимание к сомнительному коду. Но не все и не всегда. +Достаточно умный анализатор, работающий с графом потока выполнения программы, знающий сотни ловушек стандарта, способен найти многие проблемы и привлечь внимание к сомнительному коду. Но не все и не всегда. ---- diff --git a/lifetime/decltype_auto_and_explicit_types.md b/lifetime/decltype_auto_and_explicit_types.md index 7e51963..78e90a0 100644 --- a/lifetime/decltype_auto_and_explicit_types.md +++ b/lifetime/decltype_auto_and_explicit_types.md @@ -10,7 +10,7 @@ template auto find(const std::vector& v, const T& x) { ... - // очень длинное тело со множеством return + // очень длинное тело со множеством return ... } ``` @@ -21,7 +21,7 @@ auto find(const std::vector& v, const T& x) { --- ## Проблема явного указания типа -Длинно и много писать — решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах. +Длинно и много писать — решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах. И все могло быть хорошо: поменяли где-то в объявлении, получили ошибки компиляции — исправили во всех местах, на которые указали ошибки. Но в C++ есть неявное приведение типов, которое особенно жестоко наказывает при использовании ссылок. @@ -29,7 +29,7 @@ auto find(const std::vector& v, const T& x) { std::map counters = { {"hello", 5}, {"world", 5} }; std::vector keys; // получаем список ключей, используем string_view, чтобы не делать лишних копий keys.reserve(counters.size()); -std::transform(std::begin(counters), +std::transform(std::begin(counters), std::end(counters), std::back_inserter(keys), [](const std::pair& item) -> std::string_view { @@ -45,7 +45,7 @@ for (std::string_view k : keys) { [Исправляем](https://godbolt.org/z/E6evof) ошибку, добавляя `const` перед `string`: ```C++ -[](const std::pair& item) -> std::string_view +[](const std::pair& item) -> std::string_view ``` Проходят недели, код рефакторится. `counters` отъезжают в поле какого-нибудь класса. Получение и обработка ключей — в его второстепенный метод. А потом внезапно выясняется, что тип значений в ассоциативном массиве надо бы поменять на меньший — пусть `short`. @@ -70,7 +70,7 @@ public: } } ``` -Опять ошиблись. Опять надо исправлять. +Опять ошиблись. Опять надо исправлять. `std::map` может в будущем поменяться на что-то другое, у чего итератор возвращает уже не настоящий `pair`, а прокси-объект. Универсальным решением будет в этом случае — `decltype(auto)` в качестве возвращаемого значения. @@ -121,10 +121,10 @@ public: } decltype(auto) Name2() const { - return (name); // ссылка. Выражение (name) имеет тип const T&: + return (name); // ссылка. Выражение (name) имеет тип const T&: // само по себе (name) — T&, но this помечен const, поэтому // получается const T& - } + } decltype(auto) ConstName() const { return const_name; // const копия. const_name объявлен как const T diff --git a/lifetime/for_loop.md b/lifetime/for_loop.md index 0f22d48..4097614 100644 --- a/lifetime/for_loop.md +++ b/lifetime/for_loop.md @@ -4,18 +4,18 @@ Правило хитрое и состоит не только в том, что `const&&` или `&&` продляют жизнь временному объекту (но только парвая такая ссылка). На самом деле правило такое: -все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(`;`). -ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта `const&` или `&&` ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта +все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(`;`). +ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта `const&` или `&&` ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта То есть: ```C++ -const int& x = 1 + 2; // временные объекты 1, 2, -// порождают временный объект 3 (сумма). -// Их время жизни закончится на ; -// Но мы присваиваем 3 константной ссылке, +const int& x = 1 + 2; // временные объекты 1, 2, +// порождают временный объект 3 (сумма). +// Их время жизни закончится на ; +// Но мы присваиваем 3 константной ссылке, // Ее область видимости простирается ниже, дальше ; -// Так что время жизни продлевается. +// Так что время жизни продлевается. // Таким образом: 1, 2 — умирают. 3 — продолжает жить @@ -24,11 +24,11 @@ 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`. Продлений жизни не происходит — @@ -76,7 +76,7 @@ public: return vertexes; } -private: +private: VertexList vertexes; }; @@ -107,7 +107,7 @@ auto&& begin_ = std::begin(container_); auto&& end_ = std::end(container_); for (; begin_ != end_; ++begin_) { T v = *begin_; -} +} ``` В первом случае @@ -115,7 +115,7 @@ for (; begin_ != end_; ++begin_) { auto&& container_ = MakeShape().vertexes; // временный объект Shape живет до ;. Он не встретил еще ни одной const& или && // ссылки -// Подобъект vertexes — считается таким же временным. +// Подобъект vertexes — считается таким же временным. // Его время жизни закончится на ; // Но он встречает && ссылку, область видимости которой простирается ниже // и продлевает ему жизнь. Причем придлевается жизнь не только лишь подобъекту @@ -125,10 +125,10 @@ auto&& container_ = MakeShape().vertexes; Во втором случае: ```C++ auto&& container_ = MakeShape().Vertexes(); -// временный объект Shape живет до ;. Но он встречает неявную const& +// временный объект Shape живет до ;. Но он встречает неявную const& // ссылку в методе Vertexes(). Ее область видимости ограничена телом метода. // Продления жини не происходит. Возвращается ссылка на часть временного объекта -// и присваивается ссылке `container_`. +// и присваивается ссылке `container_`. // Дело доходит до ;. Временный Shape умирает. // `container_` становится висячей ссылкой. Занавес. ``` @@ -148,7 +148,7 @@ auto&& container_ = MakeShape().Vertexes(); for (auto cont = expr; auto x : cont) ``` - При использовании синтаксиса с инициализатором думать, прежде чем использовать -`auto&&` или `const auto&` для инициализатора. Впрочем, это не только про for... +`auto&&` или `const auto&` для инициализатора. Впрочем, это не только про for... - Использовать `std::ranges::for_each` - Не использовать range-based for в C++, пока его не починят diff --git a/lifetime/lambda_capture.md b/lifetime/lambda_capture.md index 4a3b17e..58558db 100644 --- a/lifetime/lambda_capture.md +++ b/lifetime/lambda_capture.md @@ -29,7 +29,7 @@ struct Task { std::function GetNotifier() { return [this]{ // this — может стать висячей ссылкой! - std::cout << "notify " << id << "\n"; + std::cout << "notify " << id << "\n"; }; } }; @@ -48,7 +48,7 @@ struct Task { std::function GetNotifier() { return [=]{ // this — может стать висячей ссылкой! - std::cout << "notify " << id << "\n"; + std::cout << "notify " << id << "\n"; }; } }; @@ -68,7 +68,7 @@ struct Task { std::function GetNotifier() & { return [this]{ // для this теперь намного сложнее стать висячей ссылкой - std::cout << "notify " << id << "\n"; + std::cout << "notify " << id << "\n"; }; } }; diff --git a/lifetime/self_init.md b/lifetime/self_init.md index 5e950a7..1029084 100644 --- a/lifetime/self_init.md +++ b/lifetime/self_init.md @@ -1,6 +1,6 @@ # Еще не мертв, но еще и не жив. Self-reference -Область видимости объекта начинается сразу же после его объявления. В той же строчке. Поэтому +Область видимости объекта начинается сразу же после его объявления. В той же строчке. Поэтому в С++ очень легко сконструировать синтаксически корректное выражение, использующее еще не сконструированный объект. ```C++ @@ -11,10 +11,10 @@ int x = x + 5; // UB // менее явно const int max_v = 10; -void fun(int y) { +void fun(int y) { const int max_v = [&]{ // локальный max_v перекрывает глобальный max_v - return std::min(max_v, y); + return std::min(max_v, y); }(); ... } @@ -29,10 +29,10 @@ void fun(int y) { ```C++ const int max_v = 10; -void fun(int y) { +void fun(int y) { const int max_v = []{ // тут виден только глобальный max_v - return std::min(max_v, y); + return std::min(max_v, y); }(); ... } @@ -46,7 +46,7 @@ void fun(int y) { ```C++ struct ExtremelyLongClassName { - + using UnspeekableInternalType = size_t; UnspeekableInternalType val; @@ -57,19 +57,19 @@ struct ExtremelyLongClassName { ExtremelyLongClassName x { x.Default() + 5 }; // Ok, well-defined -ExtremelyLongClassName y { - [] ()-> ExtremelyLongClassName::UnspeekableInternalType { +ExtremelyLongClassName y { + [] ()-> ExtremelyLongClassName::UnspeekableInternalType { // сложные вычисления return 1; }() }; -ExtremelyLongСlassName z { +ExtremelyLongСlassName z { [] ()-> decltype(z.Default()) { // Ok, well-defined // сложные вычисления return 1; }() - }; + }; ``` Также эта фича может быть полезна в каких-то [специфических](https://godbolt.org/z/qY18c8) случаях, в которых вам зачем-то нужен объект, ссылающийся сам на себя @@ -83,7 +83,7 @@ struct Iface { struct Impl : Iface { explicit Impl(const Iface* other_ = nullptr) : other(other_) { - + }; int method(int x) const override { diff --git a/lifetime/string_view.md b/lifetime/string_view.md index f0c0c80..0a8cb2e 100644 --- a/lifetime/string_view.md +++ b/lifetime/string_view.md @@ -11,7 +11,7 @@ int count_char(const std::string& s, char c) { .... } -count_char("hello world", 'l'); // создастся временный объект std::string, +count_char("hello world", 'l'); // создастся временный объект std::string, // выделится память, скопируется строка, а потом строка умрет и память // деаллоцируется — плохо, много лишних операций ``` @@ -21,7 +21,7 @@ count_char("hello world", 'l'); // создастся временный объ int count_char(const char* s, char c) { // мы тут не знаем ничего про длину строки // она вообще null-териминированная? - + // Можем только написать код, наивно рассчитывающий, что его // будут вызывать правильно. ... @@ -32,7 +32,7 @@ int count_char(const char* s, char c) { ```C++ int count_char_impl(const char* s, size_t len, char c) { - ... + ... } ``` @@ -85,12 +85,12 @@ std::string_view common_prefix(std::string_view a, std::string_view b) { int main() { using namespace std::string_literals; { - auto common = common_prefix("helloW", + auto common = common_prefix("helloW", "hello"s + "World111111111111111111111"); std::cout << common << "\n"; // ok } { - auto common = common_prefix("hello"s + "World111111111111111111111111", + auto common = common_prefix("hello"s + "World111111111111111111111111", "helloW"); std::cout << common << "\n"; // dangling ref } diff --git a/lifetime/use_after_free_in_general.md b/lifetime/use_after_free_in_general.md index 5c49d20..9fe384a 100644 --- a/lifetime/use_after_free_in_general.md +++ b/lifetime/use_after_free_in_general.md @@ -39,7 +39,7 @@ int main() { Проблема в том, что `std::min` объявлен как ```C++ -template const T& min(const T& a, const T& b); +template const T& min(const T& a, const T& b); ``` Число `10` является временным объектом (_prvalue_), который умирает сразу же по выходе из функции `std::min`. @@ -47,7 +47,7 @@ template const T& min(const T& a, const T& b); В C++ разрешено присваивать временные объекты константным ссылкам. В таком случае константная ссылка продлевает временному объекту жизнь (объект "материализуется") и живет до выхода ссылки из области видимости. Дальнейшие присваивания константным ссылкам эффекта продлевания времени жизни не имеют. Любой код, возвращающий из функции или метода ссылку или сырой указатель, является потенциальным источником проблем где угодно. -Код, который только принимает аргументы по ссылке и никуда эти ссылки не сохраняет, также может быть источником проблем, но в куда более неочевидных ситуациях. +Код, который только принимает аргументы по ссылке и никуда эти ссылки не сохраняет, также может быть источником проблем, но в куда более неочевидных ситуациях. ```C++ template @@ -79,8 +79,8 @@ std::vector append_n_copies(std::vector elements, T x, int N) { void foo() { std::vector v; v.push_back(10); ... - // v = append_n_copies(std::move(v), v.front(), 5); - // UB, use-after-move, порядок вычисления аргументов неопределен: + // v = append_n_copies(std::move(v), v.front(), 5); + // UB, use-after-move, порядок вычисления аргументов неопределен: // v.front() может быть вызван на пустом векторе auto el = v.front(); @@ -98,7 +98,7 @@ void foo() { #include template -std::reference_wrapper safe_min(std::reference_wrapper a, +std::reference_wrapper safe_min(std::reference_wrapper a, std::reference_wrapper b){ return std::min(a, b); } @@ -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 — можно безопасно вернуть ссылку - return std::min(a, b); + // оба аргумента были lvalue — можно безопасно вернуть ссылку + return std::min(a, b); } else { - // один из аргументов — временный объект. + // один из аргументов — временный объект. // возвращаем по значению. // для этого делаем копию auto temp = std::min(a,b); // auto&& нельзя! @@ -183,12 +183,12 @@ int main() { ```C++ class VectorBuilder { ... - const std::vector& GetVector() & { + const std::vector& GetVector() & { std::cout << "As Lvalue\n"; - return v; + return v; } - std::vector GetVector() && { + std::vector GetVector() && { std::cout << "As Rvalue\n"; return std::move(v); } @@ -226,7 +226,7 @@ auto&& builder = VectorBuilder{}.Append(1).Append(2).Append(3); Чтобы этого избежать, нам нужно: -Либо настраивать линтер, запрещающий использовать `auto&&` и `const auto&` c этим классом в правой части. +Либо настраивать линтер, запрещающий использовать `auto&&` и `const auto&` c этим классом в правой части. Либо жертвовать производительностью, и в rvalue версии `Append` возвращать по значению (+ move) — при большом количестве примитивных, всегда копируемых, объектов внутри, просадка будет заметной. diff --git a/lifetime/vector_invalidation.md b/lifetime/vector_invalidation.md index e9d2dbc..4ad8262 100644 --- a/lifetime/vector_invalidation.md +++ b/lifetime/vector_invalidation.md @@ -74,7 +74,7 @@ void run_actions(std::vector actions) { В языке Rust проблема ловится на этапе компиляции с помощью borrow checker'а. -Если вы можете позволить себе просадку производительности, лучше использовать специализированные контейнеры (или адапторы контейнеров) для специфичных задач. +Если вы можете позволить себе просадку производительности, лучше использовать специализированные контейнеры (или адапторы контейнеров) для специфичных задач. Так `std::queue` по умолчанию использует `std::deque` и не инвалидирует ссылки при добавлении новых элементов. А также ее нельзя неосторожно использовать в range-based-for — у нее нет итераторов begin/end ## Полезные ссылки diff --git a/numeric/floats.md b/numeric/floats.md index a66bd73..486d1b6 100644 --- a/numeric/floats.md +++ b/numeric/floats.md @@ -3,12 +3,12 @@ С `float` и `double` в принципе всегда все сложно. Особенно в C++. Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что -по IEEE 754 выражение `x/0.0` определяется как `-INF`, `NaN`, или `INF` в зависимости от знака числа `x` (`NaN` для нуля). +по IEEE 754 выражение `x/0.0` определяется как `-INF`, `NaN`, или `INF` в зависимости от знака числа `x` (`NaN` для нуля). Сравнение вещественных чисел — излюбленная головная боль. Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же — [не стоит](https://godbolt.org/z/q55891). -Выражение `x == y` фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями `-0.0` и `+0.0`, и `NaN`. +Выражение `x == y` фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями `-0.0` и `+0.0`, и `NaN`. О существовании этого и `!=` операторов для вещественных чисел стоит забыть и никогда не вспонимать. Для побитового сравнения нужно использовать `memcmp`. diff --git a/numeric/narrowing.md b/numeric/narrowing.md index c06b4cf..c507142 100644 --- a/numeric/narrowing.md +++ b/numeric/narrowing.md @@ -4,7 +4,7 @@ Так в Rust, Haskell, Kotlin нельзя просто так использовать `float` и `int` в одном арифметическом выражении, без явного указания преобразовать одно в другое. Python не так строг, но все же не дает смешивать строки, символы и числа. -В С++ запрета неяного преобразования нет, что порождает массу ошибочного кода. Причем в таком коде может быть как определенное, но неожиданное, так неопределенное поведение. +В С++ запрета неяного преобразования нет, что порождает массу ошибочного кода. Причем в таком коде может быть как определенное, но неожиданное, так неопределенное поведение. Пример: @@ -49,7 +49,7 @@ int main() { #include int main() { - std::cout << abs(3.5) << "\n"; // функция библиотеки С, + std::cout << abs(3.5) << "\n"; // функция библиотеки С, // принимает на вход тип long // результат — 3 std::cout << std::abs(3.5); // функция библиотеки С++ @@ -65,7 +65,7 @@ int main() { int main() { std::string s; - s += 48; // неявное приведение к char. + s += 48; // неявное приведение к char. s += 1000; // а тут еще и с переполнением, очень неприятным // на платформе с signed char. s += 49.5; // опять-таки неявное приведение к char diff --git a/numeric/overflow.md b/numeric/overflow.md index 778d005..289f398 100644 --- a/numeric/overflow.md +++ b/numeric/overflow.md @@ -11,7 +11,7 @@ ``` x = 2^31 - 1 iadd x 5 -``` +``` произойдет перенос разряда в знаковый бит, и переменная `x` примет отрицательное значение. В реализации конкретного языка программирования может быть проверка флага переполнения и сообщение об ошибке. А может и не быть. Может быть гарантия "цикличности" значений (после `2^31-1` идет `-2^31`), а может и не быть. @@ -45,7 +45,7 @@ namespace safe { // Все эти проверки справедливы только для целых знаковых чисел template -concept SignedInteger = std::is_signed_v +concept SignedInteger = std::is_signed_v && std::is_integral_v; enum class ArithmeticError { @@ -68,7 +68,7 @@ ErrorOrInteger add(I a, // выключаем вывод параметр // отрицательное переполнение return ArithmeticError::Overflow; } - return a + b; + return a + b; } template @@ -81,7 +81,7 @@ ErrorOrInteger sub(I a, std::type_identity_t b) { // отрицательное переполнение return ArithmeticError::Overflow; } - return a - b; + return a - b; } template @@ -89,13 +89,13 @@ ErrorOrInteger mul(I a, std::type_identity_t b) { if (a == 0 || b == 0) { return 0; } - - if (a > 0) { - if (b > 0) { + + if (a > 0) { + if (b > 0) { if (a > std::numeric_limits::max / b) { return ArithmeticError::Overflow; } - } else { + } else { if (b < std::numeric_limits::min / a) { return ArithmeticError::Overflow; } @@ -110,7 +110,7 @@ ErrorOrInteger mul(I a, std::type_identity_t b) { return ArithmeticError::Overflow; } } - } + } return a * b; } @@ -138,9 +138,9 @@ ErrorOrInteger mod(I a, std::type_identity_t b) { if (b == -1) { // По стандарту в этом случае также неопределенное поведение при // a == std::numeric_limits::min - // поскольку остаток и неполное частное от деления, + // поскольку остаток и неполное частное от деления, // например, на платформе x86 - // получаются одной и той же инструкцией div (idiv), + // получаются одной и той же инструкцией div (idiv), // что потребует дополнительной обработки. // // Но совершенно ясно, что остаток от деления чего угодно на -1 равен 0 @@ -170,7 +170,7 @@ ErrorOrInteger mod(I a, std::type_identity_t b) { Стоит заметить, что сужающее преобразование из целочисленного типа в другой целочисленный тип к неопределенному поведению не приводит, и выполнять побитовое `и` с маской перед присваиванием переменной меньшего типа не обязательно. Но желательно, чтобы избежать предупреждений компилятора ```C++ - constexpr int x = 12345678; + constexpr int x = 12345678; constexpr uint8_t first_byte = x; // Implicit cast. Warning ``` diff --git a/pointer_prominence/invalid_pointer.md b/pointer_prominence/invalid_pointer.md index 036b3e5..d2f18ca 100644 --- a/pointer_prominence/invalid_pointer.md +++ b/pointer_prominence/invalid_pointer.md @@ -1,6 +1,6 @@ # Указатель Шредингера: валиден и невалиден одновременно. -Что вообще такое указатель? +Что вообще такое указатель? Когда их пытаются объяснить новичкам в C++, часто говорять, что это число, адрес, указывающий на номер ячейки в памяти, где что-то лежит. Это в каком-то смысле справедливо на очень низком уровне — в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется. @@ -8,7 +8,7 @@ Указатель — это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации. -Для указателей в стандарте C++ подробно расписано, откуда они могут появляться. +Для указателей в стандарте C++ подробно расписано, откуда они могут появляться. Если коротко, то: 1. Как результат применения операции взятия адреса (`&x` или `std::addressof(x)`) 2. Как результат вызова оператора `new` (возможно, _placement new_) @@ -104,7 +104,7 @@ private: ``` Этот код с неопределенным поведением. Скорее всего, оно никак не проявится сейчас, но не значит, что так будет и в будущем. -Возможно, вызов `reallocate` заинлайнится в неподходящем месте и все пойдет вверх дном. +Возможно, вызов `reallocate` заинлайнится в неподходящем месте и все пойдет вверх дном. diff --git a/runtime/endless_loop.md b/runtime/endless_loop.md index d63a476..78d585e 100644 --- a/runtime/endless_loop.md +++ b/runtime/endless_loop.md @@ -9,7 +9,7 @@ Занятный пример — таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/nE7oWf) ```C++ #include - + int fermat () { const int MAX = 1000; int a=1,b=1,c=1; @@ -26,7 +26,7 @@ int fermat () { } if (c>MAX) { c=1; - } + } } return 0; } @@ -64,13 +64,13 @@ int fermat() { } if (c>MAX) { c=1; - } + } } return 1; } ``` -Даже если в цикле будут операции I/O, он все равно [может исчезнуть](https://godbolt.org/z/P8YxeT), +Даже если в цикле будут операции I/O, он все равно [может исчезнуть](https://godbolt.org/z/P8YxeT), если компилятор увидит, что эти операции от цикла не зависят ```C++ int fermat () { @@ -92,7 +92,7 @@ int fermat () { } if (c>MAX) { c=1; - } + } } return 0; } diff --git a/runtime/noexcept.md b/runtime/noexcept.md index 39dfd74..85f1841 100644 --- a/runtime/noexcept.md +++ b/runtime/noexcept.md @@ -4,13 +4,13 @@ И вроде бы все хорошо: получив такую информацию, компилятор может не генерировать дополнительные инструкции для обработки раскрутки стека. Бинарники становятся меньше, а программы быстрее. -Но проблема в том, что этот спецификатор не заставляет компиляторы провериять, +Но проблема в том, что этот спецификатор не заставляет компиляторы провериять, что функция действительно не бросает исключений. -Если мы пометим фунцию как `noexcept`, а она возьмет да кинет исключение, +Если мы пометим фунцию как `noexcept`, а она возьмет да кинет исключение, произойдет что-то странное, заканчивающееся внезапным `std::terminate`. -Так, например, неожиданно [перестанут работать](https://godbolt.org/z/E9c9Ya) `try-catch` блоки. +Так, например, неожиданно [перестанут работать](https://godbolt.org/z/E9c9Ya) `try-catch` блоки. ```C++ void may_throw(){ @@ -20,12 +20,12 @@ void may_throw(){ struct WrongNoexcept { WrongNoexcept() noexcept { may_throw(); - } + } }; // Попытки обернуть в try-catch эту функцию или любой код, -// использующий ее — бесполезны. -void throw_smth() { +// использующий ее — бесполезны. +void throw_smth() { if (rand() % 2 == 0) { throw std::runtime_error("throw"); } else { @@ -49,7 +49,7 @@ void throw_smth() { Есть спецификатор `noexcept(condition)`. И просто `noexcept` — синтаксический сахар для конструкции `noexcept(true)`. -А есть предикат `noexcept(expr)`, проверяющий, что выражение `expr` не кидает исключений по самой своей природе (сложение чисел, например) или же +А есть предикат `noexcept(expr)`, проверяющий, что выражение `expr` не кидает исключений по самой своей природе (сложение чисел, например) или же помечено как `noexcept`. И вместе они порождают конструкцию для условного навешивания noexcept: @@ -65,7 +65,7 @@ void may_throw(){ struct ConditionalNoexcept { ConditionalNoexcept() noexcept(noexcept(may_throw())) { may_throw(); - } + } }; // теперь с этой функцией все хорошо @@ -78,7 +78,7 @@ void throw_smth() { } ``` -Чтобы избежать проблем, нужно всегда и везде использовать условный `noexcept` с аккуратной проверкой каждой используемой функции, либо вовсе не использовать `noexcept`. Но во втором случае стоит помнить, +Чтобы избежать проблем, нужно всегда и везде использовать условный `noexcept` с аккуратной проверкой каждой используемой функции, либо вовсе не использовать `noexcept`. Но во втором случае стоит помнить, что операции перемещения, а также `swap` должны помечаться как `noexcept` (и быть действительно `noexcept`!) для эффективной работы со стандартными контейнерами. Не забывайте писать негативные тесты. Без них diff --git a/runtime/recursion.md b/runtime/recursion.md index ebb26c7..ec3c77c 100644 --- a/runtime/recursion.md +++ b/runtime/recursion.md @@ -1,9 +1,9 @@ # Рекурсия -Многие алгоритмы очень красиво и компактно записываются в рекурсивной форме. +Многие алгоритмы очень красиво и компактно записываются в рекурсивной форме. Сортировки, обходы графов, строковые алгоритмы. -Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке. +Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке. Конечно, есть хвостовая рекурсия, которая естественным образом может быть оптимизирована в цикл. Но это не гарантировано стандартом. Да и не всегда рекурсия именно хвостовая. Stack overflow не совсем неопределенное поведение, но точно не то, чего хочется видеть на боевом стенде. Потому в серьезных приложениях предпочитают итеративные алгоритмы рекурсивным. Если, конечно, нет гарантии, что глубина рекурсии мала. @@ -27,7 +27,7 @@ struct Node { Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева... А очередь это аллокация памяти. А аллокация памяти — операция, бросающая исключения. И вот у нас деструктор будет бросать исключения. Что совсем не хорошо. -Можно написать деструктор без аллокаций и рекурсии. Но его алгоритмическая сложность будет квадратичной: +Можно написать деструктор без аллокаций и рекурсии. Но его алгоритмическая сложность будет квадратичной: 1. Находим вершину, у которой последний элемент в векторе потомков является листом. 2. Удаляем этот элемент из вектора. @@ -52,7 +52,7 @@ struct List { ~List() { while (next) { - // деструктор все также рекурсивен, + // деструктор все также рекурсивен, // но теперь глубина рекурсии — 1 вызов next = std::move(next->next); } diff --git a/syntax/const_launder.md b/syntax/const_launder.md index 03d854c..808b047 100644 --- a/syntax/const_launder.md +++ b/syntax/const_launder.md @@ -5,7 +5,7 @@ А иногда будет неопределенное поведение, segfault, и прочие радости жизни. Разница между этими "иногда" в том, что есть настоящие константы, попытка модификации которых — UB. -А есть ссылки на константы, ссылающиеся не на константы. +А есть ссылки на константы, ссылающиеся не на константы. И раз на самом деле объект неконстантен, то модифицировать его можно без проблем. Так, например, эту "фичу" можно [эксплуатировать](https://godbolt.org/z/oW4YeP), чтобы не повторять один и тот же код для `const` и не-`const` методов класса @@ -27,8 +27,8 @@ public: } int& get_for_val_or_abs_val(int val) { - return const_cast( // отбрасываем const с результата. - // находясь в неконстантном методе, мы знаем, что результат + return const_cast( // отбрасываем const с результата. + // находясь в неконстантном методе, мы знаем, что результат // в действительности не является константой — проблем не будет. std::as_const(*this) // навешиваем const, чтобы вызвать const-метод, // а не уйти в бесконечную рекурсию @@ -46,7 +46,7 @@ private: По возможности, стоит избегать такого кода. Видно, что он очень хрупок — забытый или случайно удаленный `std::as_const` ломает его. И без настройки предупреждений, компиляторы об этом сообщать [не торопятся](https://godbolt.org/z/17fc31). -Вместо использования `const_cast` и привнесения в мир C++ еще большей нестабильности, +Вместо использования `const_cast` и привнесения в мир C++ еще большей нестабильности, решить проблему диблирования кода можно с помощью шаблонного метода: ```C++ class MyMap { @@ -89,7 +89,7 @@ private: Операции над иммутабельными данными отлично оптимизируются, распараллеливаются, и вообще ведут себя здорово. -Однако, возможность заигрывать со снятием и навешиванием `const` где угодно в коде, +Однако, возможность заигрывать со снятием и навешиванием `const` где угодно в коде, вообще говоря, исключает этот ряд оптимизаций. Так повторное обращение по константной ссылке к одному и тому же полю или методу совсем не обязано кэшироваться. @@ -100,8 +100,8 @@ using predicate = bool (*) (int); int count_if(const std::vector& v, predicate p) { int res = 0; - for (size_t i = 0; i < v.size(); ++i) { // значение v.size() нельзя - // единожды сохранить в регистре + for (size_t i = 0; i < v.size(); ++i) { // значение v.size() нельзя + // единожды сохранить в регистре if (p(v[i])) { //конкретный p может иметь доступ к изменяемой ссылке на // этот же самый v ++res; @@ -132,7 +132,7 @@ int main() { Этот код очень плох. Он не должен нигде встречаться; его никто не пропустит на ревью. Но так теоретически написать можно, поэтому оптимизация невыполняется. -Если в качестве типа предиката использовать шаблонный параметр, +Если в качестве типа предиката использовать шаблонный параметр, можно привести куда более изощренные примеры без привлечения глобальных переменных. Учитывая ограниченные возможности на автоматическую оптимизацию, подобный цикл переписывают (делая ту самую работу, которую ждали от компилятора): @@ -141,9 +141,9 @@ int count_if(const std::vector& v, predicate p) { int res = 0; for (auto x : v) { // range-based-for не обращается к size() - // а один раз считывает begin/end указатели и работает + // а один раз считывает begin/end указатели и работает // с ними; разность begin - end не рассчитывается - if (p(v[i])) { + if (p(v[i])) { ++res; } } @@ -154,10 +154,10 @@ int count_if(const std::vector& v, predicate p) { Вот менее тривиальный пример `const`, никак не способствующего оптимизации: ```C++ -void find_last_zero_pos(const std::vector& v, +void find_last_zero_pos(const std::vector& v, const int* *poiner_to_last_zero) { *poiner_to_last_zero = 0; - for (size_t i = 0; i < v.size(); ++i) { // мы опять не можем один раз + for (size_t i = 0; i < v.size(); ++i) { // мы опять не можем один раз // сохранить значение v.size() if (v[i] == 0) { // внутри вектора есть поля типа int* — begin, end @@ -169,7 +169,7 @@ void find_last_zero_pos(const std::vector& v, } ``` -Оставаясь в рамках рекомендуемых практик написания C++ программ, +Оставаясь в рамках рекомендуемых практик написания C++ программ, мы не можем соорудить пример, который бы демонстрировал неприменимость оптимизации — нам мешает инкапсуляция. До приватных полей вектора мы не можем законно добраться. @@ -184,12 +184,12 @@ int main() { И вот мы имеем парадоксальный результат: возможность написать явно некорректный код, запрещает компилятору оптимизировать цикл! И вся концепция неопределенного поведения как возможности для оптимизации (некорректного кода не бывает) разваливается. -Что ж, по крайней мере, для этого примера имеется некоторая стабильность: +Что ж, по крайней мере, для этого примера имеется некоторая стабильность: Исходный цикл со счетчиком и переписанный на range-based-for закончатся на неопределенном поведении. В современных языках (например, в Rust, благодаря семантике владения), все эти циклы могут быть успешно оптимизированы. -## Сonst, время жизни и происхождение указателей. +## Сonst, время жизни и происхождение указателей. Неизменяемые объекты всем хороши, кроме одного — это константные объекты в C++. Если они где-то засели, то их оттуда по-нормальному не выгонишь. @@ -205,7 +205,7 @@ struct Unit { }; ``` -Из-за константного поля объекты `Unit` теряют операцию присваивания. +Из-за константного поля объекты `Unit` теряют операцию присваивания. Их нелья менять местами — `std::swap` не работает больше. `std::vector` нельзя больше просто так отсортировать... В общем, сплошное удобство. @@ -222,7 +222,7 @@ std::cout << unit.back().id << ""; В зависимости от того, смогли ли при реализации вектора задушить агрессивные оптимизации компилятора, такой код может вывести либо `1 2 ` (все хорошо), либо `1 1 ` (компилятор соптимизировал константное поле!) -Компилятор имеет право воспринимать происходящее следующим образом: +Компилятор имеет право воспринимать происходящее следующим образом: - В векторе 1 элемент - Вектор не реаллоцировался. - Указатель на элемет в первом `cout` и во втором `cout` один и тот же. @@ -232,7 +232,7 @@ std::cout << unit.back().id << ""; - Вывожу закэшированное значение. К сожалению или к счастью, воспроизвести подобное поведение компилятора на практике не получается. -Тем не менее, вот такой код, который может использоваться для реализации самописных `std::optional`, +Тем не менее, вот такой код, который может использоваться для реализации самописных `std::optional`, по стандарту содержит UB (и не одно!) ```C++ @@ -251,14 +251,14 @@ std::cout << unit.back().id << ""; using storage = std::aligned_storage_t; storage s; auto p = new (&s) Unit{1,2}; - std::cout << rp->id << "\n"; - p->~Unit(); + std::cout << rp->id << "\n"; + p->~Unit(); p = new (&s) Unit{2,2}; std::cout << p->id << "\n"; - p->~Unit(); + p->~Unit(); ``` -Но поддерживать указатель, взвращенный оператором `new`, не всегда возмножно. +Но поддерживать указатель, взвращенный оператором `new`, не всегда возмножно. Он занимает место, его надо хранить, что неэффективно при реализации `optional`: для `int32_t` будет нужно в три раза больше места на 64хбитной системе (4 байта на `storage` + 8 байт на указатель)! Поэтому в стандартной библиотеке с C++17 есть функция "отмывания" невесть откуда взявшихся указателей — `std::launder`. @@ -267,11 +267,11 @@ std::cout << unit.back().id << ""; using storage = std::aligned_storage_t; storage s; new (&s) Unit{1,2}; - std::cout << std::launder(reinterpret_cast(&s))->id << "\n"; - std::launder(reinterpret_cast(&s))->~Unit(); + std::cout << std::launder(reinterpret_cast(&s))->id << "\n"; + std::launder(reinterpret_cast(&s))->~Unit(); new (&s) Unit{2,2}; - std::cout << std::launder(reinterpret_cast(&s))->id << "\n"; - std::launder(reinterpret_cast(&s))->~Unit(); + std::cout << std::launder(reinterpret_cast(&s))->id << "\n"; + std::launder(reinterpret_cast(&s))->~Unit(); ``` Так и при чем тут `const`? "Настоящая" константность (переменные и поля объявленные с `const`) вместе с UB при использовании "неправильных" указателей, как раз и позволяют компилятору производить описанные спецэффекты. diff --git a/syntax/most_vexing_parse.md b/syntax/most_vexing_parse.md index 4df2f32..d9ece92 100644 --- a/syntax/most_vexing_parse.md +++ b/syntax/most_vexing_parse.md @@ -1,6 +1,6 @@ # Most Vexing Parse -Помимо неопределенного поведения, в C++ есть неожиданное поведение, +Помимо неопределенного поведения, в C++ есть неожиданное поведение, произрастающее из следующих фантастических возможностей языка. Пользовательские типы и функции можно *объявлять* [где попало и как попало](https://godbolt.org/z/MWszrj) @@ -13,7 +13,7 @@ struct STagged {}; using S1 = STagged; // преобъявление струкруты Tag1 using S2 = STagged; // преобъявление струкруты Tag2 -void fun(struct Tag3*); // предобъявление структуры Tag3 +void fun(struct Tag3*); // предобъявление структуры Tag3 void external_fun() { int internal_fun(); // предобъявление функции! @@ -30,7 +30,7 @@ int main() { } ``` -При этом *определять* сущности можно не везде. +При этом *определять* сущности можно не везде. Типы можно определять локально — внутри функции. А функции определять нельзя. ```C++ @@ -74,7 +74,7 @@ int main() { Подобная ошибка может быть труднообнаружима, если случайно предобъявленная функция используется в контексте приведения к `bool` или если объект, который хотели сконструировать, сам является вызываемым (у него перегружен `operator()`). -Может показаться, что виноват именно конструктор по умолчанию класса `Timer`. На самом деле, виноват C++. +Может показаться, что виноват именно конструктор по умолчанию класса `Timer`. На самом деле, виноват C++. В нем можно объявлять функции вот так: ```C++ @@ -86,7 +86,7 @@ void fun(int (val)); // скобки вокруг имени параметра ```C++ int main() { const int time_to_work = 10; - Worker w(Timer(time_to_work)); // предобъявление функции, которая возвращает Worker + Worker w(Timer(time_to_work)); // предобъявление функции, которая возвращает Worker // и принимает параметр типа Timer. time_to_work — имя этого параметра! std::cout << w; diff --git a/syntax/move.md b/syntax/move.md index c5ba680..4234cfd 100644 --- a/syntax/move.md +++ b/syntax/move.md @@ -14,7 +14,7 @@ ```C++ void run_task(std::unique_ptr ptask) { // do something - ptask->go(); + ptask->go(); } void run(...){ @@ -24,7 +24,7 @@ void run(...){ } ``` При вызове `run_task` параметр передается по значению: создается новый объект `unique_ptr`, а старый останется, -но окажется пустым. Раз два объекта, то и два вызова деструктора. С деструктивной +но окажется пустым. Раз два объекта, то и два вызова деструктора. С деструктивной семантикой перемещения (например, в Rust) вызов деструктора будет только один. Можно исправить ситуацию — передать по rvalue-ссылке: @@ -32,7 +32,7 @@ void run(...){ ```C++ void run_task(std::unique_ptr&& ptask) { // do something - ptask->go(); + ptask->go(); } ``` @@ -62,11 +62,11 @@ void test_v2(){ ## Use-after-move -Во-первых, функция `std::move` ничего не делает. +Во-первых, функция `std::move` ничего не делает. Это всего лишь явное преобразование lvalue-ссылки в rvalue. Оно никак не влияет на состояние объкта. Обозреваемые эффекты от перемещения могут давать функции, работающие с этой самой rvalue-ссылкой. В основном это конструкторы и операторы перемещения. -Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение. +Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение. Оно должно быть валидным в смысле вызова деструктора. Но более ничего не требуется. Объект не обязан быть пустым после перемещения. Его поля не обязаны быть зануленными. Так у `std::thread` после перемещения нельзя вызывать ни один из методов. А `std::unique_ptr` гарантированно становится пустым (`nullptr`). Чаще всего и проще всего натолкнуться на use-after-move можно при реализцации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам. @@ -74,7 +74,7 @@ void test_v2(){ ```C++ struct Person { public: - Person(std::string first_name, + Person(std::string first_name, std::string last_name) : first_name_(std::move(first_name)), last_name_(std::move(last_name)) { std::cerr << first_name; // wrong, use-after-move @@ -89,7 +89,7 @@ private: ```C++ template - Person(T1 first_name, + Person(T1 first_name, T2 last_name) : first_name_(std::move(first_name)), last_name_(std::move(last_name)) { std::cerr << first_name; // wrong, use-after-move @@ -119,7 +119,7 @@ void remove_if(std::vector& v, P&& predicate) { } ``` -[Ошибка](https://godbolt.org/z/qY5MMn) не даст о себе знать до тех пор, пока элементы контейнера не будут содержать +[Ошибка](https://godbolt.org/z/qY5MMn) не даст о себе знать до тех пор, пока элементы контейнера не будут содержать полей, не учитывающих возможность самоприсваивания. ```C++ @@ -145,9 +145,9 @@ for (const auto& p : persons){ ```C++ MyType& operator=(MyType&& other) noexcept { - if (this == std::addressof(other)) { // addressof сработает, + if (this == std::addressof(other)) { // addressof сработает, // если у вас перегружен & - return *this; + return *this; } ... } diff --git a/syntax/stl_constructors.md b/syntax/stl_constructors.md index 51ffd74..9f4ed33 100644 --- a/syntax/stl_constructors.md +++ b/syntax/stl_constructors.md @@ -1,7 +1,7 @@ # Перегруженные конструкторы стандартной библиотеки При проектировании стандартной библиотеки C++ было принято множество странных -решений, из-за которых приходится страдать. И исправить их не представляется +решений, из-за которых приходится страдать. И исправить их не представляется возможным из-за соображений обратной совместимости. Одним из таких странных решений являются перегрузки конструкторов с радикально