mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-17 04:44:35 +03:00
fix typos and lang syntax highlight
This commit is contained in:
@@ -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) сравниваемых чисел.
|
||||
|
||||
@@ -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<int> v = {1};
|
||||
auto idx = -1;
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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<I> mod(I a, std::type_identity_t<I> b) {
|
||||
|
||||
Однако, если ваша программа только и делает, что ожидает и выполняет IO операции, то траты в два раза большего числа тактов на сложение или умножение никто и не заметит. Да и язык C++ для таких программ чаще всего не лучший выбор.
|
||||
|
||||
Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел).
|
||||
Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнении никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел).
|
||||
|
||||
Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные програмы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да?
|
||||
Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные программы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да?
|
||||
|
||||
Для выведения ограничений вам помогут отладочные `assert` с правильными проверками переполнения, которые нужно написать. Или включение `ubsan` (_undefined behavior sanitizer_) при сборке компиляторами `clang` или `gcc`.
|
||||
А также тестовые `constexpr` вычисления.
|
||||
|
||||
@@ -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` — синтаксический сахар
|
||||
|
||||
@@ -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`. И еще много всего неожиданного.
|
||||
С более полными списками можно ознакомиться по ссылкам.
|
||||
|
||||
Если вы пользуетесь запрещенными именами, то сегодня может всё работать, но не завтра.
|
||||
|
||||
@@ -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). Такие дела.
|
||||
|
||||
|
||||
@@ -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++ он не ограничивается только лишь сигнатурами функций и описанием классов.
|
||||
|
||||
@@ -37,7 +37,7 @@ struct Point {
|
||||
|
||||
Почему?
|
||||
|
||||
Это измение сломало ABI.
|
||||
Это изменение сломало ABI.
|
||||
|
||||
В С++ все типы делятся на тривиальные и нетривиальный. Тривиальные, в свою очередь, бывают еще и в разных аспектах тривиальными. В общем случае тривиальность позволяет не генерировать дополнительный код, чтобы что-то сделать.
|
||||
|
||||
@@ -70,7 +70,7 @@ struct TNCopyable {
|
||||
|
||||
static_assert(!std::is_trivially_copyable_v<TNCopyable>);
|
||||
|
||||
// Здесь будет возврат через регистр 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
--------------
|
||||
|
||||
Так если осутствие неинициализированных переменных способствует оптимизациям, почему бы их не запретить совсем, с жесткой ошибкой компиляции?
|
||||
Так если отсутствие неинициализированных переменных способствует оптимизациям, почему бы их не запретить совсем, с жесткой ошибкой компиляции?
|
||||
|
||||
Во-первых, они позволяют экономить на спичках:
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Поддержка работы с коллекциями как с кастомными, так и со стандартными в C++ от версии к версии все лучше и лучше.
|
||||
|
||||
Алгоритмы стандратной библиотеки образца 11 — 17 стандартов работали и работают с парами итераторов, задающих диапазон элементов коллекции.
|
||||
Алгоритмы стандартной библиотеки образца 11 — 17 стандартов работали и работают с парами итераторов, задающих диапазон элементов коллекции.
|
||||
|
||||
```C++
|
||||
const std::vector<int> 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<size_t> 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++ любой участок кода потенциально небезопасен и подчеркнуть это можно только комментарием или какими-нибудь ухищрениями в именовании функций или переменных.
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ std::cout << 10; // опять `a` ?!?!
|
||||
поток, с переставленными флагами форматирования. Или вы забудете вернуть их в исходное
|
||||
состояние.
|
||||
|
||||
Использование одного и того же имени метода для выстывления и получения флагов
|
||||
Использование одного и того же имени метода для выставления и получения флагов
|
||||
тоже радует. Особенно любителей возвращать значения через `lvalue`-ссылки в аргументах функций. Но это фишка дизайна чуть ли не всего функционала по настройке потоков. Так что терпим.
|
||||
|
||||
Ну и, конечно, стейт форматированя — дополнительная возможность пострелять по ногам в многопоточной среде.
|
||||
Ну и, конечно, стейт форматирования — дополнительная возможность пострелять по ногам в многопоточной среде.
|
||||
|
||||
## Грабли вторые. Глобальная локаль.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user