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

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

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

## Исполнение программы: бесконечные циклы и проблема остановки

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

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

Если в программе есть бесконечный цикл, и компилятор решил, что этот цикл не имеет обозреваемых эффектов, то он не имеет смысла и может быть выброшен\.

Занятный пример — таким образом можно ["опровергнуть" великую теорему Ферма](https://godbolt.org/z/Te1erGq4G):

```cpp
#include <iostream>

int fermat () {
  const int MAX = 1000;
  int a=1,b=1,c=1;
  while (1) {
    if ( (a*a*a) == (b*b*b) + (c*c*c) ) return 1;
    a++;
    if (a>MAX) {
      a=1;
      b++;
    }
    if (b>MAX) {
      b=1;
      c++;
    }
    if (c>MAX) {
      c=1;
    }
  }
  return 0;
}

int main () {
  if (fermat()) {
    std::cout <<
      "Fermat's Last Theorem has been disproved.\n";
  } else {
     std::cout <<
       "Fermat's Last Theorem has not been disproved.\n";
  }
  return 0;
}
```

Собрав с помощью GCC 14\.1 и ключом \-O3, уверенно получим: _Fermat's Last Theorem has been disproved_\.

Компилятор увидел, что единственный выход из цикла — _return 1_\. У цикла нет никаких видимых эффектов, так что компилятор просто заменил его на _return 1_\.

Если же попытаться узнать, что за тройку "нашла" программа, цикл вернётся\.

В _constexpr_\-контексте получим [ошибку компиляции](https://godbolt.org/z/98MYzd)\. Компилятор остановится при превышении определённой глубины анализа: _'constexpr' loop iteration count exceeds limit of 262144 \(use '\-fconstexpr\-loop\-limit\=' to increase the limit\)\._

Может показаться, будто проблема в том, что условие продолжения цикла не зависит от его тела\. Но и в [исправленной](https://godbolt.org/z/4qqfcq3EE) версии цикл исчезает:

```cpp
int fermat() {
  const int MAX = 1000;
  int a=1,b=1,c=1;
  while ((a*a*a) != ((b*b*b)+(c*c*c))) {
    a++;
    if (a>MAX) {
      a=1;
      b++;
    }
    if (b>MAX) {
      b=1;
      c++;
    }
    if (c>MAX) {
      c=1;
    }
  }
  return 1;
}
```

Даже если в цикле будут операции I/O, он всё равно [может исчезнуть](https://godbolt.org/z/8zh94noer), если компилятор увидит, что эти операции от цикла не зависят\.

```cpp
int fermat () {
  const int MAX = 1000;
  int a=1,b=1,c=1;
  while (1) {
    if ( (a*a*a) == (b*b*b) + (c*c*c) ) {
      std::cout << "Found!\n";
      return 1;
    }
    a++;
    if (a>MAX) {
      a=1;
      b++;
    }
    if (b>MAX) {
      b=1;
      c++;
    }
    if (c>MAX) {
      c=1;
    }
  }
  return 0;
}
```

Собираем с помощью GCC 14\.1 \-O3 \-std\=c\+\+20 и получаем:

```cpp
Found!
Fermat's Last Theorem has been disproved.
```

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

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

1. Wikipedia\. [Проблема остановки](https://ru.wikipedia.org/wiki/Проблема_остановки)\.
1. Wikipedia\. [Теорема Райса](https://ru.wikipedia.org/wiki/Теорема_Райса)\.
1. С проблемой остановки связаны алгоритмические ограничения статических анализаторов кода, приводящие к некоторым [ложноположительным](https://pvs-studio.ru/ru/blog/terms/6461/) и [ложноотрицательным](https://pvs-studio.ru/ru/blog/terms/6460/) срабатываниям\.

## Исполнение программы: рекурсия

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

Однако рекурсия требует места для хранения промежуточного состояния — на куче или в стеке\. Конечно, есть хвостовая рекурсия, которая естественным образом может быть оптимизирована в цикл\. Но это не гарантировано стандартом\. Да и не всегда рекурсия именно хвостовая\.

Stack Overflow не совсем неопределённое поведение, но точно не то, что хочется видеть на боевом стенде\. Потому в серьёзных приложениях предпочитают итеративные алгоритмы рекурсивным\. Если, конечно, нет гарантии, что глубина рекурсии мала\.

В деле искоренения рекурсии из своей программы нужно быть очень внимательным\. И не только в корректной имплементации алгоритмов\. Помимо алгоритмов, рекурсивными могут быть и структуры данных\. И тут в игру вступают RAII, правила нуля, порядок вызовов деструкторов и _noexcept_\.

```cpp
struct Node {
  int value = 0;
  std::vector<Node> children;
};
```

Такая структура совершенно законна для определения дерева, она [компилируется и работает](https://godbolt.org/z/99f97W4Tz)\. И может быть удобнее, чем вариант с умными указателями\.

Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно\. Пользуемся "правилом нуля" и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего\. Красота\!

Однако деструктор, сгенерированный компилятором, будет рекурсивным\! И при слишком большой глубине дерева мы получим переполнение стека\.

Хорошо, пишем свой деструктор: нам нужна очередь, чтобы обойти вершины дерева\.\.\. А очередь — это аллокация памяти\. А аллокация памяти — операция, бросающая исключения\. И вот у нас деструктор будет бросать исключения\. Что совсем нехорошо\.

Можно написать деструктор без аллокаций и рекурсии, но его алгоритмическая сложность будет квадратичной:

1. Находим вершину, у которой последний элемент в векторе потомков является листом;
1. Удаляем этот элемент из вектора;
1. Повторяем, пока дерево не закончится\.

Для обычного связанного списка проблема также сохраняется:

```cpp
struct List {
  int value = 0;
  std::unique_ptr<List> next;
};
```

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

Так что пишем деструктор, а вместе с ним все остальные специальные методы \(в указанном случае — только перемещающие операции\):

```cpp
struct List {
  int value = 0;
  std::unique_ptr<List> next;

  ~List() {
    while (next) {
      // Деструктор всё также рекурсивен,
      // но теперь глубина рекурсии — 1 вызов.
      next = std::move(next->next);
    }
  }

  List() noexcept = default;
  List(List&&) noexcept = default;
  List& operator=(List&&) noexcept = default;
};
```

С рекурсивными структурами данных в C\+\+ нужно быть очень аккуратными\. Не просто так в Rust написать их "очевидным" способом тяжело\.

## Исполнение программы: ложный noexcept

Начиная с 11 стандарта, мы можем помечать функции и методы спецификатором _noexcept_, говоря тем самым компилятору, что эта функция или метод не бросают исключения\.

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

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

Если мы пометим функцию как _noexcept_, а она возьмёт да и кинет исключение, то произойдёт что\-то странное, заканчивающееся внезапным _std::terminate_\.

Так, например, неожиданно перестанут работать _try\-catch_ блоки:

```cpp
void may_throw(){
  throw std::runtime_error("wrong noexcept");
}

struct WrongNoexcept {
  WrongNoexcept() noexcept {
     may_throw();
  }
};

// Попытки обернуть в try-catch эту функцию или любой код,
// использующий её — бесполезны.
void throw_smth() {
  if (rand() % 2 == 0) {
    throw std::runtime_error("throw");
  } else {
    WrongNoexcept w;
  }
}
```

Собрав этот код с помощью GCC или Clang, [получим](https://godbolt.org/z/836bd3qYn) уверенный [access violation](https://pvs-studio.ru/ru/blog/terms/0063/):

```cpp
terminate called after throwing an instance of 'std::runtime_error'
  what():  wrong noexcept
Program terminated with signal: SIGSEGV
```

Может быть очень сложно понять, почему это произошло, если код разнесён по разным единицам трансляции\.

#### Условный noexcept

В С\+\+ любят экономить на ключевых словах:

* _\= 0_ для объявления чисто виртуальных методов;
* новый _requires_ имеет два значения, порождая странные конструкции _requires\(requires\(\.\.\.\)\)_;
* _auto_ и для автовывода, и для переключения на trailing return type;
* _decltype_, у которого разный смысл при применении к переменной и к выражению;
* и, конечно, _noexcept_ — точно так же два значения как у _requires_\.

Есть спецификатор _noexcept\(condition\)_\. И просто _noexcept_ — синтаксический сахар для конструкции _noexcept\(true\)_\.

А есть предикат _noexcept\(expr\)_, проверяющий, что выражение _expr_ не кидает исключений по самой своей природе \(сложение чисел, например\) или же помечено как _noexcept_\.

И вместе они порождают конструкцию для условного навешивания _noexcept_:

```cpp
void fun() noexcept(noexcept(used_expr))

void may_throw(){
  throw std::runtime_error("wrong noexcept");
}

struct ConditionalNoexcept {
  ConditionalNoexcept() noexcept(noexcept(may_throw())) {
     may_throw();
  }
};

// Теперь с этой функцией всё хорошо.
void throw_smth() {
  if (rand() % 2 == 0) {
    throw std::runtime_error("throw");
  } else {
    ConditionalNoexcept w;
  }
}
```

Чтобы избежать проблем, нужно всегда и везде использовать условный _noexcept_ с аккуратной проверкой каждой используемой функции\. Либо вовсе не использовать _noexcept_\. Но во втором случае стоит помнить, что операции перемещения, а также _swap_, должны помечаться как _noexcept_ \(и быть действительно _noexcept_\!\) для эффективной работы со стандартными контейнерами\.

Не забывайте писать негативные тесты\. Без них можно проморгать появление ложного _noexcept_ и получить _std::terminate_ на боевом стенде\.

Также обратите внимание на тонкий и неприятный нюанс: если вам ну очень сильно надо кидать исключения из деструктора, обязательно явно пишите в его объявлении _noexcept\(false\)_\. По умолчанию все ваши функции и методы помечены неявно _noexcept\(false\)_, но для деструкторов в C\+\+ сделано исключение\. Они неявно помечены _noexcept\(true\)_\. [Так что](https://godbolt.org/z/5jo95d):

```cpp
struct SoBad {
  // invoke std::terminate
  ~SoBad() {
     throw std::runtime_error("so bad dtor");
  }
};

struct  NotSoBad {
  // OK
  ~NotSoBad() noexcept(false) {
    throw std::runtime_error("not so bad dtor");
  }
};
```

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

1. Cppreference\. [noexcept operator](https://en.cppreference.com/w/cpp/language/noexcept)\.
1. Cppreference\. [noexcept specifier](https://en.cppreference.com/w/cpp/language/noexcept_spec)\. 
1. Rainer Grimm\. [C\+\+ Core Guidelines: The noexcept Specifier and Operator](https://www.modernescpp.com/index.php/c-core-guidelines-the-noexcept-specifier-and-operator/)\.

## Исполнение программы: переполнение буфера

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

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

* _scanf\("%s", buf\)_ — нет проверки размера буфера;
* _strcpy\(dst, src\)_ — нет проверки размера буфера;
* _strcat\(dst, src\)_ — нет проверки размера буфера;
* _gets\(str\)_ — нет проверки размера буфера;
* _memcpy\(dst, src, n\)_ — проверку размера _dst_ нужно делать вручную;
* _strncat\(dst, src, count\) _– нужны не только ручные проверки, но и помнить, что последний аргумент это не размер буфера\. Он означает, сколько в буфер **ещё** можно записать символов\. [Распространённая путаница](https://pvs-studio.ru/ru/docs/warnings/v645/)\.

И ещё многие другие, преимущественно работающие со строками, функции\.

Эти функции доставляли и продолжают доставлять проблемы\. Некоторые компиляторы \(MSVC\) по умолчанию откажутся собирать ваш код, если увидят одну из них\. Другие будут менее заботливыми и, возможно, выдадут предупреждение\. По крайней мере, про функцию _gets_ уж точно\. Если с другими функциями у программиста есть возможность уберечься \(проверка до вызова; у _scanf_ можно указать размер для ограничения строки\), то с _gets_ — без вариантов\.

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

Проверки размеров — дополнительная работа\. Генерировать под них инструкции — замедлять программу\. Тем более программист мог всё проверить сам\. Так что в C и С\+\+ обращение за границы массива хоть на запись, хоть на чтение влечёт неопределённое поведение\. И дыры в безопасности могут зарастать различными спецэффектами\.

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

Но иногда начинается веселье:

```cpp
const int N = 10;
int elements[N];

bool contains(int x) {
  for (int i = 0; i <= N; ++i) {
    if (x == elements[i]) {
      return true;
    }
  }
  return false;
}

int main() {
  for (int i = 0; i < N; ++i) {
    std::cin >> elements[i];
  }
  return contains(5);
}
```

Эта программа, собранная GCC c оптимизациями, всегда "[найдёт](https://godbolt.org/z/fncPWvn17)" пятёрку в массиве\. Независимо от того, какие числа будут введены\. Причём никаких предупреждений ни Clang, ни GCC не выдают\. Ну хотя бы PVS\-Studio [ругается](https://godbolt.org/z/13xos9oqo): 

V557 Array overrun is possible\. The value of 'i' index could reach 10\.

Происходит такой спецэффект из следующих соображений:

1\. Компиляторы вольны считать, что UB в программах не бывает\.

2\. В этом цикле будет обращение за границы массива, а значит UB\.

```cpp
for (int i = 0; i <= N; ++i) {
  if (x == elements[i]) {
    return true;
  }
}
```

3\. Но, поскольку UB не бывает, до _N\+1_ итерации дело дойти не должно\!

4\. Значит, мы выйдем из цикла по _return true_\.

5\. А значит вся функция _contains_ — это один _return true_\. Оптимизировано\!

Или вот, конечный цикл [становится бесконечным](https://godbolt.org/z/G6aj4T1qE):

```cpp
const int N = 10;
int main() {
  int decade[N];
  for (int k = 0; k <= N; ++k) {
    printf("k is %d\n",k);
    decade[k] = -1;
  }
}
```

И фокус здесь не менее хитрый:

1. _decade\[k\] \= \-1;_ Обращение к элементу массива должно быть без UB\. А значит _k < N_;
1. Раз _k < N_, то условие продолжения цикла _k <\= N_ всегда истинно\. Проверять его не надо\. Оптимизировано\!

В этих примерах, конечно, сразу же должен броситься в глаза "<\=" в заголовках циклов\. Но и с более привычным "<" тоже можно изобрести себе проблемы\. Константа _N_, например, может быть не связана с размером массива\. И всё, приехали\.

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

* не использовать отдельно висящие константы при проверке размеров\. Лучше _std::size\(\)_ или метод _size\(\)_;
* писать меньше сырых циклов со счётчиками\. Предпочтительнее _range\-based\-for_ или стандартные алгоритмы из _\#include <algorithm\>_;
* не использовать _operator\[\]_, когда не критична производительность\. Безопаснее метод _at\(\)_ контейнера, проверяющий границы\.

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

1. Brendan Watters\. [Stack\-Based Buffer Overflow Attacks: Explained and Examples](https://www.rapid7.com/blog/post/2019/02/19/stack-based-buffer-overflow-attacks-what-you-need-to-know/)\.  
1. Dhaval Kapil\. [Buffer Overflow Exploit](https://dhavalkapil.com/blogs/Buffer-Overflow-Exploit/)\.
1. Михаил Гельвих\. [Как не надо проверять размер массива в С\+\+](https://pvs-studio.ru/ru/blog/posts/cpp/1112/)\.

## Исполнение программы: поддержка сборщика мусора \(неактуально для C\+\+23 и новее\)

Да, вы не ослышались\. И глаза вам не врут\. И я не сошёл с ума\. И вы тоже\. Скорее всего\.

C\+\+ — уникальный язык\. В его стандарте есть описание того, что в языке почти наверняка не появится\. Есть поддержка сборщика мусора, но самого сборщика мусора нет\. И поддержка эта сделана самым естественным для C\+\+ способом: введением неопределённого поведения\.

Неопределённое поведение возникает в следующей ситуации:

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

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

Но у нас нет\! Ни один из компиляторов его не поддерживает\! А стандарт [поддерживает](https://isocpp.org/wiki/faq/cpp11-library#gc-abi)\.

Так что, если вы, например, храните в младших битах указателя \(а это иногда можно делать из\-за выравнивания\) какую\-то метаинформацию, экономя память, скорее всего, в вашей программе есть UB, связанное с поддержкой сборщика мусора\. Оно наверняка никогда не выстрелит, но оно есть\.

```cpp
template <class T>
struct MayBeUninitialized {
  static_assert(alignof(T) >= 2);
    
  MayBeUninitialized() {
    // Выделяем сырую память с помощью явного вызова operator new.
    // Вся эта ерунда с поддержкой сборщика мусора описана
    // только для глобального operator new. std::malloc,
    // placement new и прочие не участвуют.
    ptr_repr_ = reinterpret_cast<uintptr_t>(
                  operator new (sizeof(T), 
                                std::align_val_t(alignof(T))));
    // Единственный указатель только был создан и
    // сразу же уничтожился.
    ptr_repr_ |= 1; // set unitialized flag
  }

  ~MayBeUninitialized() {
    Deinit();
    operator delete(GetPointer(), sizeof(T),
                    std::align_val_t(alignof(T)));
  }

  void Deinit() {
    if (!IsInitialized()) {
      return;
    }
    GetPointer()->~T();
  }

  bool IsInitialized() const {
    return !(ptr_repr_ & 1);
  }

  void Set(T x) {
    Deinit();
    new (GetPointer()) T(std::move(x));
    // drop unitialized flag
    ptr_repr_ &= (~static_cast<uintptr_t>(1));
  }


  const T& Get() const {
    if (!IsInitialized()) {
      throw std::runtime_error("not init");
    }
    return *GetPointer(); // UB
  }

private:
  T* GetPointer() const {
    constexpr auto mask = ~static_cast<uintptr_t>(1);
    auto ptr = reinterpret_cast<T*>(ptr_repr_ & mask);
    // Восстановили указатель. Но разыменование его — UB.
    return ptr;
  }

  uintptr_t ptr_repr_;
};
```

Устраняется такое недоразумение с бессмысленным для текущего положения дел в C\+\+ неопределённым поведением при помощи пары функций _declare\_reachable_ и _undeclare\_reachable_:

```cpp
MayBeUninitialized() {
  void* ptr = operator new (sizeof(T),
                            std::align_val_t(alignof(T)));
  std::declare_reachable(ptr);
  ptr_repr_ = reinterpret_cast<uintptr_t>(ptr);
  // Единственный указатель только был создан и
  // сразу же уничтожился, но мы пометили память под ним
  // достижимой, чтобы отвадить мифический сборщик мусора.
  ptr_repr_ |= 1; // set unitialized flag
}
    
~MayBeUninitialized() {
  Deinit();
  void* ptr = GetPointer();
  std::undeclare_reachable(ptr);
  operator delete (ptr, sizeof(T), std::align_val_t(alignof(T)));
}
```

Эти функции в настоящее время ничего не делают\. Они нужны только для формального следования букве стандарта\.

Если вы верите, что когда\-нибудь в C\+\+ появится сборщик мусора, будьте любезны пользоваться этими прекрасными функциями, чтобы ваша программа оставалась корректной и в далёком будущем\.

Если не верите, то можете про них забыть\. Пожалуй, это единственное UB, которое нигде и никак не проявляется\. И не проявится\. Скорее всего не проявится\. Даже есть предложения [удалить](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2186r0.html) эту совершенно дурную для C\+\+ "фичу"\.

Надо понимать, что сам по себе сборщик мусора для C\+\+ не является чем\-то сверхъестественным\. На C и C\+\+ написаны, например, сборщики мусора для JVM\. Никто не мешает задействовать их же в C\+\+\-программах: просто используем альтернативные функции для выделения памяти\. С их помощью даже можно переопределить поведение операторов _new_ и _delete_\. Но очень мало какой код на C\+\+ пишется с предположением, что под этими операторами работает сборщик мусора\.

Проверить, не запустили ли вашу программу в светлом мире со сборщиком мусора, можно, вызвав функцию _get\_pointer\_safety_\. Она возвращает одно из трёх значений:

* _pointer\_safety::strict_ — играть с восстановлением указателей абы откуда просто так нельзя\. Сборщик мусора, возможно, работает\.
* _pointer\_safety::relaxed_ — с указателями нет никаких проблем, выделенная память никуда сама по себе не денется\.
* _pointer\_safety::preferred_ — с указателями нет никаких проблем; выделенная память никуда сама по себе не денется, но, возможно, работает детектор утечек, которому важны пометки _declare\_reachable_/_undeclare\_reachable_\.

```cpp
int main() {
  switch (std::get_pointer_safety())
  {
  case std::pointer_safety::strict:
    std::cout << "strict" << std::endl;
    break;
  case std::pointer_safety::relaxed:
    std::cout << "relaxed" << std::endl;
    break;
  default:
    std::cout << "preferred" << std::endl;
  }
}
```

Отмечу, что при запуске этого кода под _valgrind\-3\.15\.0_ для _Ubuntu 20\.04 \(x86\_64\)_ выводимое сообщение \(_relaxed_\) никак не меняется\.

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

1. Cppreference\. [std::undeclare\_reachable](https://en.cppreference.com/w/cpp/memory/gc/undeclare_reachable)\. 
1. Cppreference\. [std::declare\_reachable](https://en.cppreference.com/w/cpp/memory/gc/declare_reachable)\.
1. C\+\+ Standards Committee Papers\. [Removing Garbage Collection Support](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2186r0.html)\. 
1. C\+\+ Standards Committee Papers\. [Minimal Support for Garbage Collection and Reachability\-Based Leak Detection](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2670.htm)\.
1. Wikipedia\. [Boehm garbage collector](https://en.wikipedia.org/wiki/Boehm_garbage_collector)\.
1. C\+\+ FAQ\. [Garbage collection ABI](https://isocpp.org/wiki/faq/cpp11-library#gc-abi)\.

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

Более восьми лет работает в сфере коммерческой разработки высокопроизводительного программного обеспечения на 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, заключение\.