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

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

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

## Исполнение программы: тривиальные типы и ABI

Допустим, вы написали прекрасную библиотеку для работы с двумерными векторами\. И там, конечно же, была структура _Point_\. Вот такая:

```cpp
template <typename T>
struct Point {
  ~Point() {}

  T x;
  T y;
};
```

И была функция

```cpp
Point<float> zero_point();
```

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

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

```cpp
template <typename T>
struct Point {
  T x;
  T y;
};
```

Пересобрали библиотеку и разослали пользователям новые готовые _\.dll/\.so_ файлы\. С новыми заголовками, конечно же\. Но изменение такое незначительное, что пользователи просто подложили готовые бинари себе без перекомпиляции\.\.\. и всё упало со страшным memory corruption\.

Почему?

Это изменение сломало ABI\.

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

* _trivially\_constructible_ — не надо ничего инициализировать;
* _trivially\_destructible_ — не нужно генерировать код для деструктора;
* _trivially\_copyable_ — не нужно ничего, кроме простого копирования байтов;
* _trivially\_movable_ — аналогично копированию, но при "перемещении"\.

С объектами тривиальных типов выполнимы дополнительные оптимизации\. Например, их можно передавать через регистры\. Компилятор способен догадаться соптимизировать _memcpy_ \(использованный, чтобы избежать неопределённого поведения\) в _reinterpret\_cast_\. И другие подобные вещи\.

Вот, например:

```cpp
struct TCopyable {
  int x;
  int y;
};
static_assert(std::is_trivially_copyable_v<TCopyable>);

struct TNCopyable {
  int x;
  int y;

  TNCopyable(const TNCopyable& other) :
    x{other.x}, y{other.y} {}

  // Вынуждены написать конструктор, так как
  // aggregate initialization отключился из-за
  // конструктора копирования.
  TNCopyable(int x, int y) : x{x}, y{y} {}
};

static_assert(!std::is_trivially_copyable_v<TNCopyable>);

// Здесь будет возврат через регистр rax.
// TCopyable в него как раз помещается.
extern TCopyable test_tcopy(const TCopyable& c) {
  return {c.x * 5, c.y * 6};
} 

// Здесь возврат через указатель,
// передаваемый через регистр rdi.
extern TNCopyable test_tnocopy(const TNCopyable& c) {
  return {c.x * 5, c.y * 6};
}
```

