﻿# Путеводитель C\+\+ программиста по неопределённому поведению: часть 1 из 11

Вашему вниманию предлагается первая часть электронной книги, которая посвящена неопределённому поведению\. Книга не является учебным пособием и рассчитана на тех, кто уже хорошо знаком с программированием на C\+\+\. Это своего рода путеводитель C\+\+ программиста по неопределённому поведению, причём по самым его тайным и экзотическим местам\. Автор книги — Дмитрий Свиридкин, редактор — Андрей Карпов\.

![1129_book_pt_1_ru/image1.png](https://import.viva64.com/docx/blog/1129_book_pt_1_ru/image1.png)

## Предисловие

_**Паникуй\!**_

### От автора\. Коротко о том, зачем и почему

Всё начинается просто и незатейливо: обычный десятиклассник увлекается программированием, знакомится с алгоритмическими задачками, решения которых должны быть быстрыми\. Узнаёт о языке C\+\+, учит минимальный синтаксис, основные конструкции, контейнеры, решает задачи с предопределённым и всегда корректным форматом ввода и вывода, и горя не знает\.\.\.

В это же время где\-то в большом мире матерые разработчики каждый день ругают то одни языки программирования, то другие, по самым разным причинам: не удобно, нет какой\-то возможности, много лишних букв нужно писать, ошибки в стандартной библиотеке\.\.\. Но есть язык, который ругают за всё и особенно за такую непонятную и таинственную вещь, как неопределённое поведение \(undefined behavior, UB\)\.

Спустя лет пять или шесть наш простой десятиклассник, горя не видавший в море оторванных от реальности программ, внезапно узнаёт, что тем самым горячо нелюбимым языком всегда был, остаётся и будет его C\+\+\.

А потом ещё в течение нескольких лет он наткнётся на самые кошмарные и невероятные ужасы, поджидающие программистов на C\+\+ почти на каждом шагу\. Так и появится эта серия заметок, собирающая наиболее отвратительные примеры, на которые очень легко наткнуться при решении повседневных задач\.

> "Преждевременная оптимизация — корень всех зол" \(Д\. Кнут или Э\. Хоар — в зависимости от того, какой источник смотрите\)\. 

Язык С\+\+, пожалуй, наиболее яркая тому демонстрация: огромное количество ошибок в C\+\+ программах связаны с неопределённым поведением, заложенным в фундаменте языка просто для того, чтобы дать простор оптимизациям на этапе компиляции\.

Если вы собираетесь писать на C\+\+ код, в работоспособности которого хотите быть хоть немного уверенны, стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка и его библиотеке, и всячески их избегать\. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая\.

**Важно**: этот сборник **не является учебным пособием** по языку и рассчитан на тех, кто уже знаком с программированием, с C\+\+ и понимает основные его конструкции\.

### От редактора

Тема неопределённого поведения мне очень близка\. Она пронизывает родной мне проект PVS\-Studio, одним из основателей которого я являюсь\. PVS\-Studio — это инструмент статического анализа кода, перед которым стоит необъятная задача по поиску этого самого неопределённого поведения\. У анализатора есть и другие задачи, например, поиск опечаток или недостижимого кода\. Однако UB — самый большой и неиссякаемый источник проблем в C\+\+ программах, а соответственно и поводов создавать новые диагностики для их выявления\.

Так что, встретив путеводитель Дмитрия Свиридкина по UB на GitHub \([ubbook](https://github.com/Nekrolm/ubbook/)\), я с большим любопытством с ним ознакомился\. Выписал для себя ряд интересных мыслей, которые со временем станут основой новых диагностических правил\. В общем, я получил от чтения и удовольствие, и пользу\.

После я задумался\. Во\-первых, у меня тоже есть кое\-что на тему неопределённого поведения\. Во\-вторых, таким ценным и интересным материалом хочется поделиться с как можно большим количеством программистов, для чего нужно перевести его на английский язык\. Впрочем, думал я недолго и решил попробовать реализовать это на практике\.

Я связался с Дмитрием и предложил сотрудничество по редактированию, дополнению и переводу его материала\. Он согласился, и мы приступили к работе над этой электронной книгой, которую со временем попробуем превратить в печатную\. Приглашаю посмотреть, что у нас получилось\. Запасайтесь печеньками и вниманием для приятного и вдумчивого чтения\. 

## Что такое неопределённое поведение и как оно проявляется

Неопределённое поведение \(undefined behavior, UB\) — это удивительная особенность некоторых языков программирования, позволяющая написать синтаксически корректную программу, работающую совершенно непредсказуемо при переносе её с одной платформы на другую, изменении опций компиляции/интерпретации или замене одного компилятора/интерпретатора другим\. И главное, помимо синтаксической корректности, программа **выглядит** корректной семантически\.

Состоит эта особенность в том, что в спецификации языка программирования сознательно не определяют поведение программы в каких\-то особых условиях\. Делается это из соображений производительности: не надо генерировать дополнительные инструкции с проверками, — или из соображений обеспечения гибкости при реализации каких\-то фич\. В спецификации пишут просто: "Если код делает что\-то нехорошее, то поведение не определено"\. Например:

* если обратиться по нулевому указателю, поведение не определено;
* если дважды захватить блокировку в одном и том же потоке, поведение не определено;
* если поделить на ноль, поведение не определено;
* если прочитать неинициализированную память, поведение не определено;
* и так далее, и тому подобное\.

Важно, что это "поведение не определено" означает, что произойти может что угодно: форматирование диска, ошибка компиляции, исключение, а может всё будет хорошо\. Никаких гарантий не даётся\. Отсюда и происходят весёлые, неожиданные и очень печальные в production\-коде последствия\.

И, конечно же, именно C и C\+\+ наиболее печально известны своим неопределённым поведением\. Однако надо понимать, что эта особенность присуща и другим языкам\. Во многих языках можно найти какой\-нибудь редкий особенный пример с неопределённым поведением\. Но именно в C и C\+\+ оно встречается при написании почти любой программы: слишком много фич языка содержат пункты с неопределённым поведением\.

Итак, по каким же признакам можно заподозрить UB в программе, и насколько неопределённое поведение действительно неопределённое?

Когда\-то давно UB в коде могло привести действительно к чему угодно\. Например, GCC 1\.17 [начинал запускать игрушечки](https://feross.org/gcc-ownage/)\.

Сегодня, если вы поделите что\-то на ноль, подобного наверняка не произойдёт\. Однако неприятности всё же бывают разные:

1. Для конкретной платформы и компилятора в документации сказано, что именно произойдёт несмотря на страшные слова "undefined behavior" в стандарте\. И всё будет хорошо\. Вы знаете, что делаете\. Никакой неопределённости\. Все классно\.
1. UB при работе с памятью чаще всего заканчиваются ошибкой сегментации и получением прекрасного сигнала SIGSEGV от операционной системы\. Программа падает\.
1. Программа работает и штатно завершается\. Но даёт разные \(или неадекватные\) результаты от запуска к запуску\. Также результаты меняются от сборки к сборке при изменении опций компилятора или самого компилятора\. Никаких генераторов случайных чисел вы не использовали\.
1. Программа ведёт себя неправильно несмотря на то, что в коде наставлено огромное множество проверок, _assert_'ов, _try\-catch_ блоков, каждый из которых "подтверждает", что всё корректно\. В отладчике видно, что вычисления идут корректно, но совершенно внезапно всё ломается\.
1. Программа выполняет код, который в ней есть, но не вызывался\. Отрабатываются ни разу не вызываемые функции\.
1. Компилятор "без причины" и без падения отказывается собирать код\. Линковщик выдаёт "невозможные и бессмысленные" ошибки\.
1. Проверки в коде перестают исполняться\. Под отладчиком видно, что исполнение не заходит внутрь веток _if_ или _catch_, хотя по значениям переменных заход должен быть выполнен\.
1. Внезапный необоснованный вызов _std::terminate_\.
1. Бесконечные циклы становятся конечными и наоборот\.

С неопределённым поведением часто путают два других понятия\.

1. Ещё одна страшная аббревиатура UB — неуточнённое \(unspecified\) поведение\. Стандарт не уточняет, что именно может произойти, но описывает варианты\. Так, например, порядок вычисления аргументов функции — поведение неуточнённое\.
1. Поведение, определяемое реализацией \(implementation\-defined\) — надо смотреть документацию для вашей платформы и вашего компилятора\.

Эта парочка намного лучше неопределённого, хотя и имеет с ним одну общую черту: программа, полагающаяся на любое из них, строго говоря, непереносима\.

Также выделяют два класса неопределённого поведения:

* Неопределённое поведение на уровне библиотеки \(Library Undefined Behavior\): вы сделали что\-то, что не предусматривается конкретной библиотекой \(в том числе и стандартной, но не всегда\)\. Например, библиотека [gMock](https://google.github.io/googletest/gmock_for_dummies.html) под страхом неопределённого поведения не допускает донастраивать _mock_\-объект после начала его использования\.
* Неопределённое поведение на уровне языка \(Language Undefined Behavior\): вы сделали что\-то, что фундаментально не определено спецификацией языка программирования\. Например, разыменовали нулевой указатель\.

Если вы столкнулись с первым — у вас проблемы\. Но если всё работает, то с очень большим шансом и продолжит работать до тех пор, пока вы не обновите библиотеку или не смените платформу\. А побочные эффекты часто могут быть лишь локальными\. Очень похоже на implementation\-defined поведение\.

Если вы столкнулись со вторым — у вас большие проблемы\. Код может совершенно внезапно перестать корректно работать при малейших изменениях\. А также могут быть серьёзные угрозы безопасности для пользователей вашего приложения\.

### Полезные ссылки

1. Stack Overflow\. [Undefined, unspecified and implementation\-defined behavior](https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior)\.
1. Predrag Gruevski\. [Ложные представления программистов о неопределённом поведении](https://pvs-studio.ru/ru/blog/posts/cpp/1024/)\.
1. John Regehr\. [A Guide to Undefined Behavior in C and C\+\+](https://blog.regehr.org/archives/213)\.

## Как искать неопределённое поведение?

Очень частый вопрос, который задавали мне, который я задавал сам себе и другим\. Да и каждый C\+\+ разработчик, к сожалению, должен его задавать\.

Ответ на него в общем случае — никак\. Это алгоритмически неразрешимая задача, практически ничем не отличающаяся от задачи останова\. Но программистов как палками ни гоняй, всё равно будут решать неразрешимые задачи, так что для конкретного кода и для конкретных входных данных иногда есть способы дать ответ\.

Можно проверить код до компиляции различными статическими анализаторами:

* Cppcheck,
* Clang Static Analyzer,
* PVS\-Studio,
* и другие\.

Достаточно умный анализатор, работающий с графом потока выполнения программы и знающий сотни ловушек стандарта, способен найти многие проблемы и привлечь внимание к сомнительному коду\. Но не все и не всегда\.

<details>
   <summary>Компиляторы Clang и GCC с включёнными флагами \\\-Wall \\\-Wpedantic способны находить некоторые ошибки\\\.</summary>

Например, [GCC пожалуется](https://godbolt.org/z/zM4r1s) на этот код

```cpp
int arr[5] = {1,2,3,4,5};

int main() {
    int i = 5;
    return arr[i];
}
```

выдав сообщение:

```cpp
array subscript 5 is above array bounds of 'int [5]' [-Warray-bounds]
    6 |     return arr[i];
      |            ~~~~~^
note: while referencing 'arr'
    2 | int arr[5] = {1,2,3,4,5};
```


</details>
Мы можем сами проверять часть кода в compile\-time на различных наборах входных данных, используя _constexpr_\. В контексте, вычисляемом на этапе компиляции, UB [запрещено](https://godbolt.org/z/qGGYeP):

```cpp
constexpr int my_div(int a, int b) {
    return a / b;
}

namespace test {
template <unsigned int N>
constexpr int div_test(const int (&A)[N], const int (&B)[N]) {
    int x = 0;
    for (auto i = 0u; i < N; ++i) {
        x = ::my_div(A[i], B[i]);
    }
    return x;
}

constexpr int A[] = {1,2,3,4,5};
constexpr int B[] = {1,2,3,4,0};
static_assert((div_test(A, A), true)); // OK
static_assert((div_test(A, B), true)); // Compilation error, zero division
```

Но _constexpr_ не везде применим: в зависимости от версии стандарта он налагает ограничения на тело функции, а также неявно применяет _inline_ спецификатор, "запрещая" отрывать определение функции в отдельную единицу трансляции \(или, по\-простому, определение придётся разместить в заголовочном файле\)\.

Наконец, если мы не смогли найти ошибки статическим анализом \(внешними утилитами или компилятором\), можно прибегнуть к динамическому анализу\.

При сборке компиляторами _Clang_ или _GCC_ можно включить санитайзеры _\-fsanitize\=undefined_, _\-fsanitize\=address_, _\-fsanitize\=thread_, позволяющие [отлавливать](https://godbolt.org/z/va44E7) ошибки в run\-time, но ценой значительных накладных расходов\. Так что пользоваться этими средствами нужно только на этапе тестирования и разработки\.

Также для отладочных сборок код стандартных библиотек иногда инструментирован _assert_'aми\. Так, например, сделано для различных итераторов стандартной библиотеки в поставке MSVC \(Visual Studio\)\.

Поскольку неопределённое поведение проявляется в возможностях оптимизации тем или иным компилятором, нужно собирать свой код под разные платформы, с разными уровнями оптимизаций, и сравнивать его поведение\. Код без ошибок должен быть переносимым и вести себя одинаково \(если, конечно, его задача не в генерации совершенно случайных значений\)\.

Тесты, различные сборки, статический и динамический анализ — способы немного поднять уверенность в том, что в вашем коде нет UB\. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять\. И даже этого может быть недостаточно\.

Ещё есть путь отключения каких\-либо оптимизаций флагами компилятора\. Также есть вариант включения флагов для различных нарушений стандарта \(знаменитый _\-fpermissive_\), превращающих язык C\+\+ во что\-то совершенно иное\. Но призываю вас никогда не идти этим путём\. Ваш код станет непереносимым\. Ваш код перестанет быть кодом на C\+\+\. Лучше сразу возьмите другой язык программирования\.

### Полезные ссылки

1. GCC documentation\. [Options to Request or Suppress Warnings](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html)\.
1. Clang documentation\. [AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html)\.
1. Clang documentation\. [UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)\. 
1. Shafik Yaghmour\. [Exploring Undefined Behavior Using Constexpr](https://shafik.github.io/c++/undefined%20behavior/2019/05/11/explporing_undefined_behavior_using_constexpr.html)\.
1. MSVC documentation\. [/permissive\- \(Standards conformance\)](https://learn.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance?view=msvc-160)\.
1. Wikipedia\. [List of tools for static code analysis](https://en.wikipedia.org/wiki/List_of_tools_for_static_code_analysis#Static_code_analysis_tools)\.

## Сужающие преобразования и неявное приведение типов

Неявные преобразования типов запрещены во многих современных языках программирования, особенно новых\.

Так в Rust, Haskell, Kotlin нельзя просто так использовать _float_ и _int_ в одном арифметическом выражении без явного указания преобразовать одно в другое\. Python не так строг, но все же не даёт смешивать строки, символы и числа\.

В С\+\+ запрета неявного преобразования нет, что порождает массу ошибочного кода\. Причём в таком коде может быть как определённое, но неожиданное, так и неопределённое поведение\.

Пример:

```cpp
#include <vector>
#include <numeric>
#include <iostream>

int average(const std::vector<int>& v) {
    if (v.empty()) {
        return 0;
    }
    return std::accumulate(v.begin(), v.end(), 0) / v.size();
}

int main() {
    std::cout << average({-1,-1,-1});
}
```

Любой, кто мельком бросит взгляд на этот код, будет ожидать, что результатом работы окажется _\-1_\. Но, увы, результат будет совершенно [другим](https://godbolt.org/z/6GEs4q)\. Программа, собранная GCC под платформу x86\-64, распечатает:

```cpp
1431655764
```

В этом коде нет неопределённого поведения \(по крайней мере на используемых входных данных\)\. Но есть неявное приведение типов, делающее результат неожиданным\.

1. Тип возвращаемого значения _std::accumulate_ определяется третьим аргументом\. В данном случае это целочисленный знаковый ноль — тип по умолчанию для всех числовых литералов\.
1. Тип возвращаемого значения операции деления определяется наибольшим из участвующих типов аргументов, а также правилами [integer promotion](https://wiki.sei.cmu.edu/confluence/display/c/INT02-C.+Understand+integer+conversion+rules)\. В примере тип левого аргумента — _int_, а правого — _size\_t_ — достаточно широкое беззнаковое целое\. Более широкое, чем _int_\. Потому, по правилам integer promotion, результатом будет _size\_t_\.
1. _\-3_ неявно преобразуется к типу _size\_t_ — такое преобразование вполне определено\. Результатом будет беззнаковое число 2^N \- 3\.
1. Далее будет произведено деление беззнаковых чисел: \(2^N \- 3\) / 3\. Старший бит результата окажется нулевым\.
1. Возвращаемым типом функции _average_ объявлен _int_\. Так что нужно выполнить ещё одно неявное преобразование\.
1. В общем случае преобразование unsigned \-\> signed определяется реализацией \(implementation defined\):
    * если размеры типов _int_ и _size\_t_ одинаковые, то, поскольку старший бит нулевой, положительное число укладывается в допустимый диапазон значений для типа _int_ — стандарт гарантирует, что никаких проблем нет;
    * если размеры не совпадают, то произойдёт сужающее преобразование \(_narrowing conversion_\), которое как раз\-таки отдано на откуп деталям реализации\. Так, вместо ожидаемой обрезки старших, не поместившихся битов, на некоторых платформах может произойти замена на _std::numeric\_limits<int\>::max_;
    * например, при сборке под 64\-битную платформу с помощью GCC сужающее преобразование определено, как и ожидается, через обрезку старших битов\. Поэтому итоговым результатом оказывается \(\(2^64 \- 3\) / 3 % 2^32\)\.

Неявные приведения типов касаются не только встроенных примитивов, но и более сложных типов\. И самое неприятное — они вмешиваются в выбор подходящей перегрузки функции, приводя к различным — часто неприятным — казусам\.

Пример с [_abs_](https://godbolt.org/z/KbTza4):

```cpp
#include <cmath>
#include <iostream>

int main() {
    std::cout << abs(3.5) << "\n"; // функция библиотеки С,
                                   // принимает на вход тип long
                                   // результат — 3
    std::cout << std::abs(3.5);    // функция библиотеки С++
                                   // перегружена для double
                                   // результат — 3.5
}
```

Ещё более неприятный пример наблюдается со стандартным типом _std::string_:

```cpp
#include <string>

int main() {
    std::string s;
    s += 48;    // неявное приведение к char.
    s += 1000;  // а тут еще и с переполнением, очень неприятным
                // на платформе с signed char.
    s += 49.5;  // опять-таки неявное приведение к char
}
```

Этот ужас [компилируется](https://godbolt.org/z/K4WhTe)\!

Казалось бы, этот пример совершенно ужасного использования никогда не может встретиться в нормальном коде\. Увы, но может\.

Вы можете написать обобщённый код своего _std::accumulate_ с различными проверками шаблонных аргументов и случайно, по ошибке, передать в него _string_ в качестве аккумулятора и контейнер, например, _float_\. И никакой ошибки компиляции [не будет](https://godbolt.org/z/8Y7xe8)\. Только странный баг в программе\.

<details>
   <summary>Эта программа компилируется, а результатом является пустая строка:</summary>

```cpp
#include <string>
#include <vector>
#include <iostream>

template <class Range, class Acc>
auto accumulate(Range&& r, Acc acc) 
requires(requires(){
    {acc += *std::begin(r) };
})
{
    for (auto&& x : r){
        acc += x;
    }
    return acc;
}


int main() {
    std::vector<double> v {0.5, 0.7, 0.1};
    auto res = accumulate(v, std::string{});
    std::cout << '"' << res << '"';
}
```

Вывод программы:

```cpp
""
```


</details>
Цепочки неявных преобразований могут быть очень неочевидными:

```cpp
void f(float&& x) { std::cout << "float " << x << "\n";  }
void f(int&& x) { std::cout << "int " << x << "\n";  }
void g(auto&& v) { f(v); } // C++20

int main() { 
    g(2);
    g(1.f);
}
```

Самым удивительным образом этот пример [выводит](https://godbolt.org/z/s1933K):

```cpp
float 2
int 1
```

Хотя мы подставляли типы констант совсем наоборот и почти наверняка ожидали:

```cpp
int 2
float 1
```

Это не баг компилятора и не неопределённое поведение\! Всему виной хитрая цепочка неявных преобразований\.

Рассмотрим её на примере первого вызова _g\(2\)_, подставив параметр шаблона:

```cpp
void g(int&& v) {
    // Несмотря на то что тип v — int&&
    // Дальнейшее использование v в выражениях дает int& !
    // decltype(v)   == int&&
    // decltype((v)) == int&

    // Функции f принимают только rvalue ссылки

    // Неявное преобразование int& к int&& запрещено
    //  int&& x = 5;
    //  int&& y = x; // не компилируется!

    // Таким образом перегрузка f(int&&) не может быть использована

    // Остается f(float&&)
    // int умеет неявно приводиться к float
    // int& умеет неявно выступать в роли просто int
    // неявный static_cast<float>(v) возвращает временное значение float
    // временные значения типа T неявно биндятся к T&&

    // Имеем цепочку преобразований:
    // int& -> int -> float -> float&& 

    f(v); // будет вызван f(float&&) !

    // явно: f(static_cast<float>(v));
}
```

Конечно, никто никогда \(по крайней мере явно\) не принимает примитивы по _rvalue_\-ссылкам\. Потому что это бессмысленно\. Но даже без _rvalue_\-ссылки для примитивов мы можем сотворить нечто ужасное:

```cpp
struct MyMovableStruct {
    operator bool () {
        return !data.empty();
    }
    std::string data;
};

void consume(MyMovableStruct&& x) { 
    std::cout << "MyStruct: " << x.data << "\n";  
}
void consume(bool x) { std::cout << "bool " << x << "\n";  }
void g(auto&& v) { consume(v); }
int main() { 
    g(MyMovableStruct{"hello"});
}
```

Той же самой цепочкой преобразований [получим](https://godbolt.org/z/bncnPj) в выводе "bool 1"\. Разве что последний шаг не нужен\.

Обязательно включайте предупреждения компилятора обо всех неявных преобразованиях\. Очень желательно трактовать их как ошибки\.

Не привносите неявные преобразования для своих типов — всегда помечайте однопараметрические конструкторы как _explicit_\.

Если перегружаете операторы приведения \(_operator T\(\)_\) для своих типов, также делайте их _explicit_\.

Если ваши функции/методы рассчитаны на работу только с определённым примитивным типом, навешивайте на них ограничения с помощью шаблонов, SFINAE, концептов или, что [очень просто](https://godbolt.org/z/Yx1e3d), механизма явного удаления перегрузок \(_\= delete_\):

```cpp
int only_ints(int x) { return x;}

template <class T>
auto only_ints(T x) = delete;

int main() {
    const int& x = 2;
    only_ints(2);
    only_ints(x);
    char c = '1';
    only_ints(c);   // Compilation Error.
    only_ints(2.5); // Explicitly deleted.
}
```

**Автор — Дмитрий Свиридкин**

Более восьми лет работает в сфере коммерческой разработки высокопроизводительного программного обеспечения на C и C\+\+\. С 2019 по 2021 год преподавал курсы системного программирования под Linux в СПбГУ и практики C\+\+ в ВШЭ\.  В настоящее время — Software Engineer в AWS \(Cloudfront\), занимается системной и embedded\-разработкой на Rust и C\+\+ для edge\-серверов\. Основная сфера интересов — безопасность программного обеспечения\. 

**Редактор — Андрей Карпов**

Более 15 лет занимается темой статического анализа кода и качества программного обеспечения\. Автор большого количества статей, посвящённых написанию качественного кода на языке C\+\+\. С 2011 по 2021 год удостаивался награды Microsoft MVP в номинации Developer Technologies\. Один из основателей проекта PVS\-Studio\. Долгое время являлся CTO компании и занимался разработкой С\+\+ ядра анализатора\. Основная деятельность на данный момент — управление командами, обучение сотрудников и DevRel активности\.

## Все части

1. [Часть 1](https://pvs-studio.ru/ru/blog/posts/cpp/1129/): предисловие, что такое неопределённое поведение и как оно проявляется, сужающие преобразования и неявное приведение типов\.
1. [Часть 2](https://pvs-studio.ru/ru/blog/posts/cpp/1136/): переполнение целых знаковых чисел, числа с плавающей точкой, integer promotion, _char_ и знаковое расширение\.
1. [Часть 3](https://pvs-studio.ru/ru/blog/posts/cpp/1149/): висячие ссылки, _string\_view_, синтаксический сахар с ложкой дёгтя \(range\-based for\), self\-reference, _std::vector_ и инвалидация ссылок\.
1. [Часть 4](https://pvs-studio.ru/ru/blog/posts/cpp/1156/): списки захвата лямбда\-функций, кортежи, внезапная мутабельность, неявные ссылки, use\-after\-move, lifetime extension\.
1. [Часть 5](https://pvs-studio.ru/ru/blog/posts/cpp/1160/): Most Vexing Parse, неконстантные константы, семантика перемещения, _std::enable\_if\_t_ против _std::void\_t_, забытый _return_\.
1. [Часть 6](https://pvs-studio.ru/ru/blog/posts/cpp/1163/): эллипсис и функции, _operator \[\]_, _iostreams_ \(счастливой отладки\!\), оператор запятая, function\-try\-block, типы "нулевого" размера\.
1. [Часть 7](https://pvs-studio.ru/ru/blog/posts/cpp/1174/): NULL\-терминированные строки, _std::shared\_ptr_, \(не\)явное приведение типов, как передать стандартную функцию и ничего не сломать\.
1. [Часть 8](https://pvs-studio.ru/ru/blog/posts/cpp/1178/): бесконечные циклы и проблема остановки, рекурсия, ложный _noexcept_, переполнение буфера\.
1. [Часть 9](https://pvs-studio.ru/ru/blog/posts/cpp/1182/): \(N\)RVO vs RAII, разыменование нулевых указателей, static initialization order fiasco, static inline, нарушение ODR, зарезервированные имена\.
1. [Часть 10](https://pvs-studio.ru/ru/blog/posts/cpp/1193/): тривиальные типы и ABI, неинициализированные переменные, С\+\+20 unbounded ranges, невиртуальные виртуальные функции, VLA\.
1. [Часть 11](https://pvs-studio.ru/ru/blog/posts/cpp/1199/): невалидные указатели, placement new для массивов, data race, повторный захват mutex, сигнало\(не\)безопасность, как сделать всё правильно и уйти в deadlock\.
1. [Часть 12](https://pvs-studio.ru/ru/blog/posts/cpp/1211/): _std::vector::reserve_ и _std::vector::resize_, невыровненные ссылки, время жизни и смерти, статический анализ и UB, заключение\.