diff --git a/numeric/floats.md b/numeric/floats.md index 49b2c33..e744102 100644 --- a/numeric/floats.md +++ b/numeric/floats.md @@ -8,7 +8,7 @@ Сравнение вещественных чисел — излюбленная головная боль. Выражение `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) сравниваемых чисел. diff --git a/numeric/integer_promotion.md b/numeric/integer_promotion.md index 331c8bb..dfbea2d 100644 --- a/numeric/integer_promotion.md +++ b/numeric/integer_promotion.md @@ -32,7 +32,7 @@ auto d = x / y; ## К чему это приводит? 1. К ошибкам в логике: - Неявные преобразования вовлекаются в любую операцию. Вы выполняете сравнение знакового и беззнакового числа и забыли явно привести типы? Готовтесь к тому, что `-1 < 1` может [вернуть](https://godbolt.org/z/sqvrasjE4) `false`: + Неявные преобразования вовлекаются в любую операцию. Вы выполняете сравнение знакового и беззнакового числа и забыли явно привести типы? Готовьтесь к тому, что `-1 < 1` может [вернуть](https://godbolt.org/z/sqvrasjE4) `false`: ```C++ std::vector v = {1}; auto idx = -1; diff --git a/numeric/narrowing.md b/numeric/narrowing.md index 09c6909..9de8e54 100644 --- a/numeric/narrowing.md +++ b/numeric/narrowing.md @@ -31,9 +31,9 @@ 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` +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`. Старший бит результата окажется нулевым. +4. Далее будет произведено деление беззнаковых чисел. `(2^N - 3) / 3`. Старший бит результата окажется нулевым. 5. Возвращаемым типом функции `average` объявлен `int`. Так что нужно выполнить еще одно неявное преобразование. 6. В общем случае преобразование unsigned -> signed определяется реализацией (implementation defined). 1. Если размеры типов `int` и `size_t` одинаковые, то, поскольку старший бит нулевой, положительное число укладывается в допустимый диапазон значений для типа `int` — стандарт гарантирует, что никаких проблем нет @@ -118,7 +118,7 @@ void g(int&& v) { // Неявное преобразование int& к int&& запрещено // int&& x = 5; - // int&& y = x; // не компилируеся! + // int&& y = x; // не компилируется! // Таким образом перегрузка f(int&&) не может быть использована @@ -157,15 +157,15 @@ int main() { } ``` -Той же самой цепочкой преоразований [получим](https://godbolt.org/z/bncnPj) в выводе `bool 1`. +Той же самой цепочкой преобразований [получим](https://godbolt.org/z/bncnPj) в выводе `bool 1`. Разве что последний шаг не нужен. --- -Обязателько включайте предупреждения компилятора обо всех неявных преобразованиях. Очень желательно трактовать их как ошибки. +Обязательно включайте предупреждения компилятора обо всех неявных преобразованиях. Очень желательно трактовать их как ошибки. -Не привносите невные преобразования для своих типов — всегда помечайте однопараметрические конструкторы как `explicit`. +Не привносите неявные преобразования для своих типов — всегда помечайте однопараметрические конструкторы как `explicit`. Если перегружаете операторы приведения (`operator T()`) для своих типов — также делайте их `explicit`. diff --git a/numeric/overflow.md b/numeric/overflow.md index 09c05b0..505e639 100644 --- a/numeric/overflow.md +++ b/numeric/overflow.md @@ -29,7 +29,7 @@ if (x > 0 && a > 0 && x + a <= 0) { } ``` -Но, увы, это неопреденной поведение. И компилятор имеет полное право [выкинуть](https://godbolt.org/z/dhs83T) такую проверку. +Но, увы, это неопределенное поведение. И компилятор имеет полное право [выкинуть](https://godbolt.org/z/dhs83T) такую проверку. Искусственный пример может быть недостаточно убедительным, так что обратим внимание на следущую, вполне серьезную, функцию вычисления полиномиального хэша строки: ```C++ @@ -171,9 +171,9 @@ ErrorOrInteger mod(I a, std::type_identity_t b) { Однако, если ваша программа только и делает, что ожидает и выполняет IO операции, то траты в два раза большего числа тактов на сложение или умножение никто и не заметит. Да и язык C++ для таких программ чаще всего не лучший выбор. -Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел). +Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнении никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел). -Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные програмы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да? +Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные программы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да? Для выведения ограничений вам помогут отладочные `assert` с правильными проверками переполнения, которые нужно написать. Или включение `ubsan` (_undefined behavior sanitizer_) при сборке компиляторами `clang` или `gcc`. А также тестовые `constexpr` вычисления. diff --git a/runtime/noexcept.md b/runtime/noexcept.md index ccf67e1..70368b5 100644 --- a/runtime/noexcept.md +++ b/runtime/noexcept.md @@ -7,7 +7,7 @@ Но проблема в том, что этот спецификатор не заставляет компиляторы проверять, что функция действительно не бросает исключений. -Если мы пометим фунцию как `noexcept`, а она возьмет да кинет исключение, +Если мы пометим функцию как `noexcept`, а она возьмет да кинет исключение, произойдет что-то странное, заканчивающееся внезапным `std::terminate`. Так, например, неожиданно [перестанут работать](https://godbolt.org/z/E9c9Ya) `try-catch` блоки. @@ -43,7 +43,7 @@ void throw_smth() { - `= 0` для объявления чисто виртуальных методов - новый `requires` имеет два значения, порождая странные конструкции `requires(requires(...))` - `auto` и для автовывода, и для переключения на trailing return type -- `decltype`, у которого разный смысл при примении к переменной и к выражению +- `decltype`, у которого разный смысл при применении к переменной и к выражению - и, конечно, `noexcept` — точно также два значения как у `requires`. Есть спецификатор `noexcept(condition)`. И просто `noexcept` — синтаксический сахар diff --git a/runtime/reserved_names.md b/runtime/reserved_names.md index 0a65ee2..4ec963a 100644 --- a/runtime/reserved_names.md +++ b/runtime/reserved_names.md @@ -7,7 +7,7 @@ Некоторые имена запрещены самим стандартом C/C++. Некоторые — стандартами POSIX. Некоторые — платформоспецифическими библиотеками. В последнем случая обычно вам ничего не грозит, пока библиотека не подключена. Так в глобальной области видимости нельзя использовать имена функций из библиотеки C. Ни в C, ни в C++! -Иначе вы можете столкнуться не только с ODR-violation, но еще и с удивительным поведением компиляторов, умеющих отпимизировать распространненные конструкции. +Иначе вы можете столкнуться не только с ODR-violation, но еще и с удивительным поведением компиляторов, умеющих оптимизировать распространенные конструкции. Так, если определить свой собственный `memset`: @@ -41,7 +41,7 @@ int main(){ При сборке такого примера со статически влинкованной стандартной библиотекой C, программа [упадет](https://godbolt.org/z/sq9bqhn46). Так как вместо адреса стандартной функции `read` будет подставлен адрес глобальной переменной `read`. Аналогичный пример с использованием имени `write` предлагается читателю воплотить самостоятельно в качестве упражнения. -Запретных имен много. Например, все, что начинается с `is*`, `to*` или `_*` запрещено в глобальном простанстве. `_[A-Z]*` запрещены вообще везде. POSIX резервирует имена, заканчивающиеся на `_t`. И еще много всего неожиданного. +Запретных имен много. Например, все, что начинается с `is*`, `to*` или `_*` запрещено в глобальном пространстве. `_[A-Z]*` запрещены вообще везде. POSIX резервирует имена, заканчивающиеся на `_t`. И еще много всего неожиданного. С более полными списками можно ознакомиться по ссылкам. Если вы пользуетесь запрещенными именами, то сегодня может всё работать, но не завтра. diff --git a/runtime/rvo_vs_raii.md b/runtime/rvo_vs_raii.md index 6840655..21016a6 100644 --- a/runtime/rvo_vs_raii.md +++ b/runtime/rvo_vs_raii.md @@ -2,7 +2,7 @@ C++ восхитительный язык. В нем столько идиом, концепций, и каждая со своей замечательной, иногда невыговариваемой, аббревиатурой! А самое замечательное в них то, что они иногда конфликтуют. И от их конфликта страдать придется разработчику. А иногда они вступают в симбиоз и страдать приходится еще больше. -В C++ есть конструкторы, деструкторы и приходящая с нимим концепция RAII: +В C++ есть конструкторы, деструкторы и приходящая с ними концепция RAII: Захватывай и инициализируй ресурс в конструкторе, очищай и отпускай в деструкторе. И будет тебе счастье. Ну что ж, давайте попробуем! @@ -14,7 +14,7 @@ struct Writer { public: static const size_t BufferLimit = 10; - // захватывем устройство, в которое будет писать + // захватываем устройство, в которое будет писать Writer(std::string& dev) : device_(dev) { buffer_.reserve(BufferLimit); } @@ -84,7 +84,7 @@ std::cout << text; Помните, мы обсуждали [не работающее перемещение](../syntax/move.md)? И выясняли, что в C++ нет деструктивного перемещения. А оно все-таки есть. Иногда. Когда срабатывает оптимизация возвращаемого значения и удаление лишних вызовов конструкторов копий/перемещений. -Программы выше все неправильные. Они предполагают, что деструктор `Writer` будет вызван до вовзрата значения из функции. Чего никак быть не может. Деструкторы объектов вызываются всегда после возврата из функции. Иначе эти самые значения бы просто умирали, и вызывающий код всегда получал мертвый объект. +Программы выше все неправильные. Они предполагают, что деструктор `Writer` будет вызван до возврата значения из функции. Чего никак быть не может. Деструкторы объектов вызываются всегда после возврата из функции. Иначе эти самые значения бы просто умирали, и вызывающий код всегда получал мертвый объект. Но как же тогда оно иногда работает и скрывает такую печальную ошибку? А вот как: @@ -117,7 +117,7 @@ const auto text = []{ // text пуст ``` -Никакого неопереденного поведения тут, повторяю, нет. Просто всякий деструктор/конструктор с побочными эффектами как бы «сломан» из-за разрешенных и описанных в стандарте (и даже иногда гарантированных) оптимизаций. +Никакого неопределенного поведения тут, повторяю, нет. Просто всякий деструктор/конструктор с побочными эффектами как бы «сломан» из-за разрешенных и описанных в стандарте (и даже иногда гарантированных) оптимизаций. Ну а в каком-нибудь Rust нам такую ерунду написать [просто не дадут](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c5c9b4edbf891d469214eae29a3ca1af). Такие дела. diff --git a/runtime/static_initialization_order_fiasco.md b/runtime/static_initialization_order_fiasco.md index 46943a7..64a3465 100644 --- a/runtime/static_initialization_order_fiasco.md +++ b/runtime/static_initialization_order_fiasco.md @@ -1,7 +1,7 @@ # Static initialization order fiasco Проблемы с использованием объектов до окончания их полной инициализации наигрывается во многих языках программирования. Сомнительный дизайн с разрывом объявления, конструирования и инициализации можно воплотить в жизнь чуть ли ни где угодно. Но обычно для этого все-таки надо -приложить некоторые усилия. А в C/C++ можно вляпатся незаметно, случайно и очень долго об этого не подозревать. +приложить некоторые усилия. А в C/C++ можно вляпаться незаметно, случайно и очень долго об этого не подозревать. В C/C++ мы можем разделять код программы по разным, независимым единицам трансляции (в разные .c/.cpp файлы). Они могут компилироваться параллельно. @@ -106,7 +106,7 @@ int main() { Чтобы избежать подобного, можно либо в конструкторе `TestStatic` вызвать функцию `static_name` — тогда конструктор строки завершится до завершения конструктора `TestStatic` и -порядок уничтожения объктов будет другим. +порядок уничтожения объектов будет другим. Либо (и так иногда делают) в принципе предотвратить уничтожение статической строки: [создать ее в куче](https://godbolt.org/z/j7aY7q). @@ -195,5 +195,5 @@ g++ -std=c++17 -o test2 logger.cpp main.cpp В своей заботе о минимизации зависимостей и размере обработанных препроцессором исходников (или просто последовав совету линтера), вы не включили `iostream` в интерфейсный заголовок библиотеки, но использовали его в реализации. Пользователь, не знающий об этом, получает проблемы. Не самое удачное решение. -Объекты стандартных потоков не единственная возможность для подобных ошибок. Любая библиотека, использующая глобальные статические объекты, не позаботивщаяся об их инициализации ДО любых действий пользователя — потенциальный источник проблем. +Объекты стандартных потоков не единственная возможность для подобных ошибок. Любая библиотека, использующая глобальные статические объекты, не позаботившаяся об их инициализации ДО любых действий пользователя — потенциальный источник проблем. Если вы автор библиотеки, внимательнее относитесь к проектированию ее интерфейса. В C++ он не ограничивается только лишь сигнатурами функций и описанием классов. diff --git a/runtime/trivial_types_and_ABI.md b/runtime/trivial_types_and_ABI.md index 4ad4d4a..1fdf3c2 100644 --- a/runtime/trivial_types_and_ABI.md +++ b/runtime/trivial_types_and_ABI.md @@ -37,7 +37,7 @@ struct Point { Почему? -Это измение сломало ABI. +Это изменение сломало ABI. В С++ все типы делятся на тривиальные и нетривиальный. Тривиальные, в свою очередь, бывают еще и в разных аспектах тривиальными. В общем случае тривиальность позволяет не генерировать дополнительный код, чтобы что-то сделать. @@ -70,7 +70,7 @@ struct TNCopyable { static_assert(!std::is_trivially_copyable_v); -// Здесь будет возврат через регистр rax. TCopyably в него как раз помещается +// Здесь будет возврат через регистр rax. TCopyable в него как раз помещается extern TCopyable test_tcopy(const TCopyable& c) { return {c.x *5, c.y * 6}; } @@ -134,7 +134,7 @@ extern TNPoint zero_npoint() { } ``` -``` +```asm zero_point(): # @zero_point() xorps xmm0, xmm0 ret diff --git a/runtime/uninitialized.md b/runtime/uninitialized.md index 3fda7fe..106d875 100644 --- a/runtime/uninitialized.md +++ b/runtime/uninitialized.md @@ -4,7 +4,7 @@ Новые современные языки программирования обычно запрещают использование неинициализированных переменных. Переменные либо всегда инициализируются значением по умолчанию (например, в [Go](https://golang.org/ref/spec#The_zero_value)). Либо попытка чтения из неинициализированной переменной дает ошибку компиляции (в [Kotlin](https://pl.kotl.in/PoVXtB7AB) или в [Rust](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=261f92c8ca39b10c1ac565e4f8a1e28a)). -C и C++ — старые языки. В них можно легко и просто объявить переменную, а инициализировать ее как-нибудь потом. Или забыть иницаилизировать вовсе. Но в отличие от совсем низкоуровнего ассемблера, в котором читать из неинициализированной переменной никто не запрещает — ну получите вы свои мусорные байтики и ладно — в C/C++ (а также в Rust, см [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html)) это влечет за собой неопределенное поведение. +C и C++ — старые языки. В них можно легко и просто объявить переменную, а инициализировать ее как-нибудь потом. Или забыть иницаилизировать вовсе. Но в отличие от совсем низкоуровневого ассемблера, в котором читать из неинициализированной переменной никто не запрещает — ну получите вы свои мусорные байтики и ладно — в C/C++ (а также в Rust, см [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html)) это влечет за собой неопределенное поведение. Неожиданный вариант такого UB можно наблюдать на следующем примере (взято [тут](https://stackoverflow.com/questions/54120862/does-the-c-standard-allow-for-an-uninitialized-bool-to-crash-a-program)): @@ -13,7 +13,7 @@ C и C++ — старые языки. В них можно легко и про struct FStruct { bool uninitializedBool; - // Конструктор, не иницаилизирующий поля. + // Конструктор, не инициализирующий поля. // Чтобы проблема воспроизвелась, конструктор должен быть определен в другой единице трансляции // Можно сымитировать с помощью атрибута noinline __attribute__ ((noinline)) @@ -49,7 +49,7 @@ int main() -------------- -Так если осутствие неинициализированных переменных способствует оптимизациям, почему бы их не запретить совсем, с жесткой ошибкой компиляции? +Так если отсутствие неинициализированных переменных способствует оптимизациям, почему бы их не запретить совсем, с жесткой ошибкой компиляции? Во-первых, они позволяют экономить на спичках: @@ -122,4 +122,4 @@ auto x = [&] { ... return value }(); ### И последнее -Если к вам когда-нибудь придет светлая мысль использовать неинициализированную память в качестве источника случайности, гоните её как можно быстрее! Некоторые пробовали — [не получилось](https://kqueue.org/blog/2012/06/25/more-randomness-or-less/). \ No newline at end of file +Если к вам когда-нибудь придет светлая мысль использовать неинициализированную память в качестве источника случайности, гоните её как можно быстрее! Некоторые пробовали — [не получилось](https://kqueue.org/blog/2012/06/25/more-randomness-or-less/). diff --git a/runtime/unreachable_sentinel.md b/runtime/unreachable_sentinel.md index 97765c5..26e81d4 100644 --- a/runtime/unreachable_sentinel.md +++ b/runtime/unreachable_sentinel.md @@ -2,7 +2,7 @@ Поддержка работы с коллекциями как с кастомными, так и со стандартными в C++ от версии к версии все лучше и лучше. -Алгоритмы стандратной библиотеки образца 11 — 17 стандартов работали и работают с парами итераторов, задающих диапазон элементов коллекции. +Алгоритмы стандартной библиотеки образца 11 — 17 стандартов работали и работают с парами итераторов, задающих диапазон элементов коллекции. ```C++ const std::vector v = {1,2,3,4,5}; @@ -128,7 +128,7 @@ struct Numbers { std::cout << *pos; ``` -В С++20 наконец-то все пофиксили. Нет, старые STL-алгоритмы все также не работают. Просто теперь есть новые STL-алгоритмы, почти такие же, как старые, толко в пространстве имен `std::ranges` и жестко требующие удовлетворения новых концептов итераторов. +В С++20 наконец-то все пофиксили. Нет, старые STL-алгоритмы все также не работают. Просто теперь есть новые STL-алгоритмы, почти такие же, как старые, только в пространстве имен `std::ranges` и жестко требующие удовлетворения новых концептов итераторов. Поэтому пример ниже слегка распухает. ```C++ @@ -172,7 +172,7 @@ struct Numbers { }; ``` С ними компилируется и [работает](https://godbolt.org/z/efh3qsxMd) -``` C++ +```C++ auto nums = Numbers(10); auto pos = std::ranges::find_if(nums.begin(), nums.end(), [](int x){ return x % 7 == 0;}); @@ -230,13 +230,13 @@ struct Numbers { std::vector perm = { 1, 2, 3, 4, 5, 6, 7, 8, 9}; std::ranges::shuffle(perm, std::mt19937(std::random_device()())); - // И нам поуступают запросы на поиск позиции элемента, заведомо находящегося в векторе + // И нам поступают запросы на поиск позиции элемента, заведомо находящегося в векторе // size_t p = 7; assert(p < perm.size()) return std::ranges_find(perm.begin(), std::unreachable_sentinel, p) - perm.begin(); ``` -Очевидно, это крайне небезопасный ход. К которому стоит пребегать только в случае, если вы точно все проверили и эта оптимизация критична и необходима. Если в примере выше по какой-то причине будет запрошен элемент, не присутствующий в векторе, мы получим [неопределенное поведение](https://godbolt.org/z/459Y68PcW). +Очевидно, это крайне небезопасный ход. К которому стоит прибегать только в случае, если вы точно все проверили и эта оптимизация критична и необходима. Если в примере выше по какой-то причине будет запрошен элемент, не присутствующий в векторе, мы получим [неопределенное поведение](https://godbolt.org/z/459Y68PcW). Рефакторинг больших участков кода, использующего подобные фичи, может закончиться поиском трудноуловимых багов. В отличие от Rust, в C++ мы не можем гарантированно пометить участок кода, как потенциально опасный и проблемный. В C++ любой участок кода потенциально небезопасен и подчеркнуть это можно только комментарием или какими-нибудь ухищрениями в именовании функций или переменных. diff --git a/syntax/iostreams.md b/syntax/iostreams.md index dde956b..565658f 100644 --- a/syntax/iostreams.md +++ b/syntax/iostreams.md @@ -31,10 +31,10 @@ std::cout << 10; // опять `a` ?!?! поток, с переставленными флагами форматирования. Или вы забудете вернуть их в исходное состояние. -Использование одного и того же имени метода для выстывления и получения флагов +Использование одного и того же имени метода для выставления и получения флагов тоже радует. Особенно любителей возвращать значения через `lvalue`-ссылки в аргументах функций. Но это фишка дизайна чуть ли не всего функционала по настройке потоков. Так что терпим. -Ну и, конечно, стейт форматированя — дополнительная возможность пострелять по ногам в многопоточной среде. +Ну и, конечно, стейт форматирования — дополнительная возможность пострелять по ногам в многопоточной среде. ## Грабли вторые. Глобальная локаль.