По ассемблерному листингу можно убедиться, что две "одинаковые" функции, возвращающие "одинаково" представленные в памяти структуры, делают это [по\-разному](https://godbolt.org/z/Mz8srfdsc):

```cpp
test_tcopy(TCopyable const&):     # @test_tcopy(TCopyable const&)
  mov     eax, dword ptr [rdi]
  mov     ecx, dword ptr [rdi + 4]
  lea     eax, [rax + 4*rax]
  add     ecx, ecx
  lea     ecx, [rcx + 2*rcx]
  shl     rcx, 32
  or      rax, rcx  #!
  ret
test_tnocopy(TNCopyable const&):  # @test_tnocopy(TNCopyable const&)
  mov     rax, rdi
  mov     ecx, dword ptr [rsi]
  mov     edx, dword ptr [rsi + 4]
  lea     ecx, [rcx + 4*rcx]
  add     edx, edx
  lea     edx, [rdx + 2*rdx]
  mov     dword ptr [rdi], ecx     #!
  mov     dword ptr [rdi + 4], edx #!
  ret
```

[Аналогично](https://godbolt.org/z/KK1o5E168) и с вашей 2D точкой:

```cpp
struct TPoint {
  float x;
  float y;
};
static_assert(std::is_trivially_destructible_v<TPoint>);

struct TNPoint {
  float x;
  float y;
  ~TNPoint() {} // user-provided деструктор
                // делает тип нетривиальным, даже
                // если деструктор ничего не делает.
};

static_assert(
  !std::is_trivially_destructible_v<TNPoint>);

// Возврат через регистр
extern TPoint zero_point() {
  return {0,0};
} 

// Возврат через указатель
extern TNPoint zero_npoint() {
  return {0,0};
}

zero_point():                    # @zero_point()
  xorps   xmm0, xmm0
  ret
zero_npoint():                   # @zero_npoint()
  mov     rax, rdi
  mov     qword ptr [rdi], 0
  ret
```

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

Возьмём, например, реализацию шаблона _pair_ в стандартной библиотеке C\+\+03\. В нём можно [увидеть](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_pair.h#L624) _user\-provided_, ничего дополнительно не делающие, конструкторы копий\. В C\+\+11 и выше их уже [нет](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/stl_pair.h#L210)\. Так что это ещё одна точка бинарной несовместимости библиотек на старом C\+\+ с библиотеками нового C\+\+\.

Проблемы со сломом ABI вокруг тривиальных типов могут застать вас врасплох не только в самом C\+\+, но и в любом другом языке, если вы попытаетесь через него общаться с плюсовыми библиотеками\. Например, в утилите для генерации биндингов для Rust есть [баг](https://github.com/rust-lang/rust-bindgen/issues/778)\.

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

Также помните, что тривиальность:

1. Легко ломается \(достаточно [добавить](https://godbolt.org/z/8z5GfTTTv) инициализатор, и тип уже не _trivially\_constructible_\)\.
1. Соседствует с [неинициализированными](https://godbolt.org/z/ohzov6h76) полями\. А чтение неинициализированных переменных заканчивается неопределённым поведением\.
1. Не всегда влияет на ABI \(в основном влияют деструкторы и конструкторы копирования/перемещения\)\.

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

1. Cppreference\. [std::is\_trivial](https://en.cppreference.com/w/cpp/types/is_trivial)\. 
1. [Great C\+\+ is\_trivial \- Jason Turner \- CppCon 2019](https://www.youtube.com/watch?v=ZxWjii99yao)\.

## Исполнение программы: неинициализированные переменные

Очень известный и распространённый источник проблем не только в C или C\+\+\.

Новые современные языки программирования обычно запрещают использование неинициализированных переменных\. Переменные либо всегда инициализируются значением по умолчанию \(например, в [Go](https://go.dev/ref/spec#The_zero_value)\), либо попытка чтения из неинициализированной переменной даёт ошибку компиляции \(в [Kotlin](https://play.kotlinlang.org/#eyJ2ZXJzaW9uIjoiMS40LjMwIiwiY29kZSI6Ii8qKlxuICogWW91IGNhbiBlZGl0LCBydW4sIGFuZCBzaGFyZSB0aGlzIGNvZGUuIFxuICogcGxheS5rb3RsaW5sYW5nLm9yZyBcbiAqL1xuXG5mdW4gbWFpbigpIHtcbiAgICBwcmludGxuKFwiSGVsbG8sIHdvcmxkISEhXCIpXG4gICAgdmFyIHggOiBJbnQ7XG4gICAgKyt4O1xuICAgIHByaW50bG4oeCk7XG59IiwicGxhdGZvcm0iOiJqYXZhIiwiYXJncyI6IiJ9) или в [Rust](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=261f92c8ca39b10c1ac565e4f8a1e28a)\)\.

C и C\+\+ — старые языки\. В них можно легко и просто объявить переменную, а инициализировать её когда\-нибудь потом\. Или забыть иницаилизировать вовсе\. Но в отличие от совсем низкоуровневого ассемблера, в котором читать из неинициализированной переменной никто не запрещает \(ну получите вы свои мусорные байтики и ладно\), в C и C\+\+ \(а также в Rust, см\. [MaybeUninit](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html)\) это влечёт за собой неопределённое поведение\.

Неожиданный вариант такого UB можно наблюдать на следующем примере \(взято [тут](https://stackoverflow.com/questions/54120862/does-the-c-standard-allow-for-an-uninitialized-bool-to-crash-a-program)\):

```cpp
struct FStruct {
  bool uninitializedBool;
    
  // Конструктор, не инициализирующий поля.
  // Чтобы проблема воспроизвелась,
  // конструктор должен быть определён в
  // другой единице трансляции.
  // Можно сымитировать с помощью атрибута noinline 
  __attribute__ ((noinline)) FStruct() {};
};

char destBuffer[16];

void Serialize(bool boolValue) {
  const char* whichString = boolValue ? "true" : "false";
  size_t len = strlen(whichString);
  memcpy(destBuffer, whichString, len);
}

int main()
{
  // Конструируем объект с неинициализированным полем.
  FStruct structInstance;
    
  // UB!
  Serialize(structInstance.uninitializedBool);
    
  //printf("%s", destBuffer);
  return 0;
}
```

Программа [падает](https://godbolt.org/z/rvren9er8)\. Поскольку неинициализированных переменных в корректной программе не бывает, компилятор полагает _boolValue_ всегда валидным и выполняет следующую занятную оптимизацию:

```cpp
// size_t len = strlen(whichString); // 4 или 5!
   size_t len = 5 - boolValue;
```

"Физическое" значение неинициализированной _bool_ переменной это не обязательно _true_/_false_, а, например, 7\. Поэтому в некоторых случаях получится _len \= 5 \- 7_\. Как следствие — переполнение буфера при вызове _memcpy_\.

Так если отсутствие неинициализированных переменных способствует оптимизациям, почему бы их не запретить совсем с жёсткой ошибкой компиляции?

Во\-первых, они позволяют экономить на спичках:

```cpp
int answer;
if (value == 5) {
  answer = 42;
} else {
  answer = value * 10;
}
```

Если бы нам было запрещено объявлять переменную без инициализации, мы бы либо вынуждены были написать

```cpp
int answer = 0;
```

И потратить в отладочной сборке целую одну лишнюю инструкцию _xor_ на зануление\!

Либо завернуть вычисление _answer_ в отдельную функцию \(или лямбда\-функцию\) и получить целый _call_ вместо _jmp_, если компилятор не проведёт оптимизацию\!

Либо использовать тернарный оператор и получить что\-то совершенно нечитаемое, если веток условий будет больше\.

Во\-вторых, иногда спички большие и дорогие\. И экономия оправдана:

```cpp
constexpr int data_size = 4096;
char buffer[data_size];
read(fd, buffer, data_size);
```

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

#### Получаем неинициализированные переменные и избегаем их

Сначала разберёмся: какие конструкции порождают неинициализированные переменные?

Специальные функции, например _std::make\_unique\_for\_overwrite_, мы не рассматриваем\. Как и функции выделения сырой памяти \(_\*alloc_ тоже\)\. Хочу напомнить, что писать _\(T\*\)malloc\(N\)_ в ожидании инициализированной памяти нельзя\. Для этого есть _calloc_\.

В более общем случае, если верно, что _is\_trivially\_constructible<T\> \=\= true_, то

1. _T x_;
1. _T x\[N\]_;
1. _T\* p \= new T_;
1. _T\* p \= new T\[N\]_;

[Порождают](https://godbolt.org/z/nv3938McY) неинициализированные переменные/массивы \(или указатели на неинициализированные переменные/массивы\)\.

Если тип нетривиально\-конструируемый, не спешите радоваться\. Его конструктор по умолчанию мог [забыть](https://godbolt.org/z/T3bs5fb98) что\-то проинициализировать\. Или тип просто потерял тривиальность только потому, что кто\-то [предоставил деструктор](https://godbolt.org/z/bov8vdPE5), чтобы вас запутать, или объявил [виртуальный метод](https://godbolt.org/z/8xGGxbc8a)\. А инициализацию так и не обеспечил\.

<details>
   <summary>Нетривиально конструируемый тип\\\.</summary>

```cpp
#include <iostream>
#include <cstring>
#include <type_traits>

struct S {
  int uninit;
  ~S() {}
};

static_assert(!std::is_trivially_constructible_v<S>);

int main()
{
  // Locally construct an instance of our struct here on the stack.
  // The bool member uninitializedBool is uninitialized.
  S uninit1;
  std::cout << uninit1.uninit << "\n";
  S uninit2[2];
  std::cout << uninit2[1].uninit << "\n";
  S* puninit = new S;
  std::cout << puninit->uninit << "\n";
  S* puninit2 = new S[2];
  std::cout << puninit2[1].uninit << "\n";
    
  int* p = new int;

  std::cout << *p << "\n";
  // Output "true" or "false" to stdout.
  return 0;
}
```

Этот код компилируется с помощью Clang 18\.1 несмотря на ключи \-Wall \-Wextra \-O3 \-std\=c\+\+17 \-Wpedantic \-Wuninitialized\. И выводит мусор:

```cpp
272846040
-658903036
-658903036
-658903036
-658903036
```

С GCC 14\.1 и теми же ключами этот код выводит нули, а компилятор выдаёт предупреждения, но не во всех случаях\.



```cpp
<source>: In function 'int main()':
<source>:17:36: warning: 'uninit1.S::uninit' is used uninitialized
[-Wuninitialized]
   17 |     std::cout << uninit1.uninit << "\n";
      |                                    ^~~~
<source>:16:7: note: 'uninit1.S::uninit' was declared here
   16 |     S uninit1;
      |       ^~~~~~~
<source>:19:39: warning: 'uninit2[1].S::uninit' is used
uninitialized [-Wuninitialized]
   19 |     std::cout << uninit2[1].uninit << "\n";
      |                                       ^~~~
<source>:18:7: note: 'uninit2' declared here
   18 |     S uninit2[2];
```

Но стоит лишь убрать \-O3, и [предупреждения исчезнут](https://godbolt.org/z/c7jrx1Ks8)\!


</details>
Распространённый совет по повсеместному использованию \{\} при объявлении переменных работает и гарантирует инициализацию нулями только с тривиальными типами\. Для нетривиальных — всё на совести [конструктора](https://godbolt.org/z/Y3r1WPv7z)\.

Но иногда вам может "повезти", и инициализация пройдёт \(гарантированно стандартом\!\) в два этапа: сначала нулями, потом вызовется конструктор по умолчанию\. Подробнее [тут](https://en.cppreference.com/w/cpp/language/value_initialization)\. Мне удалось воспроизвести этот эффект только при использовании _std::make\_unique;_\.

Как бороться с неинициализированными переменными и связанными с ними неопределённым поведением?

1\. Не разрывать объявление и инициализацию\. Вместо этого использовать конструкции:

```cpp
auto x = T{...};
auto x = [&] { ... return value }();
```

2\. Проверять свои конструкторы, чтобы в них были инициализированы все поля\.

3\. Пользоваться инициализаторами по умолчанию при объявлении полей структур\.

4\. Использовать свежие версии компиляторов: они всё лучше учатся выявлять обращения к неинициализированным значениям\.

5\. Не использовать _new T_, если вы не уверены в том, что делаете\. Всегда _new T\{\}_ или _new T\(\)_\.

6\. Не забывать про динамический и статический анализ внешними утилитами\. Valgrind умеет ловить обращения к неинициализированной памяти\. В PVS\-Studio этой проблеме посвящён целый пучок диагностик\.

#### И последнее

Если вам когда\-нибудь придёт светлая мысль использовать неинициализированную память в качестве источника случайности, гоните её как можно быстрее\! Некоторые пробовали — [не получилось](https://kqueue.org/blog/2012/06/25/more-randomness-or-less/)\.

## Исполнение программы: С\+\+20 unbounded ranges

Поддержка работы как с кастомными, так и со стандартными коллекциями в C\+\+ от версии к версии всё лучше и лучше\.

Алгоритмы стандартной библиотеки образца 11\-17 стандартов работали и работают с парами итераторов, задающих диапазон элементов коллекции\.

```cpp
const std::vector<int> v = {1,2,3,4,5};
std::vector<int> odds;
std::copy_if(
  v.begin(), v.end(), std::back_inserter(odds),
  [](int x){ return x % 2 == 0;});
std::vector<int> squares;
std::transform(
  odds.begin(), odds.end(), std::back_iserter(squares),
  [](int x) { return x * x;});
// return squares;
```

Многословно, неудобно\. Да ещё и совсем не zero cost — лишние аллокации обычно ни один компилятор C\+\+ не оптимизирует\. Но, конечно, мы можем провести все оптимизации самостоятельно — алгоритмы старого STL, работающие с итераторами, довольно гибкие в выборе того, что и как вы хотите сделать\.

```cpp
std::vector<int> v = {1,2,3,4,5};
v.erase(
  std::remove_if(v.begin(), v.end(),
                 [](int x){ return x % 2 != 0; }),
  v.end()
);
std::transform(v.begin(), v.end(), v.begin(),
               [](int x){return x * x;});
return v;
```

Отлично, ни одной лишней аллокации\! Но всё так же многословно и путанно\. Да ещё и странно выглядящая конструкция _erase\-remove_\.

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

В C\+\+11 появился _range\-based for_\. И стало удобно просто итерироваться по коллекции\.

```cpp
for (auto x : v) {
  // do something with x
}
```

Но так итерироваться можно лишь по всей коллекции\. А что, если вы хотите от пятого элемента в векторе до десятого? Пишите цикл со счётчиком\. Либо используйте _std::for\_each_ с парой итераторов\.

```cpp
std::for_each(v.begin() + 5, v.begin() + 10, [&](auto x) {
  // do something
});
```

Либо вам нужно откуда\-то — из книг, курсов или из самого стандарта — узнать, что _range\-based\-for_ автоматически работает для любого объекта, у которого есть методы _begin_ и _end_\.

_begin\(\)_ и _end\(\)_ должны возвращать итераторы\. Если они возвращают что\-то другое, то в 99\.9% случаев вы получите ошибку компиляции \([иногда вразумительную](https://godbolt.org/z/Yh8zeoedx)\)\. В экзотических случаях может быть что\-то [неожиданное](https://godbolt.org/z/xrEqhxrKc)\.

Из всего этого возникает вполне здравая идея: а что, если для итерирования по части коллекции сделать структуру с итераторами? И для всяких _transform_, _filter_, _reverse_\.\.\. Ух\! Да это же как раз C\+\+20 ranges \(изначально [ranges\-v3](https://github.com/ericniebler/range-v3)\)\.

И вот мы уже разрабатываем свою библиотеку для удобной работы с коллекциями, также удобно работающую с _range\-based\-for_\. И всё хорошо до тех пор, пока нас не посещает идея: а не сделать ли ленивую процедурно\-генерируемую последовательность с совместимым интерфейсом? 

Например, мы можем сделать "бесконечный" генератор чисел:

```cpp
struct Numbers {
  struct End {};  // Тип-маркер для проверки, достигли ли конца.
  struct Number { // Сам итератор-генератор. Требуются 3 операции.
    int x;
    // Проверка достижения конца.
    bool operator != (End) const { 
      return true;
    }
    // Генерация текущего элемента.
    int operator*() const {
      return x;
    }
    // Продвижение итератора — переход к следующему элементу.
    Number operator++() {
      ++x;
      return *this;
    }
  };

  explicit Numbers(int start) : begin_{start} {}

  Number begin_;

  auto begin() { return begin_; }
  End end() { return {}; }
};
```

И вот тут начинается засада\.

Ни старые алгоритмы STL, ни _range\-based\-for_ не работают — [не компилируются](https://godbolt.org/z/8Pchborav)\. Потому что требуют, чтобы _begin_ и _end_ имели одинаковый тип\.

Хорошо, мы можем исправить это относительно [безболезненно](https://godbolt.org/z/9vWMTzMrj) в нашем простеньком примере:

```cpp
struct Numbers {
  struct Number {
    int x;
    bool operator != (Number) const { 
      return true;
    }
    int operator*() const {
      return x;
    }
    Number operator++() {
      ++x;
      return *this;
    }
  };

  explicit Numbers(int start) : begin_{start} {}

  Number begin_;

  auto begin() { return begin_; }
  auto end() { return begin_; }
};
```

Правда, семантика _operator \!\=_ стала странной\. Да и нужно _end\(\)_ из чего\-то конструировать\. Если состояние нашего генератора будет более сложным, например, выделяющим что\-то на куче, мы получим дополнительные накладные расходы\. Не очень zero\-cost\.

Поэтому в C\+\+17 _range\-based\-for_ исправили\. Теперь он может [работать](https://godbolt.org/z/sG5GjGjhq) с граничными итераторами разных типов\.

Но STL\-алгоритмы всё так же [не работают](https://godbolt.org/z/9zPr1bMPP)\.

```cpp
auto nums = Numbers(10);

// Compilation error :
auto pos = std::find_if(nums.begin(), nums.end(), 
                        [](int x){ return x % 7 == 0;});  

std::cout << *pos;
// Ошибка компиляции...
```

В С\+\+20 наконец\-то всё пофиксили\. Нет, старые STL\-алгоритмы всё так же не работают\. Просто теперь есть новые STL\-алгоритмы: почти такие же, как старые, только в пространстве имён _std::ranges_ и жёстко требующие удовлетворения новых концептов итераторов\. Поэтому пример ниже слегка распухает\.

```cpp
struct Numbers {
  struct End {

  };
  struct Number {
    using difference_type = std::ptrdiff_t;
    using value_type = int;
    using pointer = void;
    using reference = value_type;
    using iterator_category = std::input_iterator_tag;

    int x;
    bool operator == (End) const { 
      return false;
    }

    int operator*() const {
      return x;
    }
    Number& operator++() {
      ++x;
      return *this;
    }
    Number operator++(int) {
      auto ret = *this;
      ++x;
      return ret;
    }
  };

  explicit Numbers(int start) : begin_{start} {}

  Number begin_;

  auto begin() { return begin_; }
  End end() { return {}; }
};
```

С ними следующий код компилируется и [работает](https://godbolt.org/z/9cPTnqxYG)\.

```cpp
auto nums = Numbers(10);

auto pos =
  std::ranges::find_if(nums.begin(), nums.end(),
                       [](int x){ return x % 7 == 0;});
std::cout << *pos;
```

Что ж\. Это было небольшое введение\. Теперь мы, наконец, можем начать отстреливать себе ноги\.

#### std::unreachable\_sentinel

Выдумывать на каждый итератор, генерирующий бесконечную последовательность, новый тип _\(EndSentinel\)_ для метода _end\(\)_ нам, благодаря C\+\+20, не надо\. В стандартной библиотеке определён тип _std::unreachable\_sentinel\_t_, задизайненный именно для этой цели\. Он сравним на равенство с любым объектом, "похожим" на _ForwardIterator_\. И результат сравнения всегда отрицательный\.

С ним наш пример с числами [упрощается](https://godbolt.org/z/6b4Ejbnrd)\.

```cpp
struct Numbers {
  struct Number {
    using difference_type = std::ptrdiff_t;
    using value_type = int;
    using pointer = void;
    using reference = value_type;
    using iterator_category = std::input_iterator_tag;

    int x;
    int operator*() const {
      return x;
    }
    Number& operator++() {
      ++x;
      return *this;
    }
    Number operator++(int) {
      auto ret = *this;
      ++x;
      return ret;
    }
        
  };

  explicit Numbers(int start) : begin_{start} {}

  Number begin_;

  auto begin() { return begin_; }
  auto end() { return std::unreachable_sentinel; } // !
};
```

Сравнение с _unreachable\_sentinel_ не требует выполнения никаких операций\. Так что его можно использовать, например, чтобы сформировать _range_, итерирование по которому будет происходить без проверки границ\.

Например:

```cpp
// Если у нас есть вектор, задающий перестановку.
std::vector<size_t> perm = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
std::ranges::shuffle(perm, std::mt19937(std::random_device()()));

// И нам поступают запросы на поиск позиции элемента,
// заведомо находящегося в векторе.
// size_t p = 7;
assert(p < perm.size());
return std::ranges::find(perm.begin(),
                         std::unreachable_sentinel, p) - perm.begin();
```

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

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

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

1. Cppreference\. [std::unreachable\_sentinel\_t, std::unreachable\_sentinel](https://en.cppreference.com/w/cpp/iterator/unreachable_sentinel_t)\.
1. Rainer Grimm\. [C\+\+20: The Ranges Library](https://www.modernescpp.com/index.php/c-20-the-ranges-library/)\.
1. Marius Bancila\. [C\+\+ code samples before and after Ranges](https://mariusbancila.ro/blog/2019/01/20/cpp-code-samples-before-and-after-ranges/)\.

## Исполнение программы: невиртуальные виртуальные функции

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

```cpp
class Processor {
public:
  virtual ~Processor() = default;
  virtual void start() = 0;
  // Stops execution, returns 'false' if already stopped.
  virtual bool stop() = 0;
};
```

Пользователи интерфейса нареализовывали своих имплементаций\. Все были счастливы, пока кто\-то не сделал асинхронную реализацию\. С ней почему\-то приложение стало падать\. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод _stop\(\)_ перед разрушением объекта\. Какая досада\!

Вы были уставшими и злыми\. А, быть может, это были и не вы, а какой\-то менее опытный коллега, которому поручили доработать интерфейс\. В общем, на свет родилась правка:

```cpp
class Processor {
public:
  virtual void start() = 0;
  // stops execution, returns `false` if already stopped
  virtual bool stop() = 0;
  virtual ~Processor() {
    stop();
  }
};
```

Логично? Да\!

Правильно? Нет\!

Если вам повезёт, то анализатор кода или достаточно умный компилятор [сможет](https://godbolt.org/z/PGeob9bn1) сообщить о проблеме\. В конструкторах и деструкторах в C\+\+ виртуальная диспетчеризация методов не работает\. В других языках, например, в C\# или Java, наоборот, доставляет свои проблемы\.

Почему так? При конструировании часть объекта\-наследника, используемая в переопределённом методе, может быть ещё не создана: конструкторы вызываются в порядке от базового класса к производному\. При деструктурировании наоборот — часть объекта\-наследника уже уничтожена, и, если позволить динамический вызов, можно легко получить _use\-after\-free_\.

Тема не новая и достаточно разжёванная, например, в статье "[Вызов виртуальных функций в конструкторах и деструкторах \(C\+\+\)](https://pvs-studio.ru/ru/blog/posts/cpp/0891/)"\.

Радуйтесь\! Это одно из немногих мест в C\+\+, где вас защитили от неопределённого поведения, связанного с временем жизни\!

Хорошо\. А если так? Добавим промежуточный вызов функции\.

```cpp
// processor.hpp
class Processor {
public:
  void start();
  // Stops execution, returns 'false' if already stopped.
  bool stop();

  virtual ~Processor();

protected:
  virtual bool stop_impl()  = 0;
  virtual void start_impl() = 0;
};


// processor.cpp
Processor::~Processor() {
  stop();
}

bool Processor::stop() {
  return stop_impl();
}
void Processor::start() {
  start_impl();
}
```

Компиляторы уже [не выдают](https://godbolt.org/z/Gao3hbeWW) замечательного предупреждения, и подвох заметить стало сложнее\. Анализатор PVS\-Studio так просто не обмануть, и пока он продолжает [выдавать](https://godbolt.org/z/s9f3PbGPG) [V1053](https://pvs-studio.ru/ru/docs/warnings/v1053/) Calling the 'stop\_impl' virtual function indirectly in the destructor may lead to unexpected result at runtime\. Check lines: 18, 22, 11\.

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

Вызов виртуальных функций класса в его конструкторах и деструкторах почти всегда является ошибкой сейчас или в будущем\. Если же это не ошибка и так и задумывалось, то стоит использовать явный статический вызов с указанием имени класса \(name qualified call\)\.

```cpp
// processor.cpp
Processor::~Processor() {
  Processor::stop();
}
```

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

```cpp
class Processor {
public:
  virtual void start() = 0;
  // Stops execution, returns 'false' if already stopped.
  virtual bool stop() = 0;

  virtual ~Processor() = default;
};

void Processor::start() {
  std::cout << "unsupported";
}

class MyProcessor : public Processor {
public:
  void start() override {
    // call default implementation
    Processor::start();
  }
};
```

Вернёмся опять к нашей остановке при вызове деструктора\. Как же с ней быть?

Есть два пути\.

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

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

```cpp
class Processor {
public:
  virtual void start() = 0;
  // Stops execution, returns 'false' if already stopped.
  virtual bool stop() = 0;
  virtual ~Processor() = 0;
};

// required!
Processor::~Processor() = default;

class MyProcessor : public Processor {
public:
  void start() override {
  }
  bool stop() override { return false; }
  // Missing destructor does not trigger CE.
  // ~MyProcessor() override = default; 
};


int main() {
  MyProcessor p;
}
```

Путь второй: добавить ещё один слой\. И пользоваться им во всех публичных API\.

```cpp
class GuardedProcessor {
  std::unique_ptr<Processor> proc;
  // ....
  ~GuardedProcessor() {
    assert(proc != nullptr);
    proc->stop();
  }
};
```

Вишенка на торте: играя с деинициализацией объектов, легко получить дублирование операций освобождения ресурсов\.

#### Освобождение ресурсов в деструкторах с использованием вспомогательных функций

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

Начнём с простейшего базового класса\.

```cpp
#include <memory>
#include <iostream>

class Resource
{
public:
  void Create() {}
  void Destroy() {}
};

class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    if (m_a != nullptr)
      m_a->Destroy();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<A>();
}
```

Пока всё хорошо\. Запускаемый [online\-пример](https://godbolt.org/z/z8EdqWv3n) распечатывает:

```cpp
~A()
```

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

```cpp
class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    Reset();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<A>();
}
```

[Online\-пример печатает](https://godbolt.org/z/hTs4YGGqc):

```cpp
~A()
A::Reset()
```

Добавился виртуальный метод _Reset_, освобождающий ресурсы\. Чтобы не дублировать код, деструктор теперь не сам освобождает ресурсы, а просто вызывает этот метод\.

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

Добавляем новый класс\-наследник\. У него будут свои собственные ресурсы вдобавок к существующим в базовом классе\. И их тоже нужно не забыть освободить в методе _Reset_\.

```cpp
class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    Reset();
  }
};

class B : public A
{
  std::unique_ptr<Resource> m_b;

public:
  void InitB()
  {
    m_b = std::make_unique<Resource>();
    m_b->Create();
  }

  void Reset() override
  {
    std::cout << "B::Reset()" << std::endl;
    if (m_b != nullptr)
    {
      m_b->Destroy();
      m_b.reset();
    }
    A::Reset();
  }

  ~B() override
  {
    std::cout << "~B()" << std::endl;
    Reset();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<B>();
  p->Reset();
  std::cout << "------------" << std::endl;
  p->InitA();
}
```

[Online\-пример печатает](https://godbolt.org/z/rPzbdrfo1):

```cpp
B::Reset()
A::Reset()
------------
~B()
B::Reset()
A::Reset()
~A()
A::Reset()
```

Если из внешнего кода явно вызываем метод _Reset_, то всё отрабатывает как и задумывалось: вызывается _B::Reset\(\)_, которая затем вызывает одноимённый метод из базового класса\.

А вот в деструкторах происходит что\-то неожиданное\. Каждый деструктор использует _Reset_ своего собственного класса, что порождает избыточный вызов\.

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

[Вывод кода](https://godbolt.org/z/dnGKEr95r), где добавлен ещё один класс:

```cpp
C::Reset()
B::Reset()
A::Reset()
------------
~C()
C::Reset()
B::Reset()
A::Reset()
~B()
B::Reset()
A::Reset()
~A()
A::Reset()
```

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

Заметив описанную ошибку и попытавшись её исправить, программист рискует допустить две другие типовые ошибки\.

**Первый вариант\.** Объявить методы _Reset_ невиртуальными и не вызывать в них базовые варианты \(_x::Reset_\)\. Тогда каждый деструктор будет вызывать только функцию _Reset_ из своего класса и освобождать только свои ресурсы\. Это действительно уберёт повторные вызовы в деструкторах, однако поломает очистку состояния объекта при вызове _Reset_ извне\. [Сломанный код распечатает](https://godbolt.org/z/TM9eqo4Pz):

```cpp
A::Reset()   // Сломали очистку ресурсов извне
------------
~C()
C::Reset()
~B()
B::Reset()
~A()
A::Reset()
```

**Второй вариант\.** Вызвать виртуальную функцию _Reset_ однократно только в деструкторе базового класса\. Это приведёт к утечкам, так как согласно правилам C\+\+ будет вызвана функция _Reset_, реализованная в базовом классе, а не в наследниках\. Это логично, так как к моменту вызова деструктора _\~A\(\)_ все наследники разрушены, и вызывать их методы нельзя\. [Сломанный код распечатает](https://godbolt.org/z/6cs9aj4aG):

```cpp
C::Reset()
B::Reset()
A::Reset()
------------
~C()
~B()
~A()
A::Reset()  // Утечка! Освобождены только ресурсы в базовом классе
```

Этот пример навеян реальным случаем бага, [найденным](https://pvs-studio.ru/ru/blog/posts/cpp/1125/) с помощью PVS\-Studio в движке qdEngine\. Так что это реальные ловушки\. Будьте бдительны, проектируя подобные классы\.

**Как же правильно реализовать классы, чтобы избежать множественных избыточных вызовов?**

Нужно отделить очистку ресурсов в классах от интерфейса для их очистки извне\.

Лучше всего возложить ответственность за освобождение ресурсов на соответствующие деструкторы полей наших классов и не писать свои собственные деструкторы _\~A\(\)_, _\~B\(\)_ и _\~C\(\)_\. Но не всегда такое возможно: типы для полей могут быть предоставлены библиотекой, которую вы не можете поправить, а на создание дополнительных обёрток у вас нет времени\.

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

Тогда деструкторы смогут просто делегировать свою работу функциям _ResetImpl_\.

Функция _Reset_ останется публичной и виртуальной\. Она будет очищать данные всех классов, используя всё те же вспомогательные функции _ResetImpl_\.

Соберём всё вместе и напишем корректный код:

```cpp
class A
{
  std::unique_ptr<Resource> m_a;

  void ResetImpl()
  {
    std::cout << "A::ResetImpl()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    ResetImpl();
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    ResetImpl();
  }
};

class B : public A
{
  std::unique_ptr<Resource> m_b;

  void ResetImpl()
  {
    std::cout << "B::ResetImpl()" << std::endl;
    if (m_b != nullptr)
    {
      m_b->Destroy();
      m_b.reset();
    }
  }

public:
  void InitB()
  {
    m_b = std::make_unique<Resource>();
    m_b->Create();
  }

  void Reset() override
  {
    std::cout << "B::Reset()" << std::endl;
    ResetImpl();
    A::Reset();
  }

  ~B() override
  {
    std::cout << "~B()" << std::endl;
    ResetImpl();
  }
};

class C : public B
{
  std::unique_ptr<Resource> m_c;

  void ResetImpl()
  {
    std::cout << "C::ResetImpl()" << std::endl;
    if (m_c != nullptr)
    {
      m_c->Destroy();
      m_c.reset();
    }
  }

public:
  void InitC()
  {
    m_c = std::make_unique<Resource>();
    m_c->Create();
  }

  void Reset() override
  {
    std::cout << "C::Reset()" << std::endl;
    ResetImpl();
    B::Reset();
  }

  ~C() override
  {
    std::cout << "~C()" << std::endl;
    ResetImpl();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<C>();
  p->Reset();
  std::cout << "------------" << std::endl;
}
```

[Online\-пример печатает](https://godbolt.org/z/75YWWoW68):

```cpp
C::Reset()
C::ResetImpl()
B::Reset()
B::ResetImpl()
A::Reset()
A::ResetImpl()
------------
~C()
C::ResetImpl()
~B()
B::ResetImpl()
~A()
A::ResetImpl()
```

Не сказать, что красиво получилось\. В глазах рябит от букв A, B, C, и очень легко опечататься\. Простим это синтетическим примерам\. Избыточность побеждена многословностью\!

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

1. Андрей Карпов\. [Вызов виртуальных функций в конструкторах и деструкторах \(C\+\+\)](https://pvs-studio.ru/ru/blog/posts/cpp/0891/)\.
1. Андрей Карпов\. [С\+\+: освобождение ресурсов в деструкторах с использованием вспомогательных функций](https://pvs-studio.ru/ru/blog/posts/cpp/1125/)\.

## Исполнение программы: Varialbe Length Arrays

Память в C и C\+\+ под наши объекты, как известно, можно выделять на стеке, а можно в куче\. На стеке она обычно выделяется автоматически, и нам об этом сильно беспокоиться не надо\.

```cpp
int32_t foo(int32_t x, int32_t y) {
  int32_t z = x + y;
  return z;
}
```

При вызове функции _foo_ на стеке после аргументов \(хотя не факт, что аргументы будут переданы через стек\) будет выделено \(просто вершина стека будет сдвинута\) ещё 4 байта под переменную _z_\. А может быть и больше, кто ж знает, что там настроено у компилятора\! А может быть и не будет выделено\. Например, если компилятор оптимизирует переменную и сложит результат сразу в регистр _rax_\. Дикая природа удивительна, не правда ли?

Освобождается память со стека тоже автоматически, причём всегда\. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку, и теперь адрес возврата не ведёт куда\-то не туда, или не используете _attribute \( \( naked \) \)_\. Но, мне кажется, в этих случаях у вас куда более серьёзные проблемы\.\.\. Во всех остальных случаях память со стека освобождается автоматически\. Потому, как известно, вот такой код порождает висячий указатель:

```cpp
int32_t* foo(int32_t x, int32_t y) {
  int32_t z = x + y;
  return &z;
}
```

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

Однажды один большой и сложный HTTP\-сервер внезапно упал\. Упал он, как ни странно, с моим любимым сообщением segmentation fault \(core dumped\)\. К этому все, впрочем, уже привыкли, ведь HTTP\-сервер был написан на чистом и прекрасном C\. Так что падение — это что\-то само собой разумеющееся\.

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

Закончился стек\.

Но как же так?\! В core dump было всего от силы 40 стэк\-фреймов\! Как он мог закончится? Там же 10 мегабайт под Linux\!

Путешествуя по этому стэк\-трейсу, я поднялся на пять стек\-фреймов выше\. Все они были довольно небольшого размера: 40 байт, 180, килобайт\.\.\. А вот шестой фрейм оказался невероятно большим\! 8 мегабайт\!

Открыв соответствующий исходник, я обнаружил:

```cpp
int encoded_len = request->content_len * 4 / 3 + 1;
char encoded_buffer[encoded_len];
encoded_len =
  encode_base64(request->content, content_len,
                encoded_buffer, encoded_len);
process(encoded_buffer, enconded_len);
```

Знакомьтесь, [variable\-length array](https://pvs-studio.ru/ru/blog/terms/6659/) \(VLA\)\! Прекрасная фича языка C\. Существует в языке C\+\+ как нестандартное расширение \(MSVC не поддерживает, в GCC и Clang компилируется\)\.

Это массив переменной длины **на стеке**\. Причина падения была [ясна](https://godbolt.org/z/Ps9deqnxj)\.

VLA, концептуально, фича довольно полезная\. Но крайне небезопасная\. Вам нужен буффер, но его длину вы узнаете только в runtime? Пожалуйста, VLA\! Не нужен никакой _malloc_/_new_? Просто объяви массив и укажи длину\! К тому же это в среднем намного быстрее, чем _malloc_, и не утечёт, автоматически освободится\! Ну и, конечно же, что может быть лучше, чем получить _segfault_ вместо _out\-of\-memory_?

Лучше VLA может быть только прямое использование функции _alloca\(\)_\. Ведь, в отличие от VLA, у неё намного больше вариативности по отрыванию ног\.

```cpp
void fill(char* ptr, int n) {
  for (int i =0;i<n;++i) {
    ptr[i] = i * i;
  }
}

int use_alloca(int n) {
  char* ptr = (char*)alloca(n);
  fill(ptr, n);
  return ptr[n-1];
}

int main() {
  int n = 0;
  for (int i = 1; i < 10000; ++i) {
    n += use_alloca(i);
  }
  return n ? 0 : 1;
}
```

Тут каждый вызов _alloca_ не приводит к переполнению стека сам по себе\. Но если _use\_alloca_ будет заинлайнена компилятором по какой\-либо причине, мы получим [SIGSEGV](https://godbolt.org/z/1xWsjqK4G)\.

Дополнительная прелесть, что вызов функции _alloca_ может быть куда\-то спрятан, и вы даже можете не догадываться, что её используете\. Например, _alloca_ живёт внутри макроса для конвертации строк A2W \(библиотека ATL\)\. Казалось бы, вы просто конвертируете в цикле строчки\. И всё даже работает, пока строчек мало и они короткие\. А потом раз, и Stack Overflow\. Если не знать такой нюанс устройства макроса, её практически нереально найти, например, на code review\. Ладно, именно про _alloca_ в цикле PVS\-Studio [предупредит](https://pvs-studio.ru/ru/docs/warnings/v505/)\. Но в целом очень коварно\.

Использование _alloca_ и VLA крайне не рекомендуется\. Некоторые стандарты кодирования \(например, MISRA C\) вообще [запрещают](https://pvs-studio.ru/ru/docs/warnings/v2598/) использование VLA\.

[man](https://man7.org/linux/man-pages/man3/alloca.3.html) упоминает случай, когда их использование может быть оправдано: ваш код полагается на _setjmp/longjump_, и нормальный менеджмент динамически выделенной памяти может быть осложнён, а стек всё равно будет очищен даже при _longjmp_\. Не буду спрашивать, зачем оно вам\.\.\.

_alloca_ и VLA действительно в среднем быстрее, чем динамическое выделение памяти \(_new_, _malloc_\)\. Но если уж нужно действительно быстро, то вариант с преаллоцированным массивом или массивом фиксированной длины [получше будет](https://quick-bench.com/q/JWSPzPFknaSECE2W1fPiQvnEdGs)\.

<details>
   <summary>Немного замеров\\\.</summary>

Код:

```cpp
#include <vector>
#include <alloca.h>

int calc(char* arr, int n) {
    for (int i = 0; i < n; ++i) {
        arr[i] = (i * i) & 0xFF;
    }
    return arr[n-1];
}

int test_vla(int n) {
    char arr[n];
    return calc(arr, n);
}

int test_dyn(int n) {
    std::vector<char> v(n);
    return calc(v.data(), n);
}

int __attribute__ ((noinline)) test_alloca(int n) {
    char* arr = (char*)alloca(n);
    return calc(arr, n);
}

int test_fixed(int n) {
    char arr[10005];
    return calc(arr, n);
}

static void VLA(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  int size = 10000;
  for (auto _ : state) {
    if  (size < 10005) {
      size += 1; 
    }
    benchmark::DoNotOptimize(test_vla(size));
  }
}
// Register the function as a benchmark
BENCHMARK(VLA);

static void ALLOCA_TEST(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  int size = 10000;
  for (auto _ : state) {
    if  (size < 10005) {
      size += 1; 
    }
    benchmark::DoNotOptimize(test_alloca(size));
  }
}
// Register the function as a benchmark
BENCHMARK(ALLOCA_TEST);

static void DYN(benchmark::State& state) {
  // Code before the loop is not measured
  int size = 10000;
  for (auto _ : state) {
    if  (size < 10005) {
      size += 1; 
    }
     benchmark::DoNotOptimize(test_dyn(size));
  }
}
BENCHMARK(DYN);

static void FIXED(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  int size = 1;
  char fixed[10005];
  for (auto _ : state) {
    if  (size < 10005) {
      size += 1; 
    }
    benchmark::DoNotOptimize(calc(fixed, size));
  }
}
// Register the function as a benchmark
BENCHMARK(FIXED);

static void FIXED_LOCAL(benchmark::State& state) {
  // Code inside this loop is measured repeatedly
  int size = 1;
  for (auto _ : state) {
    if  (size < 10005) {
      size += 1; 
    }
    benchmark::DoNotOptimize(test_fixed(size));
  }
}
// Register the function as a benchmark
BENCHMARK(FIXED_LOCAL);
```

А теперь графики\.

![1193_book_pt_10_ru/image2.png](https://import.viva64.com/docx/blog/1193_book_pt_10_ru/image2.png)




</details>


А в C\+\+ \(без расширений\) VLA нет\. Там есть шаблоны, а они [не дружат](https://godbolt.org/z/vczz8df5Y) с VLA\.

```cpp
#include <iostream>

template <size_t N>
void test_array(int (&arr)[N]) {
  std::cout << sizeof(arr) << "\n";
}

int main(int argc, char* argv[]) {
  int fixed[15];
  int vla[argc];
  test_array(fixed);
  test_array(vla);      // Compilation error!
}
```

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

1. PVS\-Studio\. Терминология\. [Массив переменной длины](https://pvs-studio.ru/ru/blog/terms/6659/)\.
1. Jonathan Corbet\. [Variable\-length arrays and the max\(\) mess](https://lwn.net/Articles/749064/)\.
1. Linux manual page\. [alloca](https://man7.org/linux/man-pages/man3/alloca.3.html)\. 
1. Chris Wellons\. [Legitimate Use of Variable Length Arrays](https://nullprogram.com/blog/2019/10/27/)\.
1. Cppreference\. [Array declaration](https://en.cppreference.com/w/c/language/array)\. 
1. Документация PVS\-Studio\. [V505\. The 'alloca' function is used inside the loop\. This can quickly overflow stack](https://pvs-studio.ru/ru/docs/warnings/v505/)\.
1. Документация PVS\-Studio\. [V2598\. MISRA\. Variable length array types are not allowed](https://pvs-studio.ru/ru/docs/warnings/v2598/)\.

## Исполнение программы: ODR violation и разделяемые библиотеки

Ранее я рассматривал ODR violation в общих чертах и предупреждал о том, что может произойти, если случайно выбрать не то имя переменной, структуры или функции в C\+\+\. В этой же части я бы хотел продемонстрировать более изящный пример, не требующий приложения никаких усилий по написанию кривого кода\. Достаточно просто иметь кривой код в ваших third\-party зависимостях\.

Недавно я имел дело со странным баг\-репортом:


> Во внутреннем репозитории с пакетами обновилcя пакет с библиотекой \[GTest\]\(https://github\.com/google/googletest\) \\\(известная уважаемая библиотека для написания самых разных тестов на C\\\+\\\+\\\)\\\. И в результате обновления некоторые тесты в конечных приложениях стали внезапно падать\\\.
> 
> Падать они стали по\\\-разному: у одних стали валиться проверяющие ассерты, у других же всё работало, проверки проходили, но \[ctest\]\(https://cmake\.org/cmake/help/latest/manual/ctest\.1\.html\) рапортовал, что тестирующий процесс вышел с ненулевым кодом возврата\\\.

Если с первыми происходило что\-то совершенно невразумительное, то со вторыми можно было работать\.

Запускаем тест вручную: 5/5 passed\. Segmentation Fault\. О\!

Запустив тест под отладчиком и выведя бэктрейс, я получил нечто следующего вида:

```cpp
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7e123fe in __GI___libc_free (mem=0x55555556a) at
./malloc/malloc.c:3368
3368    ./malloc/malloc.c: No such file or directory.
(gdb) bt
#0  0x00007ffff7e123fe in __GI___libc_free (mem=0x55555556a) at
./malloc/malloc.c:3368
#1  0x00007ffff7fb75ea in
std::vector<std::__cxx11::basic_string<char, std::char_traits<char>,
std::allocator<char> >,
std::allocator<std::__cxx11::basic_string<char,
std::char_traits<char>, std::allocator<char> > > >::
~vector() () from ./libgtest.so
#2  0x00007ffff7db2a56 in __cxa_finalize (d=0x7ffff7fb5090) at
./stdlib/cxa_finalize.c:83
#3  0x00007ffff7fb2367 in __do_global_dtors_aux () from
./libgmock.so
__do_global_dtors_aux () from ./libgmock.so
```

Что\-то страшное и одновременно прекрасное произошло, осталось лишь понять, что именно\.

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

```cpp
file(GLOB files "*_test.cpp")
foreach(file ${files})
  add_test(...., ${file})
endforeach()
```

Я открыл исходники падающего и не падающего тестов: падающий тест использовал _gMock_, а не падающий не использовал\. Но при этом оба исполняемых файла были слинкованы с библиотекой _libgmock\.so_\.

Посмотрим ещё раз на фрагмент бэктрейса:

```cpp
std::allocator<char> > > >::~vector() () from ./libgtest.so
#2  0x00007ffff7db2a56 in __cxa_finalize (d=0x7ffff7fb5090) at
./stdlib/cxa_finalize.c:83
#3  0x00007ffff7fb2367 in __do_global_dtors_aux ()
from ./libgmock.so
```

Финализация глобальных объектов в _libgmock_ как\-то связана с деструктором глобальной переменной в _libgtest_\.

Я открыл список изменений\. Что же там такое обновилось во внутреннем пакете с GTest?\.\.

```cpp
Commit: ...
Produce shared libraries along with statics
```

И ровно две строчки, добавляющие в его CMakeLists\.txt ещё и динамические версии библиотек _libgmock_ и _libgtest_\.

Интересно\. Я вышел в Интернет с этим вопросом и обнаружил что\-то очень [похожее](https://github.com/google/googletest/issues/930)\. Первым делом, глянув на дату issue, а также сверив даты коммитов во внутренней версии, я был глубоко разочарован темпами обновления зависимостей \(ведь на дворе был конец 2023 года, а issue датируется 2016\)\. Но это уже совсем другая проблема C\+\+\.\.\.

Что же произошло на самом деле?

Фреймворк GoogleTest содержал две библиотеки:

* **GTest** — core\-библиотека со всеми причиндалами для автоматической регистрации тестов и лёгкого их запуска через макрос _RUN\_ALL\_TESTS\._
* **gMock** — библиотека специально для mock\-тестирования\.

_gMock_ линкуется с _GTest_\. Конечный пользовательский исполняемый файл с тестами линкуется с обеими библиотеками\.

Всё нормально, ничего криминального в этом нет\.

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

Проблемным объектом, чей деструктор приводил к падению, оказался, как видно из бэктрейса, вектор строк в _libgtest\.so_ В исходниках _GTest_ я обнаружил, что этот вектор — глобальная переменная, куда _InitGoogleTest\(\)_ складывает распознанные аргументы командной строки\. Просто глобальная переменная, объявленная в компилируемом файле\. Она не была в заголовочном файле\. Всё вроде бы должно было быть хорошо\.\.\. за одним исключением: она не была помечена _static_ и не была обёрнута в анонимный _namespace_\.

И что? Ведь всё же работало? Да, работало\. Хитрость в том, как собирается библиотека gMock\. Воспроизведём всё пошагово\.

Заведём свой _GTest_ дома\.

```cpp
// gtest.h
#pragma once
void initGoogleTest(int argc, char* argv[]);
void runTests();

// gtest.cpp
#include "gtest.h"

#include <vector>
#include <string>
#include <iostream>

// Глобальная переменная,
// не static, как было в gtest
std::vector<std::string> g_args;

void runTests() {
  // Просто для демонстрации.
  std::cout << "run gtest\n";
  for (const auto& arg : g_args) {
    std::cout << arg << " ";
  }
  std::cout << "\n";
}

void initGoogleTest(int argc, char* argv[]) {
  for (int i = 0; i < argc; ++i) {
    g_args.push_back(argv[i]);
  }
}
```

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

```cpp
g++ -std=c++17 -fPIC -O2 -c gtest.cpp
ar rcs libgtest.a gtest.o
```

Добавим свой gMock

```cpp
// gmock.h
#pragma once
void runMocks();

// gmock.cpp
#include "gmock.h" 
#include "gtest.h" // gmock линкуется с gtest!

#include <iostream>

void runMocks() {
  // Для демонстрации.
  std::cout << "run Mocks:\n";
  runTests();
}
```

[Соберём](https://github.com/google/googletest/blob/d41f53ae7816863cc52bf3f357dff25597c58864/googlemock/make/Makefile#L89C1-L90C24) его также в статическую библиотеку\.

```cpp
g++ -std=c++17 -fPIC -O2 -c gmock.cpp
ar rcs libgmock.a gmock.o gtest.o  #
```

И начнём пользоваться\.

```cpp
// main.cpp
#include "gtest.h"
#include "gmock.h"

int main(int argc, char* argv[]) {
  initGoogleTest(argc, argv);
  runMocks();
  runTests();
  return 0;
}
```



```cpp
g++ -std=c++17 -O2 -o main main.cpp -L . -lgtest -lgmock
./main 1 2 3 4
run Mocks:
run gtest
./main 1 2 3 4 
run gtest
./main 1 2 3 4
```

Должно быть очевидно, что, прилинковав обе библиотеки _GTest_ и _gMock_, мы уже как бы получили ODR\-violation: _gMock_ содержит в себе _GTest_, а значит, у нас две копии глобальной переменной\. Но всё работает: линкер выкинул одну из копий\.

А теперь **добавим** динамические версии\.

```cpp
g++ -shared -fPIC -o libgtest.so gtest.o
g++ -shared -fPIC -o libgmock.so gtest.o gmock.o
```

И пересоберём клиентский код\.

```cpp
g++ -std=c++17 -O2 -o main main.cpp -L . -lgtest -lgmock
LD_LIBRARY_PATH=. ./main 
run Mocks:
run gtest
./main 
run gtest
./main 
Segmentation fault (core dumped)
```

Ура\! Падает\! Обе библиотеки имеют свою собственную версию глобальной переменной с одним и тем же неявно экспортируемым именем\. Использоваться опять\-таки будет только **одна**\. После загрузки библиотеки, после конструирования глобальной переменной стандарт C\+\+ требует зарегистрировать — например, через _\_\_cxa\_atexit_ — функцию для вызова деструктора\. У нас две библиотеки, значит, две функции будут вызваны\. На одном и том же объекте\. Double free\. Конструктор, кстати, также вызывается дважды по одному и тому же адресу:

```cpp
struct GArgs : std::vector<std::string> {
  GArgs() {
    std::cout << "Construct it: " <<
              uintptr_t(this) << "\n";
  }
};

GArgs g_args;

LD_LIBRARY_PATH=.  ./main 1 2 3
Construct it: 140368928546992
Construct it: 140368928546992
run Mocks:
run gtest
./main 1 2 3 
run gtest
./main 1 2 3 
Segmentation fault (core dumped)
```

И это прекрасно\. Хорошо\. Теперь проблема ясна\. Тесты, которые падали на ассертах, также страдали, но от других глобальных переменных\. Остался последний вопрос: я упомянул, что все тесты собирались одинаково, но какие\-то не падали — те, что не использовали _gMock_, но всё равно с ним линковались\.

Закомментируем использование нашего _gMock_\.

```cpp
#include "gtest.h"
#include "gmock.h"

int main(int argc, char* argv[]) {
  initGoogleTest(argc, argv);
  // runMocks();
  runTests();
  return 0;
}

g++ -std=c++17 -O2 -o main main.cpp -L . -lgtest -lgmock
LD_LIBRARY_PATH=.  ./main 1 2 3
Construct it: 140699707998384
run gtest
./main 1 2 3
```

О как\! Конструктор вызвался только раз\. Значит, и деструктор будет вызван только раз\. Всё отлично\. Но ведь мы же линковали\.\.\. Современные линковщики достаточно умны, чтобы не тащить то, что не используется \(что, кстати, иногда является проблемой, если у конструкторов в библиотеке есть побочные эффекты\)\.

Если мы заставим GCC прилинковать _gMock_ насильно\.

```cpp
g++ -std=c++17 -O2 -o main main.cpp
    -Wl,--no-as-needed -L . -lgtest -lgmock
LD_LIBRARY_PATH=.  ./main 1 2 3
Construct it: 139687276064944
Construct it: 139687276064944
run gtest
./main 1 2 3 
Segmentation fault (core dumped)
```

Всё будет сломано, как и должно\.

Подобный паттерн по созданию проблем оказывается невероятно распространённым\! И не только в C\+\+\.

_LibA_ статически влинковывается в _LibB_ и обе _LibA_ и _LibB_ влинковываются в _BinC_\. Самый частый кандидат на такую _LibA_ — библиотеки менеджмента памяти\.

Например, при сборке динамических библиотек в Rust и подключении их в другие Rust\-проекты практически всегда люди натыкаются на эту [проблему](https://users.rust-lang.org/t/rust-dynamic-library-dynamically-called-in-rust-randomly-crashed-when-statically-linked-to-std/11074): _std_ статически влинковывается и в библиотеку, и в исполняемый файл\.

Я также обнаружил подобные проблемы в AWS SDK: глобальный _UniquePtr_ также двумя путями попадает в конечное приложение, потому в его деструктор воткнули зануление, чтобы не вызывать _delete_ дважды\.

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

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