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

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

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

## Исполнение программы: \(N\)RVO vs RAII

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

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

Ну что ж, давайте попробуем\!

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

```cpp
struct Writer {
public:
  static const size_t BufferLimit = 10;

  // Захватываем устройство, в которое будет писать.
  Writer(std::string& dev) : device_(dev) {
    buffer_.reserve(BufferLimit);
  }

  // В деструкторе отпускаем, записывая всё,
  // что набуферизировали.
  ~Writer() {
    Flush();
  }

  void Dump(int x) {
    if (buffer_.size() == BufferLimit){
      Flush();
    }
    buffer_.push_back(x);
  }
private:
  void Flush() {
    for (auto x : buffer_) {
      device_.append(std::to_string(x));
    }
    buffer_.clear();
  }

  std::string& device_;
  std::vector<int> buffer_;
};
```

И попробуем им красиво воспользоваться:

```cpp
const auto text = []{
  std::string out;
  Writer writer(out);
  writer.Dump(1);
  writer.Dump(2);
  writer.Dump(3);
  return out;
}();
std::cout << text;
```

[Работает\!](https://godbolt.org/z/bbcsKencT) Печатает _123_\. Всё, как мы и ожидали\. Как похорошел язык\!

Ага\. Только работает оно исключительно потому, что нам повезло\. Тут, начиная с C\+\+17, гарантированные NRVO \(_named return value optimization_\) и copy elision\. А программа написана вообще\-то с очень злобной ошибкой\. И если мы возьмём, например, MSVC, который частенько забывает полностью соответствовать последним стандартам, то результат внезапно будет [иной](https://godbolt.org/z/7xonWKWc6)\. А именно — программа ничего не печатает\.

Если мы чуть\-чуть модифицируем программу:

```cpp
int x = 0; std::cin >> x;

const auto text = [x]{
  if (x < 1000) {
    std::string out;
    Writer writer(out);
    writer.Dump(1);
    writer.Dump(2);
    writer.Dump(3);
    return out;
  } else {
    return std::string("hello\n");
  }
}();
std::cout << text;
```

То под Clang все ещё работает, а под GCC — [нет](https://godbolt.org/z/5GWba8)\.

И самое замечательное во всём этом безобразии, что никакое это не неопределённое поведение\!

Помните, мы обсуждали неработающее перемещение? И выясняли, что в C\+\+ нет деструктивного перемещения\. А оно всё\-таки есть\. Иногда, когда одновременно срабатывают оптимизация возвращаемого значения и удаление лишних вызовов конструкторов копий/перемещений\.

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

Но как же тогда оно иногда работает и скрывает такую печальную ошибку? А вот как:

```cpp
const auto text = []{
  std::string out;    
  Writer writer(out);  // (2) Адреса out и text одинаковые. 
                       // По сути, это один и тот же объект 
  writer.Dump(1);
  writer.Dump(2);
  writer.Dump(3);
  return out;    // (1) Это единственная точка возврата
                 // из функции. NRVO позволяет в качестве
                 // адреса временной переменной out
                 // подложить адрес переменной,
                 // в которую мы запишем результат — text.
}(); // (3) Деструктор Writer пишет напрямую в text.
```

Без всех хитроумных оптимизаций происходит следующее:

```cpp
const auto text = []{
  std::string out;     // (0) Строка пуста
  Writer writer(out);  // (1) Адреса out и text разные.
                       // Это разные объекты.
  writer.Dump(1);
  writer.Dump(2);
  writer.Dump(3);      // (2) Записи не происходило,
                       // буфер не заполнился.
  return out;  // (3) Возвращаем копию out — пустую строку.
}(); // (3) Деструктор Writer пишет в out,
     // она умирает и не достаётся никому, text пуст.
```

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

Ну а в каком\-нибудь Rust нам такую ерунду написать [просто не дадут](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c5c9b4edbf891d469214eae29a3ca1af)\. Такие дела\.

Исправляется проблема либо вытаскиванием _Flush_ наружу и его явным вызовом, либо добавлением ещё одной вложенной области видимости:

```cpp
const auto text = []{
  std::string out;
  {
    Writer writer(out);
    writer.Dump(1);
    writer.Dump(2);
    writer.Dump(3);
  } // Деструктор Writer вызывается здесь.
  return out;
}();
std::cout << text;
```

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

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

1. Cppreference\. [Copy elision](https://en.cppreference.com/w/cpp/language/copy_elision)\. 
1. Working Draft Programming Languages — C\+\+\. [Copy/move elision](http://eel.is/c++draft/class.copy.elision)\.

## Исполнение программы: разыменование нулевых указателей

Самая крутая ошибка с самыми жуткими последствиями\. _null_ вообще называют [ошибкой на миллиард долларов](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/)\. От них страдает куча кода на самых разных языках программирования\. Но если в условной _Java_ при обращении по _null_\-ссылке вы получите исключение с вполне предсказуемыми последствиями \(ну упало и упало\), то в великом и ужасном C\+\+, а также в C, за вами придёт неопределённое поведение\. И оно будет действительно неопределённым\!

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

Но для начала, конечно, надо отметить, что — после всех обсуждений туманных формулировок стандарта — **до июля 2024 года** было некоторое [соглашение](https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#315), что всё\-таки не сама по себе конструкция _\*p_, где _p_ — нулевой указатель, вызывает неопределённое поведение, а _lvalue\-to\-rvalue_ преобразование\. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — всё нормально\.

Так, совершенно законно вы могли вызвать статические методы класса через _nullptr_:

```cpp
struct S {
  static void foo() {};
};

S *p = nullptr;
p->foo();
```

А также можно было писать вот такую ерунду:

```cpp
S* p = nullptr;
*p;
```

Причём эту ерунду можно было писать только в C\+\+\. В C это безобразие всё\-таки запретили [\(см\. 6\.5\.3\.2, сноска 104\)](https://web.archive.org/web/20181230041359if_/http:/www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf)\. И в C применять оператор разыменования к невалидным и нулевым указателями нельзя нигде\. А у C\+\+ свой, особый путь\. И эти странные примеры [собирались](https://godbolt.org/z/zPx31e) в _constexpr_ контексте \(напоминаю, в нём запрещено UB, и компилятор проверяет\)\.

Но вот уж совсем недавно было принято решение всё\-таки это безобразие [пресечь](https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#2823)\. И все примеры выше теперь содержат неопределённое поведение\.

Но никто всё ещё не запрещает разыменовывать _nullptr_ в невычисляемом контексте \(внутри _decltype_\):

```cpp
#define LVALUE(T) (*static_cast<T*>(nullptr))

struct S {
  int foo() { return 1; };
};

using val_t = decltype(LVALUE(S).foo());
```

Но несмотря на то, что так делать можно, совершенно не значит, что так делать _нужно_\. Потому что последствия от разыменования _nullptr_ там, где это делать нельзя, могут быть печальными\. Лезвие тонкое, острое, можно легко оступиться и что\-нибудь взорвать\.

Если разыменовать _nullptr_, может быть исполнен код, который никак [не вызывался](https://godbolt.org/z/hPje47):

```cpp
#include <cstdlib>

typedef int (*Function)();

static Function Do = nullptr;

static int EraseAll() {
  return system("rm -rf /");
}

void NeverCalled() {
  Do = EraseAll;  
}

int main() {
  return Do();
}
```

Компилятор обнаруживает разыменование _nullptr_ \(вызов функции _Do_\)\. Это — неопределённое поведение\. Такого быть не может\. Компилятор обнаруживает, что есть одно место, где этому указателю присваивается ненулевое значение\. И, раз нуля быть не может, значит именно это значение он и использует\. Как результат — исполняется код функции, которую мы не вызывали\.

Или вот, совершенно дурная программа:

```cpp
void run(int* ptr) {
  int x = *ptr;
  if (!ptr) {
    printf("Null!\n");
    return;
  }
  *ptr = x;
}

int main() {
  int x = 0;
  scanf("%d", &x);  
  run(x == 0 ? nullptr : &x);
}
```

Из\-за разыменования указателя _ptr_ проверка на _nullptr_ после разыменования [может быть удалена](https://godbolt.org/z/6r17dd8h6)\. Воспроизводится, например, при сборке с помощью GCC 14\.2 \(\-O1 \-std\=c\+\+17\)\. Output: 

```cpp
Null!
```

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

```cpp
void run(int* ptr) {
  try_do_something(ptr); // Если функция разыменует указатель, 
                         // и оптимизатор это увидит, проверка ниже
                         // может быть удалена.
  if (!ptr) {
    printf("Null!\n");
    return;
  }
  *ptr = x;
}
```

Такая ситуация уже куда ближе к реальности\.

В стандартной библиотеке C, например, есть функции, от которых можно было бы, по неопытности, ожидать проверки на _nullptr_, но они этого не делают\.

_strlen_, _strcmp_, другие строковые функции, а в C\+\+ ещё конструктор _std::string\(const char\*\)_ — их вызов с _nullptr_ в качестве аргумента ведёт к неопределённому поведению \(и удалению нижерасположенных проверок, если вам не повезёт\)\.

Ещё есть особо мерзкие в этом смысле _memcpy_ и _memmove_, которые, несмотря на принимаемые в аргументах размеры буферов, всё равно приводят к неопределённому поведению, если передать в них _nullptr_ и нулевой размер\! И точно так же это может проявиться в удалении ваших проверок\.

```cpp
int main(int argc, char **argv) {
  char *string = NULL;
  int length = 0;
  if (argc > 1) {
    string = argv[1];
    length = strlen(string);
    if (length >= LENGTH) exit(1);
  }

  char buffer[LENGTH];
  memcpy(buffer, string, length); // При передаче nullptr
                                  // length будет нулевым,
                                  // но это не спасает от UB.
  buffer[length] = 0;

  if (string == NULL) {
    printf("String is null, so cancel the launch.\n");
  } else {
    printf("String is not null, so launch the missiles!\n");
  }
}
```

На одних и тех же входных данных \(вернее, их отсутствии\) этот код завершается с [разными результатами](https://godbolt.org/z/zc4xGz) в зависимости от компилятора и уровня оптимизаций\.

Если вы недостаточно напуганы, то вот ещё замечательная [история](https://devblogs.microsoft.com/oldnewthing/?p=97635) о том, как весело и задорно падала функция вида:

```cpp
void refresh(int* frameCount)
{
  if (frameCount != nullptr) {
    ++(*frameCount); // Прямо вот тут грохалась из-за
                     // разыменования nullptr.
  }
  ....
}
```

Просто потому, что где\-то совершенно в не связанном с ней классе написали:

```cpp
class refarray {
public:
  refarray(int length)
  {
    m_array = new int*[length];
    for (int i = 0; i < length; i++) {
      m_array[i] = nullptr;
    }
  }

  int& operator[](int i)
  {
    // Разыменование указателя без проверки на null.
    return *m_array[i];
  }
private:
  int** m_array;
};
```

И вызвали функцию так:

```cpp
refresh(&(some_refarray[0]));
```

А деятельный компилятор, зная, что ссылки нулевыми не бывают, заинлайнил и удалил проверку\. Здорово, не правда ли?

Возможно, вы думаете, что ситуации разыменования указателя до проверки — это, скорее, теоретические опасения, чем практическая беда\. Команда PVS\-Studio вынуждена расстроить — это одна из самых частых ошибок\. На момент написания книги команда обнаружила в процессе проверки различных открытых проектов уже 1822 таких случая\. Они бережно собраны в "[коллекции ошибок](https://pvs-studio.ru/ru/blog/examples/v595/)", где с ними можно познакомиться и философски поразмышлять о бытие нулевых указателей\.

Не забывайте проверять на _nullptr_, иначе оно взорвётся\.

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

1. Андрей Карпов\. [Разыменовывание нулевого указателя приводит к неопределённому поведению](https://pvs-studio.ru/ru/blog/posts/cpp/0306/)\.
1. [Как можно и как нельзя использовать нулевой указатель в С\+\+](https://habr.com/ru/articles/513058/)\.
1. Hacker News\. [Memcpy \(and friends\) with NULL pointers](https://news.ycombinator.com/item?id=12002746)\.
1. Андрей Карпов\. [Четыре причины проверять, что вернула функция malloc](https://pvs-studio.ru/ru/blog/posts/cpp/0938/)\.

## Исполнение программы: static initialization order fiasco

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

В C и C\+\+ мы можем разделять код программы по разным, независимым единицам трансляции \(в разные _\.c/\.cpp_ файлы\)\. Они могут компилироваться параллельно\. Скорость сборки повышается\. И всё было бы хорошо\.

Но как только в одном "модуле" появляется глобальная переменная, используемая в другом модуле, начинаются проблемы\. И проблемы не только от того, что глобальные переменные в принципе признак не самого удачного дизайна\. Проблема в том, что связи между модулями нет \(заголовочные файлы ничего не связывают\), и после объединения модулей код с инициализацией глобальной переменной может оказаться **после** кода с использованием\.

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

```cpp
// module.h
extern int global_value;

// module.cpp
#include "module.h"

int init_func() {
  return 5 * 5;
}
int global_value = init_func(); 

// main.cpp
#include "module.h"

#include <iostream>

static int use_global = global_value * 5;

int main() {
  std::cout << use_global;
}
```

Результат будет [зависеть](https://godbolt.org/z/zoxqP9K8x) от того, в каком порядке будут обработаны _main\.cpp_ и _module\.cpp_\.

До C\+\+11 в следующем простеньком примере было неопределённое поведение как раз из\-за возможности неправильного порядка инициализации статических объектов:

```cpp
#include <iostream>

struct Init {
  Init() {
    std::cout << "Init!\n"; 
  }
} init; // До C++11 не было гарантии,
        // что std::cout сконструирован к этому моменту.

int main() {
  return 0;
}
```

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

```cpp
// module.h
int global_variable();

// module.cpp
int global_variable() {
  static int glob_var = init_func();
  return glob_var;
}
```

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

Помимо неопределённого поведения из\-за неправильного порядка инициализации, наиграть можно проблемы и с порядком деинициализации\!

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

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

const std::string& static_name() {
  static const std::string name = "Hello! Hello! long long string!";        
  return name;
}

struct TestStatic {
  TestStatic() {
    std::cout << "ctor: " << "ok" << "\n";
  }
  ~TestStatic() {
    std::cout << "dctor: " << static_name() << "\n";
  }
} test;


int main() {
  std::cout << static_name() << "\n";
}
```

Сначала отрабатывает конструктор _TestStatic_\. Затем _main_, вызвав _static\_name_, конструирует строку\. По завершении программы **сначала** уничтожается строка, а затем деструктор _TestStatic_ обращается к [уже уничтоженной строке](https://godbolt.org/z/Ejbo7TdWc)\.

Чтобы избежать подобного, можно либо в конструкторе _TestStatic_ вызвать функцию _static\_name_ — тогда конструктор строки завершится до завершения конструктора _TestStatic_, и порядок уничтожения объектов будет другим\.

Либо \(и так иногда делают\) в принципе предотвратить уничтожение статической строки: [создать её в куче](https://godbolt.org/z/fPaj9oKeG)\.

```cpp
const std::string& static_name() {
  static const std::string* name 
    = new std::string("Hello! Hello! long long string!");        
  return *name;
}
```

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

#### Initialization order fiasco и неиспользуемые заголовки

Для ускорения процесса сборки хорошей практикой в C\+\+ является уменьшение количества подключаемых заголовков\. Подключать стараются только то, что действительно используется\. Если размер структур в конкретном файле не важен \(например, используются только ссылки и указатели\), то можно подключить отдельный маленький заголовок с предобъявлениями \(например, _iosfwd_ вместо _iostream_\)\. Есть линтеры \([cpplint](https://github.com/cpplint/cpplint), например\), которые могут подсказывать, какие заголовочные файлы у вас совсем не используются\. Все неиспользуемое — в мусор\!

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

На самом деле, есть подводные камни, об которые легко разбиться\. И они связаны с порядком инициализации статических объектов \(спасибо [Egor Suvorov](https://github.com/yeputons) за концепцию примера\)\.

Допустим, вы пишете библиотеку логгирования\. Её интерфейс скромен:

```cpp
// logger.h

#include <string_view>
void log(std::string_view message);
```

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

В первой реализации вы решили логгировать в _stdout_ с помощью стандартной библиотеки потоков ввода/вывода:

```cpp
// logger.cpp
#include "logger.h"

#include <iostream>

void log(std::string_view message) {
  std::cout << "INFO: " << message << std::endl;
}
```

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

```cpp
// main.cpp
#include "logger.h"

struct StaticFactory {
  StaticFactory() {
    log("factory created");
  }
} factory;


int main() {
  log("start main");
  return 0;
}
```

Он, располагая компилятором GCC version 10\.3\.0 \(Ubuntu 10\.3\.0\-1ubuntu1\), собрал приложение командой:

```cpp
g++ -std=c++17 -o test main.cpp logger.cpp
```

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

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

```cpp
g++ -std=c++17 -o test2 logger.cpp main.cpp
```

Запускаете\. И — о, чудо\! — ничего не падает\. Закрываем баг\-репорт?

В этом примере очень злобная ошибка с нарушением порядка инициализации статических объектов\. C\+\+11 гарантирует, что объекты _std::cin_, _std::cout_, _std::cerr_ и их "широкие" аналоги будут инициализированы **до** любого статического объекта, объявленного в вашем файле, **только если** заголовок _<iostream\>_ подключён **перед** объявлением ваших объектов\. Достигается это в глубинах _<iostream\>_ созданием статического объекта _std::ios\_base::Init_\. До C\+\+11 гарантий не было\. Тёмные времена\.

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

Объекты стандартных потоков не единственная возможность для подобных ошибок\. Любая библиотека, использующая глобальные статические объекты, не позаботившаяся об их инициализации **до** любых действий пользователя — потенциальный источник проблем\. Если вы — автор библиотеки, внимательнее относитесь к проектированию её интерфейса\. В C\+\+ он не ограничивается только сигнатурами функций и описанием классов\.

## Исполнение программы: static inline

C\+\+ славен тем, что почти все его конструкции невероятно сильно зависят от контекста, и, просто взглянув на случайный участок кода, крайне сложно быть уверенным в понимании того, что же он делает\. Перегруженные операторы, контекстно\-зависимые значения ключевых слов, ADL, _auto_, _auto_, _auto_\!

Одно из самых перегруженных значениями ключевых слов в C\+\+ — _static_:

* _static_ — это и модификатор видимости, влияющий на линковку;
* _static_ — это и storage\-модификатор, влияющий на то, где и как долго переменная будет храниться;
* _static_ — это ещё и модификатор, влияющий на то, как переменная или метод, ассоциированные с классом или структурой, будут взаимодействовать с объектами этих типов\.

В C\+\+23 [будут](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1169r4.html) ещё и static\-перегрузки для _operator\(\)_\! Это будет что\-то новое, восхитительное и прекрасное\.

Главное, не путать со static\-модификатором при перегрузке других операторов вне класса, ведь это уже модификатор видимости\! И, если написать в разных единицах трансляции что\-нибудь вот такое:

```cpp
/// TU1.cpp
static Monoid operator + (Monoid a, Monoid b) {
  return {
    a.value + b.value
  };
}

Monoid sum(Monoid a, Monoid b) {
  return a + b;
}

/// TU2.cpp
static Monoid operator + (Monoid a, Monoid b) {
  return {
    a.value * b.value
  };
}

Monoid mult(Monoid a, Monoid b) {
  return a + b;
}

/// main.cpp
int main(int argc, char **argv) {
  auto v1 = sum({5}, {6}).value;
  auto v2 = mult({5}, {6}).value;
  std::cout << v1 << " " << v2 << "\n";
}
```

То оно даже будет [работать](https://godbolt.org/z/G5747YGK8) ожидаемым образом, ведь никакой проблемы нет — определения локальны в единицах трансляции\.

В C\+\+17 дополнительными значениями обросло ещё и ключевое слово _inline_\.

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

В современном C\+\+ _inline_ используется чаще всего только для того, чтобы поместить определение функции в заголовочный файл\. В C это тоже работает, но [совсем не так](https://godbolt.org/z/hKWY5eWqG) — вместо ошибки multiple definition, к которой приводит помещение не\-_inline_ функций в заголовочный файл, и которую мы хотели избежать, мы вовсе получили undefined reference\.

В C _inline_\-определения из заголовка нужно сопрячь модификатором _static_\. И, возможно, получить code bloating, потому что получите копию функции в каждой единице трансляции, и все они будут считаться разными, если линковщик окажется недостаточно умным\.

Либо всё\-таки [предоставить](https://godbolt.org/z/a4W8nhTzW) одно не\-_inline_ определение где\-нибудь, например, вот таким мерзким трюком:

```cpp
// square.h
#ifdef DEFINE_STUB
#define INLINE 
#else 
#define INLINE inline
#endif

INLINE int square(int num) {
  return num * num;
}

// square.c
#define DEFINE_STUB 
#include "square.h"

// main.c
#include "square.h"

int main() {
  return square(5);
}
```

Или же [упомянуть](https://godbolt.org/z/c1WTqEE1a) где\-нибудь объявление этой функции со спецификатором_ extern_ \(или даже без него [может работать](https://godbolt.org/z/crKvsEqP8)\):

```cpp
// square.h
inline int square(int num) {
  return num * num;
}

// square.c
#include "square.h"
extern int square(int num);

// main.c
#include "square.h"

int main() {
  return square(5);
}
```

Либо использовать GCC и [собирать](https://godbolt.org/z/EG7Kxbfov) "сишный" код всегда с включёнными оптимизациями\. Только release cборки\! Я таких разработчиков тоже видел\. Но работает это решение [не всегда](https://godbolt.org/z/hc9MrbdY6):

```cpp
// square.h
inline int square(int num) {
  return num * num;
}

inline int cube(int num) {
  return num * num * num;
}

// main.c
#include "square.h"
#include <stdlib.h>

typedef int (*fn) (int);

int main() {
  fn f;
  if (rand() % 2) {
    f = square;
  } else {
    f = cube;
  }
  // Адреса inline-функции неизвестны ->
  //   undefined reference
  return f(5);
}
```

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

К сожалению, в C\+\+ до 17 стандарта просто так взять и поместить в заголовочный файл определение какой\-то константы было не всегда возможно\. А если и возможно, то с интересными [спецэффектами](https://godbolt.org/z/14M7s7KvG)\.

```cpp
// my_class.hpp
struct MyClass {
  static const int max_limit = 5000;
};

// main.cpp
#include "my_class.hpp"

#include <algorithm>

int main() {
  int limit = MyClass::max_limit; // OK
  return std::min(5, MyClass::max_limit); // Compilation error!
    // std::min хочет принять ссылку,
    // но линкер не знает адрес этой константы!
}
```

Можно написать:

```cpp
// my_class.hpp
struct MyClass {
  static constexpr int max_limit = 5000;
};
```

И оно [заработает](https://godbolt.org/z/xcfzzMxzd)\.

Но _constexpr_ возможен не всегда, и тогда всё\-таки придётся взять и отнести определение в отдельную единицу трансляции\.\.\.

Пришёл C\+\+17, и нашим мучениям настал конец\! Теперь можно написать _inline_ у переменной, и компилятор это съест, сгенерирует подобающую аннотацию для символа в объектном файле, чтобы линковщик более не кричал на multiple definition\. Пусть берет любое — мы гарантируем, что все определения одинаковые, а иначе — undefined behavior\.

```cpp
// my_class.hpp
#include <unordered_map>
#include <string>

struct MyClass {
  static const inline
  std::unordered_map<std::string, int> supported_types_versions =
  {
    {"int", 5},
    {"string", 10}
  };
};

inline const
std::unordered_map<std::string, int> another_useful_map = {
  {"int", 5},
  {"string", 6}
};

void test();

// my_class.cpp
#include "my_class.hpp"
#include <iostream>

void test() {
  std::cout << another_useful_map.size() << "\n";
}

// main.cpp
#include "my_class.hpp"
#include <algorithm>
#include <iostream>

int main() {
  std::cout << MyClass::supported_types_versions.size() << "\n";
  test();
}
```

Всё прекрасно [работает](https://godbolt.org/z/WfaoWTbP6) — никаких multiple definitions и никаких undefined references\! Невероятно похорошел C\+\+ при 17\-м стандарте\!

Внимательный читатель уже должен был почувствовать и даже заметить подвох\.

Вот перед вами блок кода:

```cpp
DEFINE_NAMESPACE(details)
{
  class Impl { .... };

  static int process(Impl);

  static inline const
    std::vector<std::string> type_list = { .... };
};
```

Может ли что\-то пойти не так?

Конечно же может\! Это же C\+\+\!

_DEFINE\_NAMESPACE\(name\)_ может быть опредёлен как:

```cpp
#define DEFINE_NAMESPACE(name) namespace name
```

А может быть как:

```cpp
#define DEFINE_NAMESPACE(name) struct name
```

Что?\! Да\! Что, если из благих побуждений, чтобы спрятать доступ к перегрузке функции _process_ от вездесущего ADL, однажды сумрачному гению автора библиотеки пришло в голову именно такое решение, которое включается и выключается всего одним макросом\!

В таких случаях вообще\-то _type\_list_ — это разные вещи\.

В случае _namespace_ — это _static inline_ глобальная переменная\. _inline_ тут как бы бесполезен, потому что _static_ модифицирует видимость глобальной переменной \(linkage\)\. В каждой единице трансляции, где такой заголовок окажется подключённым, будет своя копия переменной _type\_list_\.

В случае же _class_ или _struct_ этот _static inline_ — поле, ассоциированное с классом, и оно будет одно на всех\.

Ну ладно, какая разница\! Они же константы и объявлены одинаково\! Никто ничего не заметит на практике\.\.\. Разумеется\.

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

Вот так [всё работает](https://godbolt.org/z/5M7bYrWMz)\. Красиво и ожидаемо\.

```cpp
// plugin_storage.h
#include <vector>
#include <string>
using PluginName = std::string;
struct PluginStorage {
  static inline std::vector<PluginName> registered_plugins;
};

// plugin.cpp
#include "plugin_storage.h"

namespace {
struct Registrator {
  Registrator() {
    PluginStorage::registered_plugins.push_back("plugin");
  }
} static registrator_;
}

// main.cpp
#include "plugin_storage.h"
#include <iostream>
int main() {
  // Печатает ровно один элемент.
  for (auto&& p : PluginStorage::registered_plugins) {
    std::cout << p << "\n";
  }
}
```

Но меняем _struct_ _PluginStorage_ на _namespace PluginStorage_ — всё компилируется, но [уже не работает](https://godbolt.org/z/Mojn953Yh)\. Переменная _PluginStorage_ своя в каждой единице трансляции, поэтому в _main_ мы видим пустой список\. Нужно удалить _static_ перед _inline_, и мы [получим](https://godbolt.org/z/Wd5PdbfYd) желаемое поведение снова\.

#### Итого

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

* спрятать в функцию, чтобы избежать static initialization order fiasco;
* не написать лишних _static_;
* не запихнуть по неосторожности в заголовочный файл;
* максимально ограничить доступ\.

И ещё не забыть про многопоточный доступ\.

С\+\+17 породил _static inline_ переменные\. Они удобные, но только когда неизменяемые\. Хотя и не беспроблемные\. Средства просмотра изменений на ревью могут не показывать весь файл, а лишь часть с добавлением\. Если видите _static inline_, не забудьте посмотреть, в каком он контексте\. Если это проигнорировать, в лучшем случае ваши исполняемые файлы будут тяжёлыми, в худшем — можно уйти во многие часы безнадёжной отладки после какого\-нибудь минималистичного изменения: кто\-то объявление переменной с глобальным состоянием в заголовок вынес или наоборот внёс, логически же ничего не поменялось\.\.\.

Изменяемые статики — страшное зло\. С ними не только у рядовых разработчиков проблемы\. Например, для Clang более года висел [баг](https://github.com/llvm/llvm-project/issues/55804), связанный с порядком инициализации статиков внутри одной единицы трансляции из\-за неправильной сортировки _static_ глобальных переменных и _static inline_ полей классов\.

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

1. Cppreference\. [static](https://en.cppreference.com/w/cpp/keyword/static)\. 
1. Cppreference\. [inline specifier](https://en.cppreference.com/w/cpp/language/inline)\. 

## Исполнение программы: нарушение ODR \(One definition rule\)

Вызвать функцию, которая не должна вызываться, испортить стек, сломать проверенную временем стороннюю библиотеку, довести до безумия программиста, пытающегося найти проблему под отладчиком — всё может ODR violation\!

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

В некоторых языках неопределённости нет\. Например, в Python каждое следующее определение перекрывает предыдущее:

```cpp
# hello.py
def hello():
    print("hello world")
    
hello() # hello world 

def hello():
    print("Hello ODR!")
    
hello() # Hello ODR!
```

В иных языках множественные определения просто приводят к [ошибке компиляции](https://rextester.com/KICEDE17400)\.

```cpp
fun x y = x + y

gun x y = x - y

fun x y = x * y

main = print $ "Hello, world!" ++ (show $ fun 5 6)

--    Multiple declarations of 'fun'
--    Declared at: 1124215805/source.hs:3:1
--                 1124215805/source.hs:7:1
```

С и С\+\+ не исключения — в них переопределения функций, классов, шаблонов тоже диагностируются и выливаются в [ошибку компиляции](https://godbolt.org/z/KzxrY3qr9)\.

```cpp
int fun() {
  return 5;
}

int fun() { // CE: redefinition
  return 6;
}
```

И вроде бы всё хорошо\. Ожидаемое, отличное решение\. Но есть нюансы\.

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

Знакомые с Python, наверное, знают, что в нём каждый отдельный файл — модуль — отдельное пространство имён\. Имена классов и функции из разных файлов никак не интерферируют, до тех пор, пока не будут импортированы\.

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

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

Наибольшую неприятность доставляет то, что проблема не ограничивается сборкой лишь вашего кода\. Ведь вы можете случайно использовать какое\-то имя, встречающееся в сторонней библиотеке\! И тогда можно сломать эту библиотеку как в своём проекте, так и в чужом, если ваш код будет использоваться в качестве зависимости\. Причём достаточно случайно угадать лишь имя функции: в С нет перегрузок функции и определение функции с тем же именем, но с другими аргументами — ODR violation\.

Из\-за всех этих проблем в стандартах C и С\+\+ даже указаны [ограничения](https://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier/228797#228797) на имена, которые вы можете использовать в своём коде, чтобы случайно не сломать стандартную библиотеку\!

Что же делать?

В мире чистого C с этим борются комплексом методов:

1\. Ручной имплементацией механизма пространств имён\. Каждой функции и структуре в проекте дописывают префиксом имя проекта\.

2\. Настраивают видимость символов:

* _static_ делает функцию или глобальную переменную "невидимой" за пределами единицы трансляции;
* _\_\_attribute\_\_\(\(visibility\("hidden"\)\)\)_ для приватных структур и функций;
* флаги _\-fvisibility\=hidden_, _\-fvisibility\-inlines\-hidden_ и выставление атрибутов только для публичного интерфейса\.

3\. Пишут [скрипты](https://sourceware.org/binutils/docs/ld/Scripts.html) для линкера, если предыдущий пункт пропустил в итоговый бинарь что\-то лишнее\.

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

В C\+\+ ситуация немного лучше\.

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

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

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

```cpp
// A.cpp

namespace {
  struct S {
    S() {
      std::cout << "Hello A!\n";
    }
  };
}

void fun_A() {
  S{};
}

// B.cpp

namespace {
  struct S {
    S() {
      std::cout << "Hello B!\n";
    }
  };
}

void fun_B() {
  S{};
}
```

Структуры _S_ находятся в разных анонимных пространствах имён, проблем с нарушением ODR [не возникает](https://wandbox.org/permlink/yxZCpLZ46dWt6EnW)\.

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

Наконец, в C\+\+, начиная с 20 стандарта, появились модули\. Приватные, явно неэкспортируемые имена внутри одного модуля не интерферируют с именами из других модулей\. Но для экспортируемых имён все проблемы сохраняются: объявлять пространство имён и следить за пересечениями надо самостоятельно\.

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

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

В C\+\+ мы можем определять функции, принадлежащие к какому угодно пространству имён, в любой единице трансляции\. А шаблоны компилируются в два прохода с привлечением ADL \(argument dependent lookup\)\. И горе вам, если один из проходов вытянет разные функции\!

```cpp
struct A {};
struct B{};
struct D : B {};

// demo_1.cpp
bool operator<(A, B) { std::cout << "demo_1\n"; return true; }
void demo_1() { 
  A a; D d;
  std::less<void> comparator; 
  comparator(a, d); // Шаблонный оператор ()
                    // ищет подходящее определение для <.
}

// demo_2.cpp
bool operator<(A, D) { std::cout << "demo_2\n"; return true; }
void demo_2() {
  A a; D d;
  std::less<void> comparator;
  comparator(a, d);
}

int main() {
  demo_1();
  demo_2();
  return 0;
}
```

В этом примере \(спасибо [LDVSOFT](https://github.com/LDVSOFT)\) разный порядок компиляции даёт разные результаты:

* либо [оба вызова печатают demo\_1](https://wandbox.org/permlink/UHGmw1Q1Y1rhpe0u);
* либо [оба вызова печатают demo\_2](https://wandbox.org/permlink/cSfcUuyIDfnAvf3d);
* возможно даже поиграть с компилятором так, чтобы работало как задумывалось\. Это остаётся читателю в качестве домашнего задания\.

Занятно, что из\-за специфики и трудности реализации двухэтапной компиляции шаблонов разные компиляторы будут давать разный результат, если поместить этот пример в одну единицу трансляции\! И о проблеме никто не сообщит\!

Для упрощения анализа печать строк [заменена](https://godbolt.org/z/hPPjjbThq) на печать чисел 1 и 2\.

GCC:

```cpp
demo_1():
  mov   esi, 1
  mov   edi, OFFSET FLAT:_ZSt4cout
  jmp   std::basic_ostream<char, std::char_traits<char> >::operator<<(int)

demo_2():
  mov   esi, 1
  mov   edi, OFFSET FLAT:_ZSt4cout
  jmp   std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
```

MSVC:

```cpp
void demo_1(void) PROC                           ; demo_1, COMDAT
  push   2
  mov    ecx,
         OFFSET
         std::basic_ostream<char,std::char_traits<char> > std::cout
           ; std::cout
  call   std::basic_ostream<char,std::char_traits<char> > &
         std::basic_ostream<char,std::char_traits<char> >::operator<<(int)
           ; std::basic_ostream<char,std::char_traits<char> >::operator<<
  ret    0
void demo_1(void) ENDP  

void demo_2(void) PROC                           ; demo_2, COMDAT
  push   2
  mov    ecx,
         OFFSET
         std::basic_ostream<char,std::char_traits<char> > std::cout 
           ; std::cout
  call   std::basic_ostream<char,std::char_traits<char> > &
         std::basic_ostream<char,std::char_traits<char> >::operator<<(int)
           ; std::basic_ostream<char,std::char_traits<char> >::operator<<
  ret    0
```

Код, собранный GCC, печатает 11, MSVC — 22\.

Страшно? Не бойтесь\! Если в этом примере _operator <_ действительно предполагался приватным, то заворачивание его в анонимное пространство имён решило бы проблему\. Внутри _std::less<void\>::operator\(\)_ оператор _<_ не был бы найден, и вы бы получили ошибку компиляции \([она бы вам не понравилась](https://godbolt.org/z/Pz4Ks5eTx)\)\. Пришлось бы использовать сравнение [явно](https://godbolt.org/z/4bnTMod98), и тут уже всё определено\.

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

ODR violation почти всегда ходит вместе с проблемами обновлений и слома ABI\.

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

Слом ABI, потенциальное нарушение ODR — одни из самых острых причин, почему миграция на новые версии стандарта, компиляторов и библиотек в C\+\+ мире занимает многие годы\. Нужно всё пересобрать, всё перетестить, убедиться, что никто не привнёс неправильных имён\.

Как это ни парадоксально, но возможность нарушить ODR иногда оказывается полезной\. Связанное с ним неопределённое поведение является в каком\-то смысле определённым и контролируемым: какое из определений будет использоваться, задаётся порядком, на который можно влиять\. GCC, например, поддерживает _\_\_attribute\_\_\(\(weak\)\)_ для пометки функций, которые ожидаемо будут замещаться альтернативными определениями \(с более эффективной реализацией, без отладочных инструкций, например\)\. Или же техника symbol hooking, использующая LD\_PRELOAD, чтобы заменить определённые функции из динамических библиотек для отладки с инструментированным аллокатором или же для перехвата вызовов и сбора статистики\.

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

1. Cppreference\. [Definitions and ODR \(One Definition Rule\)](https://en.cppreference.com/w/cpp/language/definition)\.
1. Wikipedia\. [Weak symbol](https://en.wikipedia.org/wiki/Weak_symbol)\. 
1. [Hooking on Linux with LD\_PRELOAD](https://liveoverflow.com/hooking-on-linux-with-ld_preload-pwn-adventure-3/)\.
1. GCC Wiki\. [Visibility](https://gcc.gnu.org/wiki/Visibility)\. 

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

Эта тема тесно связана с ODR violation\.

В C и C\+\+ невероятно много идентификаторов, использовать которые для своих переменных и типов запрещено под страхом неопределённого поведения\.

Некоторые имена запрещены самими стандартами C и C\+\+, некоторые — стандартами POSIX, некоторые — платформоспецифическими библиотеками\. В последнем случае вам обычно ничего не грозит, пока библиотека не подключена\.

Так, в глобальной области видимости нельзя использовать имена функций из библиотеки C\. Ни в C, ни в C\+\+\! Иначе вы можете столкнуться не только с ODR violation, но ещё и с удивительным поведением компиляторов, умеющих оптимизировать распространённые конструкции\.

Так, если определить свой собственный _memset_:

```cpp
void *memset (void *destination, int c, unsigned long n) {
  for (unsigned long i = 0; i < n; ++i) {
    ((char*)(destination))[i] = c;
  }
  return destination;
}
```

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

```cpp
void *memset (void* destination, int c, unsigned long n) {
  return memset(destination, c, n);
}
```

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

Однако декорирование не спасает, если объявлять не функции, а глобальные переменные:

```cpp
#include <iostream>
int read;
int main(){
  std::ios_base::sync_with_stdio(false);
  std::cin >> read;
}
```

При сборке такого примера со статически влинкованной стандартной библиотекой C программа [упадёт](https://godbolt.org/z/8Gsxoohcv) \(SIGSEGV\), так как вместо адреса стандартной функции _read_ будет подставлен адрес глобальной переменной _read_\. Аналогичный пример с использованием имени _write_ предлагается читателю воплотить самостоятельно в качестве упражнения\.

Запретных имён много\. Например, всё, что начинается с _is\*_, _to\*_ или _\_\*_ запрещено в глобальном пространстве\. _\_\[A\-Z\]\*_ запрещены вообще везде\. POSIX резервирует имена, заканчивающиеся на _\_t_\. И ещё много всего неожиданного\. 

<details>
   <summary>Кстати, эти пространства имён ещё нельзя и расширять собственными сущностями\\\.</summary>

Вы можете расширить пространства имён _std_ или _POSIX_\.  Несмотря на то, что такая программа успешно компилируется и исполняется, модификация этих пространств имён может привести к неопределённому поведению программы, если иное не указано стандартом\.

Содержимое пространства имён _std_ определяется исключительно комитетом стандартизации, и стандарт запрещает добавлять в него:

* декларации переменных;
* декларации функций;
* декларации классов/структур/объединений;
* декларации перечислений;
* декларации шаблонов функций, классов и переменных \(C\+\+14\)\.

Стандарт разрешает добавлять следующие специализации шаблонов, определённых в пространстве имён _std_, если они зависят хотя бы от одного определённого в программе типа \(program\-defined type\):

* полная или частичная специализация шаблона класса;
* полная специализация шаблона функции \(до C\+\+20\);
* полная или частичная специализация шаблона переменной, не лежащей в заголовочном файле '<type\_traits\>' \(до C\+\+20\)\.

Однако специализации шаблонов, лежащих внутри классов или шаблонов классов, запрещены\.

В отличие от пространства имён _std_, какая\-либо модификация пространства имён _POSIX_ полностью запрещена\.


</details>
Если вы пользуетесь запрещёнными именами, то сегодня может всё работать, но не завтра\.

Чтобы не жить в страхе, во многих случаях достаточно использовать _static_ или анонимные пространства имён\. Или просто не использовать C и C\+\+\.

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

1. The GNU C Library \(glibc\) manual\. [Reserved Names](https://www.gnu.org/software/libc/manual/html_node/Reserved-Names.html)\.
1. Stack Overflow\. [What are the rules about using an underscore in a C\+\+ identifier?](https://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier)
1. SEI CERT C\+\+ Coding Standard\. [DCL51\-CPP\. Do not declare or define a reserved identifier](https://wiki.sei.cmu.edu/confluence/display/cplusplus/DCL51-CPP.+Do+not+declare+or+define+a+reserved+identifier)\.
1. Документация PVS\-Studio\. Диагностика V1061\. [Extending 'std' or 'posix' namespace may result in undefined behavior](https://pvs-studio.ru/ru/docs/warnings/v1061/)\.

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

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