mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-17 21:04:35 +03:00
Punctuation: replace --/--- with —.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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++. Лучше сразу возьмите другой язык программирования.
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ auto find(const std::vector<T>& v, const T& x) {
|
||||
---
|
||||
## Проблема явного указания типа
|
||||
|
||||
Длинно и много писать -- решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах.
|
||||
Длинно и много писать — решается с помощью `using`-псевдонимов. Так что это не проблема. Другое дело, что изменение типа в одном месте потребует синхронизированных изменений в других местах.
|
||||
|
||||
И все могло быть хорошо: поменяли где-то в объявлении, получили ошибки компиляции -- исправили во всех местах, на которые указали ошибки. Но в C++ есть неявное приведение типов, которое особенно жестоко наказывает при использовании ссылок.
|
||||
И все могло быть хорошо: поменяли где-то в объявлении, получили ошибки компиляции — исправили во всех местах, на которые указали ошибки. Но в C++ есть неявное приведение типов, которое особенно жестоко наказывает при использовании ссылок.
|
||||
|
||||
```C++
|
||||
std::map<std::string, int> 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<const std::string, int>& 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(x), int&&>);
|
||||
decltype(auto) y = 5;
|
||||
static_assert(std::is_same_v<decltype(y), int>);
|
||||
```
|
||||
Разница в том, что `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)` это хрупкий и тонкий механизм, способный перевернуть все с ног на голову с помощью минимального изменения в коде — "лишних" скобок или `&&`.
|
||||
|
||||
---
|
||||
## Полезные ссылки
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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<void()> GetNotifier() {
|
||||
return [this]{
|
||||
// this -- может стать висячей ссылкой!
|
||||
// this — может стать висячей ссылкой!
|
||||
std::cout << "notify " << id << "\n";
|
||||
};
|
||||
}
|
||||
@@ -47,7 +47,7 @@ struct Task {
|
||||
|
||||
std::function<void()> GetNotifier() {
|
||||
return [=]{
|
||||
// this -- может стать висячей ссылкой!
|
||||
// this — может стать висячей ссылкой!
|
||||
std::cout << "notify " << id << "\n";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 сперва для него.
|
||||
|
||||
|
||||
@@ -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<decltype(a)> &&
|
||||
std::is_lvalue_reference_v<decltype(b)>) {
|
||||
// оба аргумента были 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++
|
||||
|
||||
@@ -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<Action> actions) {
|
||||
```
|
||||
Красиво, коротко, с неопределенным поведением и неправильно.
|
||||
|
||||
- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end ивалидируются -- цикл продолжится по уничтоженным данным.
|
||||
- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end ивалидируются — цикл продолжится по уничтоженным данным.
|
||||
- Если реаллокации не произойдет, цикл пройдет только по тому набору элементов, что были в векторе изначально. До добавленных в процессе дело не дойдет.
|
||||
|
||||
Корректый код:
|
||||
@@ -53,7 +53,7 @@ void run_actions(std::vector<Action> actions) {
|
||||
}
|
||||
}
|
||||
```
|
||||
И у нас опять неопределенное поведение -- `push_back` может вызвать реаллокацию вектора
|
||||
И у нас опять неопределенное поведение — `push_back` может вызвать реаллокацию вектора
|
||||
и тогда ссылка `act` станет висячей.
|
||||
|
||||
Корректный код:
|
||||
@@ -68,14 +68,14 @@ void run_actions(std::vector<Action> 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
|
||||
|
||||
@@ -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` могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого
|
||||
|
||||
@@ -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<int>::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`):
|
||||
|
||||
|
||||
@@ -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<I> div(I a, std::type_identity_t<I> b) {
|
||||
|
||||
if (a == std::numeric_limits<I>::min && b == -1) {
|
||||
// диапазон [min, max] несимметричный относительно 0.
|
||||
// abs(min) > max -- будет переполнение
|
||||
// abs(min) > max — будет переполнение
|
||||
return ArithmeticError::Overflow;
|
||||
}
|
||||
return a / b;
|
||||
@@ -157,7 +157,7 @@ ErrorOrInteger<I> mod(I a, std::type_identity_t<I> b) {
|
||||
|
||||
Однако, если ваша программа только и делает, что ожидает и выполняет IO операции, то траты в два раза большего числа тактов на сложение или умножение никто и не заметит. Да и язык C++ для таких программ чаще всего не лучший выбор.
|
||||
|
||||
Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет -- все определено как вычисления по модулю `2^N` (N -- количество бит для выбранного типа чисел).
|
||||
Итак, если вы работаете только лишь с беззнаковыми числами (`unsigned`), то с неопределенным поведением при переполнеии никаких проблем нет — все определено как вычисления по модулю `2^N` (N — количество бит для выбранного типа чисел).
|
||||
|
||||
Если же вы работаете со знаковыми числами, либо используйте безопасные обертки, сообщающие каким-либо образом об ошибках. Либо выводите ограничения на входные данные програмы целиком таким образом, чтобы переполнения не возникало, и не забывайте эти ограничения проверять. Все просто, да?
|
||||
|
||||
@@ -165,7 +165,7 @@ ErrorOrInteger<I> mod(I a, std::type_identity_t<I> 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.
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Бесконечные циклы и проблема останова
|
||||
|
||||
Определить, завершается или не завершается программа на конкретном наборе данных -- алгоритимически невозможно в общем случае.
|
||||
Определить, завершается или не завершается программа на конкретном наборе данных — алгоритимически невозможно в общем случае.
|
||||
|
||||
Но в стандартах C и C++ зачем-то сказано, что валидная программа должна либо гарантированно завершаться, либо гарантированно производить обозреваемые эффекты: запрашивать ввод-вывод, взаимодействовать с `volatile` переменными и подобное. А иначе поведение программы неопределенное. Так что "правильные" компиляторы C++ настолько суровы, что способны решать алгоритмически неразрешимые задачи.
|
||||
|
||||
Если в программе есть бесконечный цикл, и компилятор решил, что этот цикл не имеет обозреваемых эффектов, то цикл не имеет смысла и может быть выброшен.
|
||||
|
||||
Занятный пример -- таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/nE7oWf)
|
||||
Занятный пример — таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/nE7oWf)
|
||||
```C++
|
||||
#include <iostream>
|
||||
|
||||
@@ -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) версии цикл исчезает
|
||||
|
||||
@@ -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` не кидает исключений по самой своей природе (сложение чисел, например) или же
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int&>( // отбрасываем 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<int>& 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<int>& v,
|
||||
```
|
||||
|
||||
Оставаясь в рамках рекомендуемых практик написания C++ программ,
|
||||
мы не можем соорудить пример, который бы демонстрировал неприменимость оптимизации -- нам мешает инкапсуляция.
|
||||
мы не можем соорудить пример, который бы демонстрировал неприменимость оптимизации — нам мешает инкапсуляция.
|
||||
До приватных полей вектора мы не можем законно добраться.
|
||||
|
||||
Но ненормальный код не запрещен! Применим грубую силу:
|
||||
@@ -191,7 +191,7 @@ int main() {
|
||||
|
||||
## Сonst, время жизни и происхождение указателей.
|
||||
|
||||
Неизменяемые объекты всем хороши, кроме одного -- это константные объекты в C++.
|
||||
Неизменяемые объекты всем хороши, кроме одного — это константные объекты в C++.
|
||||
Если они где-то засели, то их оттуда по-нормальному не выгонишь.
|
||||
|
||||
Что имеется в виду:
|
||||
@@ -206,7 +206,7 @@ struct Unit {
|
||||
```
|
||||
|
||||
Из-за константного поля объекты `Unit` теряют операцию присваивания.
|
||||
Их нелья менять местами -- `std::swap` не работает больше.
|
||||
Их нелья менять местами — `std::swap` не работает больше.
|
||||
`std::vector<Unit>` нельзя больше просто так отсортировать... В общем, сплошное удобство.
|
||||
|
||||
Но самое интересное начинается, если сделать что-то такое
|
||||
@@ -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<sizeof(Unit), alignof(Unit)>;
|
||||
|
||||
@@ -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)!)
|
||||
|
||||
@@ -27,7 +27,7 @@ void run(...){
|
||||
но окажется пустым. Раз два объекта, то и два вызова деструктора. С деструктивной
|
||||
семантикой перемещения (например, в Rust) вызов деструктора будет только один.
|
||||
|
||||
Можно исправить ситуацию -- передать по rvalue-ссылке:
|
||||
Можно исправить ситуацию — передать по rvalue-ссылке:
|
||||
|
||||
```C++
|
||||
void run_task(std::unique_ptr<Task>&& 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 <class T1, class T2>
|
||||
@@ -99,7 +99,7 @@ private:
|
||||
Person p("John", "Smith"); // T1, T2 = const char*
|
||||
```
|
||||
|
||||
Другой интересный случай использования после перемещения -- self-move-assignment.
|
||||
Другой интересный случай использования после перемещения — self-move-assignment.
|
||||
В результате которого из объекта могут внезапно пропадать данные. А могут и не пропадать. В зависимости от того, как
|
||||
реализовали перемещение для конкретного типа.
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ S3: 3
|
||||
Потому что у строки есть конструктор, принимающий число `n` и символ `c`, который
|
||||
нужно повторить `n` раз. А еще есть конструктор, принимающий список инициализации (`std::initializer_list<T>`), состоящий из символов. И существование этого конструктора взаимодействует с неявным приведением типов!
|
||||
|
||||
- `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`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user