Punctuation: replace --/--- with —.

This commit is contained in:
Lapshin Dmitry (LDVSOFT)
2021-01-25 17:32:50 +03:00
parent 3c1d2a51d4
commit 1c3f97d4a9
20 changed files with 118 additions and 118 deletions

View File

@@ -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)

View File

@@ -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++. Лучше сразу возьмите другой язык программирования.

View File

@@ -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)` это хрупкий и тонкий механизм, способный перевернуть все с ног на голову с помощью минимального изменения в коде "лишних" скобок или `&&`.
---
## Полезные ссылки

View File

@@ -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).
---

View File

@@ -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";
};
}

View File

@@ -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 {

View File

@@ -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 сперва для него.

View File

@@ -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++

View File

@@ -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

View File

@@ -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` могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого

View File

@@ -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`):

View File

@@ -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.
```

View File

@@ -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
}
}
```

View File

@@ -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) версии цикл исчезает

View File

@@ -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` не кидает исключений по самой своей природе (сложение чисел, например) или же

View File

@@ -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);
}
}

View File

@@ -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)>;

View File

@@ -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)!)

View File

@@ -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.
В результате которого из объекта могут внезапно пропадать данные. А могут и не пропадать. В зависимости от того, как
реализовали перемещение для конкретного типа.

View File

@@ -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`