﻿# Дизайн и эволюция constexpr в C\+\+

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


> Мы опубликовали и перевели эту статью с разрешения правообладателя\\\. Автор статьи – Евгений Шульгин, email \\\- \[izaronplatz@gmail\\\.com\]\(mailto:izaronplatz@gmail\.com\)\\\. \[Оригинал\]\(https://habr\.com/ru/post/579490/\) опубликован на сайте Habr\\\. Также приглашаем познакомиться с другими теоретическими статьями с тегом \[\\\#Knowledge\]\(https://pvs\-studio\.ru/ru/blog/posts/?tag\=Knowledge\)\\\.

У _constexpr_ с каждым годом становится больше возможностей\. Сейчас использовать в compile\-time вычислениях можно почти всю стандартную библиотеку\. Пример вычисления числа до 1000 с наиболшим количеством делителей: [ссылка на код](https://godbolt.org/z/MYTbbsqvT)\.

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

Эта статья подходит как тем, кто еще не знает, что такое _constexpr_, так и тем, кто уже долгое время его использует\.

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

## C\+\+98 и C\+\+03: Сословия среди const\-переменных

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

```cpp
enum EPlants
{
  APRICOT = 1 << 0,
  LIME = 1 << 1,
  PAPAYA = 1 << 2,
  TOMATO = 1 << 3,
  PEPPER = 1 << 4,
  FRUIT = APRICOT | LIME | PAPAYA,
  VEGETABLE = TOMATO | PEPPER,
};

template<int V> int foo();
int foo6 = foo<1+2+3>();
int foo110 = foo<(1 < 2) ? 10*11 : VEGETABLE>();

int v;
switch (v)
{
case 1 + 4 + 7:
case 1 << (5 | sizeof(int)):
case (12 & 15) + PEPPER:
    break;
}
```

Эти выражения описаны в разделе **\[expr\.const\]** и называются _constant\-expression_\. Они могут содержать только:

* [Литералы](https://eel.is/c++draft/lex.literal) \(туда входят целые числа, это интегральные типы\);
* Значения _enum_\-ов;
* Параметр шаблона интегрального или _enum_\-типа \(напр\., значение _V_ из _template <int V\>_\);
* _sizeof_\-выражение;
* _const_\-переменные, инициализированные _constant\-expression_ \- **интересный пункт**\.

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

Если у переменной static storage duration, то в обычной ситуации память под нее заполняется нулями и изменяется во время работы программы\. Но для переменных из списка выше это _слишком поздно_ \- вычислять их значения нужно еще до окончания процесса компиляции\.

В стандартах C\+\+98/03 есть два вида _static initialization_:

1. _zero\-initialization_, память заполняется нулями, затем в ходе программы изменяется;
1. _initialization with a constant expression_, память \(если нужна\) сразу содержит вычисленное значение\.

**Note\.** Все остальные инициализации называются _dynamic initialization_, мы их не рассматриваем\.

**Note\.** _zero\-initialized_ переменная во время работы программы в свою очередь может быть проинициализирована еще раз "нормальным" значением, это уже будет _dynamic initialization_ \(пусть даже до старта _main_\)\.

Рассмотрим пример с обоими видами переменных:

```cpp
int foo()
{
  return 13;
}

const int test1 = 1 + 2 + 3 + 4;  // initialization with a const. expr.
const int test2 = 15 * test1 + 8; // initialization with a const. expr.
const int test3 = foo() + 5;      // zero-initialization
const int test4 = (1 < 2) ? 10 * test3 : 12345; // zero-initialization
const int test5 = (1 > 2) ? 10 * test3 : 12345; // initialization with
                                                // a const. expr.
```

Переменные _test1_, _test2_, _test5_ можно использовать как параметр шаблона, значение справа от case в switch и т\.д\., а _test3_ и _test4_ \- нельзя\.

Как видно из требований к _constant\-expression_ и из примера, есть транзитивность \- если какая\-то составная часть выражения не является _constant\-expression_, то и само выражение не является _constant\-expression_\. При этом рассматриваются только те части выражения, которые реально вычисляются \- поэтому _test4_ и _test5_ попадают в разные группы\.

Если нигде не берется адрес _constant\-expression_ переменной, то скомпилированной программе разрешено не резервировать для нее память, поэтому "заставим" ее это сделать\. Выведем значения переменных и их адреса:

```cpp
int main()
{
  std::cout << test1 << std::endl;
  std::cout << test2 << std::endl;
  std::cout << test3 << std::endl;
  std::cout << test4 << std::endl;
  std::cout << test5 << std::endl;

  std::cout << &test1 << std::endl;
  std::cout << &test2 << std::endl;
  std::cout << &test3 << std::endl;
  std::cout << &test4 << std::endl;
  std::cout << &test5 << std::endl;
}

izaron@izaron:~/cpp$ clang++ --std=c++98 a.cpp 
izaron@izaron:~/cpp$ ./a.out 
10
158
18
180
12345
0x402004
0x402008
0x404198
0x40419c
0x40200c
```

Скомпилируем объектный файл и посмотрим на таблицу символов:

```cpp
izaron@izaron:~/cpp$ clang++ --std=c++98 a.cpp -c
izaron@izaron:~/cpp$ objdump -t -C a.o

a.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 a.cpp
0000000000000080 l     F .text.startup  0000000000000015 _GLOBAL__sub_I_a.cpp
0000000000000000 l     O .rodata        0000000000000004 test1
0000000000000004 l     O .rodata        0000000000000004 test2
0000000000000004 l     O .bss   0000000000000004 test3
0000000000000008 l     O .bss   0000000000000004 test4
0000000000000008 l     O .rodata        0000000000000004 test5
```

В конкретной версии компилятора под конкретную архитектуру, в конкретной программе zero\-initialized переменные попали в секцию [_\.bss_](https://en.wikipedia.org/wiki/.bss), остальные в секцию _\.rodata_\.

Загрузчик операционной системы перед запуском загружает программу так, что секция _\.rodata_ оказывается в сегменте с read\-only режимом, который защищен от записи на уровне ОС\.

Попробуем через _const\_cast_ изменить данные по адресу переменных\. Стандарт дает уклончивый ответ на тему того, когда запись в результат _const\_cast_\-а может спровоцировать undefined behaviour\. По крайней мере, этого не происходит, если мы убираем const с объекта/указателя на объект, который не является фундаментально константным изначально\. Т\.е\. надо различать _физическую_ константность и _логическую_\.

UB\-санитайзер поймает UB \(программа крашится\), если попытаться изменить _\.rodata_\-переменную, и его не будет, если записывать в _\.bss_ или автоматические переменные\.

```cpp
const int &ref = testX;
const_cast<int&>(ref) = 13; // OK for test3, test4;
                            // SEGV for test1, test2, test5
std::cout << ref << std::endl;
```

Таким образом, одни const\-переменные "константнее" других\. Насколько известно, в то время **не было** простого способа проверить или как\-то проконтролировать, что переменная была _initialized with a const\. expr_\.

## 0\-∞: Вычислитель констант в компиляторе

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

Компиляторы идейно похожи друг на друга, я опишу процесс вычисления на основе компилятора Clang/LLVM\. Я скопировал базовую информацию про этот компилятор из своей [прошлой статьи](https://habr.com/ru/post/576052/):

**\[НАЧАЛО БЛОКА SPOILER\]**

### Clang and LLVM

Про само устройство Clang и LLVM написано уже много статей\. На хабре я бы посоветовал [эту статью](https://habr.com/ru/company/huawei/blog/511854/), чтобы понять их краткую историю и общую схему\.

Количество стадий компиляций зависит от того, кто объясняет устройство компилятора\. Анатомия компилятора многоуровнева, и на самом абстрактном уровне выглядит так, что есть три разные программы:

* **Front\-end:** переводит исходник из C/C\+\+/Ada/Rust/Haskell/\.\.\. в [LLVM IR](https://llvm.org/docs/LangRef.html) \- особое промежуточное представление\. Фронтендом для C\-like языков является Clang\.
* **Middle\-end:** LLVM IR оптимизируется в зависимости от настроек\.
* **Back\-end**: LLVM IR переводится в машинный код под нужную платформу \- x86/Arm/PowerPC/\.\.\.

Для простых языков реально написать компилятор под [1000 строк](https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html) и получить всю мощь фреймворка LLVM \- для этого нужно реализовать фронтенд\. Также можно использовать lex/yacc \- готовые синтаксические парсеры\.

На менее абстрактном уровне находится фронтенд Clang, который выполняет такие действия \(не рассматривая препроцессор и прочие "микро"\-шаги\):

* [Лексический анализ](https://ru.wikipedia.org/wiki/%D0%9B%D0%B5%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7): перевод символов в токены, например _\[\]\(\) \{ return 13 \+ 37; \}_ преобразуются в _\(l\_square\) \(r\_square\) \(l\_paren\) \(r\_paren\) \(l\_brace\) \(return\) \(numeric\_constant:13\) \(plus\) \(numeric\_constant:37\) \(semi\) \(r\_brace\)_\.
* [Синтаксический анализ](https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7): создание AST \(Abstract Syntax Tree\), то есть перевод токенов из предыдущего пункта в вид _\(lambda\-expr \(body \(return\-expr \(plus\-expr \(number 13\) \(number 37\)\)\)\)\)_\.
* Кодогенерация: создание LLVM IR по данному AST\.

**\[КОНЕЦ БЛОКА SPOILER\]**

Итак, вычисление константных выражений \(и близкородственных ему вещей, как инстанцирование шаблонов\) происходит строго в фронтенде компилятора C\+\+ \(в нашем случае в Clang\), а LLVM такими вещами не занимается\.

"Микросервис", который занимается вычислением константных выражений \(от самых простых в C\+\+98 до сложных в C\+\+23\), назовем условно **вычислитель констант**\.

Если согласно стандарту в каком\-то месте кода ожидается константное выражение; и выражение, которое там находится, действительно удовлетворяет требованиям на константное выражение, то Clang должен "не отходя от кассы" в 100% случаев уметь вычислять его\.

Ограничения на константные выражения на протяжении лет постоянно смягчались, а вычислитель констант Clang соответственно постоянно усложнялся, вплоть до управления моделью памяти\.

Есть [старая документация](https://clang.llvm.org/docs/InternalsManual.html) 9\-летней давности, описывающая вычисление констант для C\+\+98/03\. Так как константные выражения тогда были очень простыми, они выполнялись с помощью обычного [constant folding](https://en.wikipedia.org/wiki/Constant_folding) через анализ синтаксического дерева \(AST\)\. Так как в синтаксическом дереве все арифметические выражения уже разобраны в виде под\-деревьев, то вычисление константы \- просто элементарный обход под\-дерева\.

Исходник вычислителя констант находится в [lib/AST/ExprConstant\.cpp](https://clang.llvm.org/doxygen/ExprConstant_8cpp_source.html) и на момент написания статьи разросся до почти 16 тысяч строк\. С годами он научился интерпретировать много всего, например циклы \([EvaluateLoopBody](https://clang.llvm.org/doxygen/ExprConstant_8cpp.html)\), и всё это на синтаксическом дереве\.

У константных выражений есть важное отличие от кода, выполняющегося в рантайме \- они _обязаны_ не допускать undefined behaviour\. Если вычислитель констант наткнется на UB, компиляция провалится\.

```cpp
c.cpp:15:19: error: constexpr variable 'foo' must be initialized by a
                    constant expression
    constexpr int foo = 13 + 2147483647;
                  ^     ~~~~~~~~~~~~~~~
```

Вычислитель констант используется не только для константных выражений, но также для поиска потенциальных багов в остальном коде\. Это побочная выгода от технологии\. Вот так находится переполнение для не\-константного кода \(могут максимум бросить warning\):

```cpp
c.cpp:15:18: warning: overflow in expression; result is -2147483636
                      with type 'int' [-Winteger-overflow]
    int foo = 13 + 2147483647;
                 ^
```

## 2003: Макросы не нужны

Изменения в стандарт происходят через _предложения_\.

**\[НАЧАЛО БЛОКА SPOILER\]**

### Где находятся предложения и из чего они состоят?

Все предложения в стандарт находятся на [open\-std\.org](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/)\. Практически все написаны понятно \- чаще всего есть:

* Краткий обзор области со ссылками на разделы стандарта;
* Текущие проблемы;
* Предлагаемое решение проблем;
* Предлагаемые изменения в текст стандарта;
* Ссылки на предложения\-предшественники и прошлые ревизии предложения;
* В особо продвинутых предложениях \- ссылки на реализацию в форке компилятора\. В тех, что я видел, реализацию делали в форке clang\-a\.

По ссылкам на предыдущие предложения можно отследить эволюцию для каждого куска C\+\+\.

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

Принять участие в эволюции C\+\+ может любой \- для русскоязычных экспертов есть [stdcpp\.ru](https://stdcpp.ru/)\.

**\[КОНЕЦ БЛОКА SPOILER\]**

Предложение от 2003 года [\[N1521\] Generalized Constant Expressions](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1521.pdf) указывает на проблему того, что если часть выражения вычисляется с использованием вызова метода, то выражение не является _constant\-expression_\. Это заставляет злоупотреблять макросами, если нужно получить более\-менее сложное константное выражение:

```cpp
#define SQUARE(X) ((X) * (X))
inline int square(int x) { return x * x; }
// ^^^ определение макроса и метода
square(9)
std::numeric_limits<int>::max()
// ^^^ невозможны в составе constant-expression
SQUARE(9)
INT_MAX
// ^^^ теоретически могут быть в составе constant expression
```

Поэтому предлагается ввести понятие _constant\-valued_ методов, которых будет разрешено использовать в _constant\-expression_\. Метод считается _constant\-valued_, если это _inline_\-метод, нерекурсивный, возвращающий не _void_, и его тело состоит из единственного выражения вида _return expr;_, где после подстановки аргументов \(куда также идут _constant\-expression_\) получился бы _constant\-expression_\.

**Note\.** Забегая вперед, термин _constant\-valued_ не прижился\.

```cpp
int square(int x) { return x * x; }         // constant-valued
long long_max(int x) { return 2147483647; } // constant-valued
int abs(int x) { return x < 0 ? -x : x; }   // constant-valued
int next(int x) { return ++x; }             // NOT constant-valued
```

Таким образом, все переменные _test1\-5_ из прошлого раздела становились бы "фундаментально" константными, без изменения в коде\.

Предложение считает, что можно пойти еще дальше и надо рассмотреть вариант, что такой код тоже должен компилироваться:

```cpp
struct cayley
{
  const int value;
  cayley(int a, int b)
    : value(square(a) + square(b)) {}
  operator int() const { return value; }
};

std::bitset<cayley(98, -23)> s; // eq. to bitset<10133>
```

Потому что переменная _value_ "фундаментально константная" \- инициализировалась в конструкторе через _constant\-expression_ с двумя вызовами _constant\-valued_ метода\. То есть, согласно общей логике предложения, этот код можно свести примерно к такому \(вынос переменных и методов вне структуры\):

```cpp
// имитация вызова конструктора cayley::cayley(98, -23) и operator int()
const int cayley_98_m23_value = square(98) + square(-23);

int cayley_98_m23_operator_int()
{
  return cayley_98_m23_value;
}

// создание битсета
std::bitset<cayley_98_m23_operator_int()> s; // eq. to bitset<10133>
```

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

**Note\.** Однако предложения не существуют в отрыве от компиляторов \- если предложение нереально реализовать в разумный срок, его вряд ли примут\.

Как с переменными, программист не может проконтролировать, что метод является _constant\-valued_\.

## 2006\-2007: Тайное становится явным

К счастью, через 3 года в следующих ревизиях этого предложения \([\[N2235\]](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2235.pdf)\) признали, что слишком много неявности это плохо\. В список проблем добавили невозможность контроля за инициализацией:

```cpp
struct S
{
  static const int size;
};

const int limit = 2 * S::size; // dynamic initialization
const int S::size = 256; // constant expression initialization
const int z = std::numeric_limits<int>::max(); // dynamic initialization
```

По задумке программиста, _limit_ должен был быть инициализирован constant\-expression, но этого не происходит, так как _S::size_ определён "слишком поздно", после _limit_\. Если бы была возможность запросить нужный тип инициализации, компилятор выдал бы ошибку\.

Аналогично с методами\. _Constant\-valued_ методы переименовали в _constant\-expression_ методы\. Требования к ним остались те же, но теперь, чтобы их было возможно использовать в _constant\-expression_, их необходимо объявлять с ключевым словом _constexpr_\. Зато компиляция будет падать, если тело метода не является правильным _return expr;_\.

Также компиляция упадет, если _constexpr_\-метод принципиально не сможет быть использован в constant\-expression при любых аргументах, с ошибкой _constexpr function never produces a constant expression_\. Это надо для того, чтобы программист был точно уверен в том, что метод является потенциально используемым в _constant\-expression_\.

Некоторые методы стандартной библиотеки \(напр\. из _std::numeric\_limits_\) предлагается пометить _constexpr_, так как они удовлетворяют требованиям на него\.

Переменные или члены класса также можно объявлять _constexpr_, тогда компиляция упадет, если переменная инициализируется не через _constant\-expression_\.

На тот момент решили сохранить совместимость нового слова с переменными, неявно инициализированными через _constant\-expression_ без ключевого слова _constexpr_, то есть этот код работал \(забегая вперед, сейчас этот код _с \-\-std\=c\+\+11_ не компилируется, возможно этот код так никогда и не начал работать\):

```cpp
const double mass = 9.8;
constexpr double energy = mass * square(56.6); // OK, хотя mass не объявлен с
                                               // constexpr
extern const int side;
constexpr int area = square(side); // error: square(side) is not
                                   // a constant expression
```

_constant\-expression_\-конструкторы для пользовательских классов также легализовали\. Этот конструктор должен иметь пустое тело и инициализировать члены _constexpr\-expression_\-ами, если пользователь создает _constexpr_\-объект данного класса\.

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

Пример класса с _constexpr_\-ами из предложения:

```cpp
struct complex
{
  constexpr complex(double r, double i) : re(r), im(i) { }

  constexpr double real() { return re; }
  constexpr double imag() { return im; }

private:
  double re;
  double im;
};

constexpr complex I(0, 1); // OK -- literal complex
```

Такие объекты, как _I_, в предложении назвали _user\-defined literals_\. "Литерал" \- это нечто вроде базовой сущности в C\+\+\. Так же, как "простые" литералы \(числа, символы и т\.д\.\) сразу подставляются в ассемблерные команды, а строковые литералы лежат в секции подобной _\.rodata_, так и пользовательские литералы занимают где\-нибудь там своё место\.

Теперь _constexpr_\-переменными могли являться не только числа и перечисления, а [литеральные типы](https://en.cppreference.com/w/cpp/named_req/LiteralType), которых ввели в этом предложении \(пока еще без _reference type_\)\. Литеральный тип \- такой, который может быть передан в _constexpr_\-метод, и/или изменен и/или возвращен из нее\. Это достаточно простые типы, чтобы компиляторы могли его поддерживать в вычислителе констант\.

Ключевое слово _constexpr_ стало спецификатором, нужным компилятору \- примерно как _override_ в классах\. После обсуждения предложения, для этого ключевого слова решили не делать новый [storage class](https://en.cppreference.com/w/cpp/language/storage_duration) \(хотя было бы законно\), новый [type qualifier](https://en.cppreference.com/w/cpp/language/cv), и также его решили не разрешать использовать для аргументов методов, чтобы не переусложнять правила перегрузки методов\.

## 2007: Первые constexpr для структур данных

В этом году вышло предложение [\[N2349\] Constant Expressions in the Standard Library](http://open-std.org/JTC1/SC22/WG21/docs/papers/2007/n2349.pdf), где пометили как _constexpr_ некоторые методы и константы, а также некоторые методы контейнеров, например:

```cpp
template<size_t N>
class bitset
{
  // ...
  constexpr bitset();
  constexpr bitset(unsigned long);
  // ...
  constexpr size_t size();
  // ...
  constexpr bool operator[](size_t) const;
};
```

Конструкторы инициализируют члены класса через _constant\-expression_, остальные методы внутри себя имеют _return expr;_, подходящий под текущие ограничения\.

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

## 2008: Рекурсивные constexpr\-методы

_constexpr_\-методы изначально не предполагалось разрешать делать рекурсивными по причине отсутствия убедительных доводов в наличие рекурсии, но затем это ограничение убрали, что отметили в [\[N2826\] Issues with Constexpr](http://open-std.org/JTC1/SC22/WG21/docs/papers/2009/n2826.html)\.

```cpp
constexpr unsigned int factorial( unsigned int n )
{
  return n==0 ? 1 : n * factorial( n-1 );
}
```

У компилятора существует некий предел вложенности вызовов \(в clang это 512 вложенных вызовов\), при превышении он откажется считать выражение\.

Подобные пределы также есть, например, для инстанцирования шаблонов \(если мы бы считали в compile\-time через шаблоны, а не через _constexpr_\-методы\)\.

## 2010: "const T&" как аргументы в constexpr\-методах

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

```cpp
template< class T >
constexpr const T& max( const T& a, const T& b ); // не скомпилируется

constexpr pair(); // можно поставить constexpr
pair(const T1& x, const T2& y); // нельзя поставить constexpr
```

Предложение [\[N3039\] Constexpr functions with const reference parameters \(a summary\)](http://open-std.org/JTC1/SC22/WG21/docs/papers/2010/n3039.pdf) разрешает константные ссылки в аргументах и как возвращаемое значение\.

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

В общем случае работа со ссылками и указателями в _constant\-expression_ превращает C\+\+\-компилятор в C\+\+\-интерпретатор, поэтому накладываются различные ограничения\.

Если вычислитель констант может обработать метод с аргументом типа _T_, то типа const _T&_ тоже сможет \- если будет "представлять себе", что для этого аргумента создается "временный объект"\.

Более\-менее сложная работа со ссылками и попытки что\-либо поломать не скомпилируются\.

```cpp
template<typename T> constexpr T self(const T& a) { return *(&a); }
template<typename T> constexpr const T* self_ptr(const T& a) { return &a; }

template<typename T> constexpr const T& self_ref(const T& a)
{
  return *(&a);
}

template<typename T> constexpr const T& near_ref(const T& a)
{
  return *(&a + 1);
}

constexpr auto test1 = self(123);     // OK
constexpr auto test2 = self_ptr(123); // FAIL, pointer to temporary is not
                                      // a constant expression
constexpr auto test3 = self_ref(123); // OK
constexpr auto tets4 = near_ref(123); // FAIL, read of dereferenced
                                      // one-past-the-end pointer is not
                                      // allowed in a constant expression
```

## 2011: static\_assert в constexpr\-методах

Предложение [\[N3268\] static\_assert and list\-initialization in constexpr functions](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3268.htm) вводит возможность писать "статические" объявления, не вляющие на результат работы метода: _typedef_, _using_, _static\_assert_\. Это небольшое развинчивание гаек для _constexpr_\-методов\.

## 2012: \(Почти\) любой код в constexpr\-функциях

В 2012 году произошел большой рывок вперед с предложением [\[N3444\] Relaxing syntactic constraints on constexpr functions](http://open-std.org/JTC1/SC22/WG21/docs/papers/2012/n3444.html)\. Есть множество простых методов, которых желательно уметь вычислять в compile\-time, например степень _a^n_:

```cpp
// Compute a to the power of n
int pow(int a, int n)
{
  if (n < 0)
    throw std::range_error("negative exponent for integer power");
  if (n == 0)
    return 1;
  int sqrt = pow(a, n/2);
  int result = sqrt * sqrt;
  if (n % 2)
    return result * a;
  return result;
}
```

Однако программистам приходится фокусничать и писать в функциональном стиле \(убирать локальные переменные и _if_\-ы\), чтобы сделать ее _constexpr_\-вариант:

```cpp
constexpr int pow_helper(int a, int n, int sqrt)
{
  return sqrt * sqrt * ((n % 2) ? a : 1);
}

// Compute a to the power of n
constexpr int pow(int a, int n)
{
  return (n < 0)
    ? throw std::range_error("negative exponent for integer power")
    : (n == 0) ? 1 : pow_helper(a, n, pow(a, n/2));
}
```

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

* Так как в константных выражениях нельзя изменять переменные, циклы \(_for_/_while_/_do_/range\-based for\) заведомо невозможно использовать;
* _switch_ и _goto_ запрещены, чтобы вычислитель констант не моделировал сложные потоки управления;
* Как при старых ограничениях, для метода теоретически должен существовать набор аргументов, при которых ее можно использовать в константных выражениях\. Иначе считается, что метод помечен _constexpr_ ошибочно, и компиляция упадет с _constexpr function never produces a constant expression_\.

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

**Note\.** Таких переменных не будет сильно много из\-за жесткого ограничения на "глубину вызовов"\.

В методах можно объявлять _статические_ переменные\. Они могут иметь не\-литеральный тип \(чтобы, например, возвращать ссылки на них из метода; сами ссылки \- тип как раз литеральный\), но у них не должно быть _dynamic initialization_ \(т\.е\. должна быть хотя бы _zero\-initialization_\) и нетривиального деструктора\. Предложение приводит пример, где это могло бы быть полезно \(получение ссылки на нужный объект в compile\-time\):

```cpp
constexpr mutex &get_mutex(bool which)
{
  static mutex m1, m2; // non-const, non-literal, ok
  if (which)
    return m1;
  else
    return m2;
}
```

Также разрешили объявлять типы \(_class_, _enum_ и т\.д\.\) и возвращать _void_\.

## 2013: \(Почти\) любой код в constexpr\-функциях ver 2\.0 Mutable Edition

Однако Комитет решил, что поддержка циклов \(хотя бы _for_\) в _constexpr_\-методах \- это must have\. В 2013 году вышла поправленная версия предложения [\[N3597\] Relaxing constraints on constexpr functions](http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3597.html)\.

Для реализации "_constexpr_ _for_"\-а рассматривалось четыре варианта\.

Самым далеким от "остального C\+\+" варианта было создание совершенно новой конструкции для итераций, который подходил бы для функционального стиля тогдашнего _constexpr_\-кода; но это бы фактически создало новый под\-язык _constexpr C\+\+_ функционального стиля\.

Самым близким к "остальному C\+\+" вариантом было не заменять качество количеством, а просто стараться поддерживать в _constexpr_\-вычислениях широкое подмножество C\+\+ \(в идеале \- его весь\)\. **Этот вариант был выбран\.** Это сильно повлияло на дальнейшую историю _constexpr_\.

Поэтому возникла необходимость в **мутабельности объектов в рамках _constexpr_\-вычислений**\. Согласно предложению, объект, созданный в рамках вычисления _constexpr_\-выражения, теперь можно изменять в течение процесса вычисления, до тех пор, пока не закончится процесс вычисления или [лайфтайм](http://eel.is/c++draft/basic.life) объекта\.

Эти вычисления до сих пор происходят внутри своей "песочницы", ничто снаружи на них не влияет, поэтому, по идее, вычисление _constexpr_\-выражения с одними и теми же аргументами будет давать один и тот же результат \(не считая погрешностей в float\- и double\-вычислениях\)\.

Для лучшего понимания я скопировал кусок кода из предложения:

```cpp
constexpr int f(int a)
{
  int n = a;
  ++n;                  // '++n' is not a constant expression
  return n * a;
}

int k = f(4);           // OK, this is a constant expression.
                        // 'n' in 'f' can be modified because its lifetime
                        // began during the evaluation of the expression.

constexpr int k2 = ++k; // error, not a constant expression, cannot modify
                        // 'k' because its lifetime did not begin within
                        // this expression.

struct X
{
  constexpr X() : n(5)
  {
    n *= 2;             // not a constant expression
  }

  int n;
};

constexpr int g()
{
  X x;                  // initialization of 'x' is a constant expression
  return x.n;
}

constexpr int k3 = g(); // OK, this is a constant expression.
                        // 'x.n' can be modified because the lifetime of
                        // 'x' began during the evaluation of 'g()'.
```

От себя замечу, что теперь компилируется такой код:

```cpp
constexpr void add(X& x)
{
  x.n++;
}

constexpr int g()
{
  X x;
  add(x);
  return x.n;
}
```

Теперь в _constexpr_\-методах работает значимая часть С\+\+, и в методах разрешены сайд\-эффекты, локальные в рамках _constexpr_\-вычисления\. Вычислитель констант стал сложнее, но все еще справлялся с задачей\.

## 2013: Легендарные const\-методы и популярные constexpr\-методы

_constexpr_\-методы класса на данный момент автоматически помечаются как _const_\-методы\.

В предложении [\[N3598\] constexpr member functions and implicit const](http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3598.html) обратили внимание, что _constexpr_\-методы класса не обязательно неявно делать _const_\-методами\.

Это стало актуальнее с мутабельностью в _constexpr_\-вычислениях; но и до этого мешало использовать один и тот же метод в _constexpr_ и не\-_constexpr_ коде:

```cpp
struct B
{
  constexpr B() : a() {}
  constexpr const A &getA() const /*implicit*/ { return a; }
  A &getA() { return a; } // дублирование кода
  A a;
};
```

Интересно, что предложение давало на выбор три опции, из них был выбран второй:

1. Статус\-кво; минус: дублирование кода
1. _constexpr_ не будет неявно значить _const_; минус: ломает [ABI](https://habr.com/ru/post/490222/) \(const является частью [mangled\-имени](https://en.wikipedia.org/wiki/Name_mangling) метода\)
1. Добавить новый квалификатор и писать _constexpr A &getA\(\) mutable \{ return a; \}_; минус: новый баззворд в конце объявления

## 2015\-2016: Синтаксический сахар для шаблонов

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

```cpp
template <class T, class... Args> 
enable_if_t<is_constructible_v<T, Args...>, unique_ptr<T>> 
make_unique(Args&&... args) 
{
  return unique_ptr<T>(new T(forward<Args>(args)...));
}  

template <class T, class... Args>  
enable_if_t<!is_constructible_v<T, Args...>, unique_ptr<T>>
make_unique(Args&&... args) 
{
  return unique_ptr<T>(new T{forward<Args>(args)...});
}
```

Предложение [\[N4461\] Static if resurrected](http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4461.html) вводит выражение _static\_if_ \(позаимствовав из языка D\), чтобы код стал менее страшным:

```cpp
template <class T, class... Args> 
unique_ptr<T>
make_unique(Args&&... args) 
{
  static_if (is_constructible_v<T, Args...>)
  {
    return unique_ptr<T>(new T(forward<Args>(args)...));
  }
  else
  {
    return unique_ptr<T>(new T{forward<Args>(args)...});
  }
}
```

Этот кусок C\+\+ имеет довольно посредственное отношение к _constexpr_\-вычислениям и работает в другой сфере\. Но _static\_if_ в следующих ревизиях решили переименовать:

```cpp
constexpr_if (is_constructible_v<T, Args...>)
{
  return unique_ptr<T>(new T(forward<Args>(args)...));
}
constexpr_else
{
  return unique_ptr<T>(new T{forward<Args>(args)...});
}
```

Затем еще немного\.\.\.

```cpp
constexpr if (is_constructible_v<T, Args...>)
{
  return unique_ptr<T>(new T(forward<Args>(args)...));
}
constexpr_else
{
  return unique_ptr<T>(new T{forward<Args>(args)...});
}
```

И финальный вариант:

```cpp
if constexpr (is_constructible_v<T, Args...>)
{
  return unique_ptr<T>(new T(forward<Args>(args)...));
}
else
{
  return unique_ptr<T>(new T{forward<Args>(args)...});
}
```

## 2015: Constexpr\-лямбды

В очень хорошем предложении [\[N4487\] Constexpr Lambda](http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4487.pdf) подробно проработали вопрос использования closure type в _constexpr_\-вычислениях \(и написали поддержку в форкнутом clang\)\.

Чтобы понять, как возможно иметь _constexpr_\-лямбды, нужно понимать, как они устроены "внутри"\. Есть [статья про историю лямбд](https://habr.com/ru/company/otus/blog/444524/), где описано, как прото\-лямбды существовали уже в C\+\+03, и в сегодняшних лямбда\-выражениях создается похожий класс, скрытый за чертогами компилятора\.

**\[НАЧАЛО БЛОКА SPOILER\]**

### Прото\-лямбда для \[\]\(int x\) \{ std::cout << x << std::endl; \}

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

struct PrintFunctor
{
  void operator()(int x) const
  {
    std::cout << x << std::endl;
  }
};
int main()
{
  std::vector<int> v;
  v.push_back(1);
  v.push_back(2);
  std::for_each(v.begin(), v.end(), PrintFunctor());
}
```

**\[КОНЕЦ БЛОКА SPOILER\]**

Если все "захваченные" переменные являются литеральными типами, то closure type тоже предложено считать литеральным типом, а _operator\(\)_ пометить _constexpr_\. Работающий пример _constexpr_\-лямбд:

```cpp
constexpr auto add = [] (int n, int m)
{
  auto L = [=] { return n; };
  auto R = [=] { return m; };
  return [=] { return L() + R(); };
};

static_assert(add(3, 4)() == 7, "");
```

## 2017\-2019: Двойные стандарты

В предложении [\[P0595\] The constexpr Operator](http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0595r0.html) рассмотрели возможность "знать" внутри метода, где сейчас выполняется метод \- в вычислителе констант или в рантайме\. Автор предложил использовать для этого вызов _constexpr\(\)_, которые будет соответственно возвращать _true_ или _false_\.

```cpp
constexpr double hard_math_function(double b, int x)
{
  if (constexpr() && x >= 0)
  {
    // медленная формула, более точная (compile-time)
  }
  else
  {
    // быстрая формула, менее точная (run-time)
  }
}
```

Затем оператор был заменен на "магическую" функцию _std::is\_constant\_evaluated\(\)_ \([\[P0595R2\]](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0595r2.html)\) и в таком виде принят в стандарт С\+\+20\.

Если предложение разрабатывается долго, то авторы иногда делают "rebase" \(аналогично как в проектах в git/svn\), приводя его в соответствие с обновившимся состоянием\.

Так и здесь \- авторы [\[P1938\] if consteval](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1938r0.html) \(про _consteval_ будет позже\) обнаружили, что лучше создать новую запись:

```cpp
if consteval { }
if (std::is_constant_evaluated()) { }
// ^^^ аналогичные записи
```

Это решение было принято в C\+\+23 \- [ссылка на голосование](https://github.com/cplusplus/papers/issues/677)\.

## 2017\-2019: We need to go deeper

В _constexpr_\-методах во время _constexpr_\-вычислений пока нельзя использовать дебаггер и выводить логи\. Предложение [\[P0596\] std::constexpr\_trace and std::constexpr\_assert](http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0596r0.html) рассматривает введение специальных методов для этих целей\.

Это предложение было благосклонно принято \- [ссылка на голосование](https://github.com/cplusplus/papers/issues/602), но пока еще не доработано\.

## 2017: Злой двойник стандартной библиотеки

На данный момент _std::vector_, который желательно затащить в compile\-time, не может работать в _constexpr_\-вычислениях, в основном из\-за недоступности там операторов _new/delete_\.

Идея о допуске операторов _new_ и _delete_ в вычислитель констант на тот момент выглядела слишком амбициозно, поэтому в довольно странном предложении [\[P0597\] std::constexpr\_vector](http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0597r0.html) рассматривается введение магического _std::constexpr\_vector<T\>_\.

Он является противоположносью _std::vector<T\>_ \- может быть создан и изменен только во время _constexpr_\-вычислений\.

```cpp
constexpr constexpr_vector<int> x;           // Okay.
constexpr constexpr_vector<int> y{ 1, 2, 3 };// Okay.
const constexpr_vector<int> xe;              // Invalid: not constexpr
```

Как вычислитель констант должен работать с памятью \- не описано\. [@antoshkka](https://habr.com/users/antoshkka) и [@ZaMaZaN4iK](https://habr.com/users/zamazan4ik) \(авторы многих предложений\) в [\[P0639R0\] Changing attack vector of the constexpr\_vector](http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0639r0.html) выявили большое количество минусов подхода, и предложили поменять направление работы в сторону абстрактного магического _constexpr allocator_, который не дублирует всю стандартную библиотеку\.

## 2017\-2019: Constexpr обретает память

В презентации [Constexpr ALL the thing\!](https://youtu.be/HMB9oXFobJc) демонстрировался пример _constexpr_\-библиотеки для работы с JSON\-объектами \(то же самое, но в бумажном виде, есть в [\[P0810\] constexpr in practice](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0810r0.pdf)\):

```cpp
constexpr auto jsv
    = R"({
          "feature-x-enabled": true,
          "value-of-y": 1729,
          "z-options": {"a": null,
                        "b": "220 and 284",
                        "c": [6, 28, 496]}
         })"_json;

if constexpr (jsv["feature-x-enabled"])
{
  // code for feature x
}
else
{
  // code when feature x turned off
}
```

Авторы сильно пострадали от невозможности использовать контейнеры STL, и написали свои велосипедные аналоги _std::vector_ и _std::map_, имеющие внутри себя _std::array_ \(умеющий работать в _constexpr_\)\.

Предложение [\[P0784\] Standard containers and constexpr](http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0784r7.html) исследует возможность ввода STL\-контейнеров в _constexpr_\-вычисления\.

**Note\.** Важно знать, что такое _аллокатор_\. STL\-контейнеры работают через него с памятью\. Какой именно аллокатор \- задается через [аргумент шаблона](https://en.cppreference.com/w/cpp/container/vector)\. Чтобы войти в тему, можно почитать [эту статью](https://habr.com/ru/post/505632/)\.

Что же мешает разрешить STL\-контейнеры в _constexpr_\-вычислениях? Есть три проблемы:

1. Деструкторы не могут быть объявлены _constexpr_ \(у _constexpr_\-объектов он обязан быть тривиальным\)\.
1. Недоступна динамическая аллокация/деаллокация памяти\.
1. Недоступен _placement\-new_ для вызова конструктора в аллоцированной памяти\.

**Первая проблема\.** С этим разобрались быстро \- авторы предложения обсуждали проблему с разработчиками фронтенда MSVC\+\+, GCC, Clang, EDG, и они подтвердили, что ограничение можно легко ослабить\. Теперь от литеральных типов можно требовать наличие _constexpr_\-деструктора, а не строго тривиального деструктора\.

**Вторая проблема\.** Работа с памятью не очень проста\. Как уже упоминалось, вычислитель констант _обязан_ отлавливать undefined behaviour в любом виде и останавливать компиляцию в случае его наличия\.

Это значит, что вместе с многими объектами нужно трекать их "метаданные", которые держат руку на пульсе и не дают сломать выполнение программы\. Пара примеров таких метаданных:

* Информация, какое поле в _union_\-е активно \([\[P1330\]](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1330r0.pdf)\) \(примеры undefined behavior: запись в член неактивного поля\)
* Жесткая связь между указателем или ссылкой и соответствующим ему реальным ранее созданным объектом \(примеры undefined behavior: бесконечное множество\)

Из\-за этого не имеет смысла использовать подобные методы:

```cpp
void* operator new(std::size_t);
```

Так как нет никакого обоснования приводить _void\*_ к _T\*_\. Короче говоря, новая ссылка/указатель либо могут начать указывать на существующий объект, либо быть созданным "одновременно" с ним, и никак иначе\.

Поэтому есть две опции работы с памятью, которые будут законны в _constexpr_\-вычислениях:

1. Простые new\- и delete\-выражения: _int\* i \= new int\(42\)_;
1. Использование стандартного аллокатора: [std::allocator](https://en.cppreference.com/w/cpp/memory/allocator) \(его подпилили напильником\)

**Третья проблема\.** Стандартные контейнеры разделяют аллокацию памяти и конструирование объектов в этой памяти\. С аллокацией уже разобрались \- с условием на метаданные ее обеспечить возможно\.

Контейнеры полагаются на [std::allocator\_traits](https://en.cppreference.com/w/cpp/memory/allocator_traits), в частности для конструирования \- на его метод [construct](https://en.cppreference.com/w/cpp/memory/allocator_traits/construct)\. До предложения он имел вид

```cpp
template< class T, class... Args >
static void construct( Alloc& a, T* p, Args&&... args )
{
  ::new (static_cast<void*>(p)) T(std::forward<Args>(args)...);
  // ^^^ placement-new, запрещенный в constexpr-вычислениях
}
```

То есть его нельзя использовать из\-за приведения к _void\*_ и placement\-new \(которые в общем виде в _constexpr_ запрещены\)\. А в предложении преобразовался в:

```cpp
template< class T, class... Args >
static constexpr void construct( Alloc& a, T* p, Args&&... args )
{
  std::construct_at(p, std::forward<Args>(args)...);
}
```

[std::construct\_at](https://en.cppreference.com/w/cpp/memory/construct_at) \- это метод, который в рантайме работает аналогично старому коду \(с приведением к _void\*_\), а в _constexpr_\-вычислениях он:

\.∧＿∧

\( ･ω･｡\)つ━☆・\*。

⊂　 ノ 　　　・゜\+\.

しーＪ　　　°。\+ \*´¨\)

　　　　　　　　　\.· ´¸\.·\*´¨\) ¸\.·\*¨\)

　　　　　　　　　　\(¸\.·´ \(¸\.·'\* ☆ Вжуххххх, и просто работает\! ☆

То есть компиляторный вычислитель констант обработает его особым образом: по видимости, вызвав конструктор у объекта, который связан с _T\* p_, без бюрократии\.

Этого достаточно, чтобы контейнеры стало возможно использовать в _constexpr_\-вычислениях\.

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

Такой новый тип аллокации памяти назван _transient constexpr allocations_\. Слово _transient_ приблизительно можно перевести как "мимолетный" или "недолговечный"\.

В этом предложении еще был кусок про _non\-transient allocation_ \- дать возможность освобождать не всю аллоцированную память, тогда неосвобожденная память "вываливается" из "песочницы" и конвертируется бы в static storage \(другими словами, в секцию _\.rodata_\)\. Но Комитет посчитал эту возможность "слишком хрупкой" \("_too brittle_"\) по многим причинам, и пока его не принял\.

В остальном, это предложение было принято\.

## 2018: Поймай меня, если сможешь

Предложение [\[P1002\] Try\-catch blocks in constexpr functions](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1002r1.pdf) вводит try\-catch блоки в _constexpr_\-вычисления\.

Это предложение немного сбивает с толку, так как _throw_ на тот момент был запрещен в _constexpr_\-вычислениях \(значит, _catch_\-кусок кода никогда не запускается\)\.

Судя по документу, это ввели, чтобы пометить все методы _std::vector_ как _constexpr_ \- в libc\+\+ \(реализация STL\) в методе _vector::insert_ используется try\-catch блок\.

## 2018: Я сказал constexpr\!

Из личного опыта знакомо, что двойственность _constexpr_\-методов \(могут выполняться и в compile\-time, и в run\-time\) приводит к тому, что вычисления проваливаются в рантайм там, где не ожидаешь: [пример кода](https://godbolt.org/z/f8xY7T9xn); и для того, чтобы гарантировать нужный этап, нужно фокусничать: [пример кода](https://godbolt.org/z/9Prs41nhj)\.

Предложение [\[P1073\] constexpr\! functions](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1073r0.html) вводит новое ключевое слово _constexpr\!_ для методов, которые должны работать только в compile\-time\. Эти методы называются _immediate_\-методами\.

```cpp
constexpr! int sqr(int n)
{
  return n*n;
}

constexpr int r = sqr(100);  // Okay.
int x = 100;
int r2 = sqr(x);             // Error: Call does not produce
                             // a constant.
```

Если существует _возможность_ того, что в _constexpr\!_\-метод могут попасть переменные, неизвестные на этапе компиляции \(что норма для _constexpr_\-методов\), то программа не компилируется:

```cpp
constexpr! int sqrsqr(int n)
{
  return sqr(sqr(n)); // Not a constant expression at this point,
}                     // but that's okay.

constexpr int dblsqr(int n)
{
  return 2 * sqr(n); // Error: Enclosing function is not
}                    // constexpr!.
```

К _constexpr\!_\-методу нельзя взять указатель или ссылку\. Бэкенду компилятора вовсе не обязательно \(и не нужно\) знать про существование таких функций, помещать их в таблицы символов и т\.д\.

В следующих ревизиях этого предложения ключевое слово _constexpr\!_ поменяли на _consteval_\.

Разница между _constexpr_ и _consteval_ налицо \- во втором случае нет провала в рантайм: [пример с constexpr](https://godbolt.org/z/f8xY7T9xn), [пример с consteval](https://godbolt.org/z/x6ds7vM8r)\.

## 2018: Слишком радикальный constexpr

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

Предложение [\[P1235\] Implicit constexpr](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1235r0.pdf) предлагает по умолчанию помечать все методы, имеющие определение, как _constexpr_\. Но можно и запретить выполнять метод в compile\-time:

1. <нет спецификатора\> \- метод помечается как _constexpr_, если возможно\.
1. _constexpr_ \- работает как сейчас
1. _constexpr\(false\)_ \- не может быть вызван в compile\-time
1. _constexpr\(true\)_ \- может быть вызван только в compile\-time, т\.е\. аналогично _constexpr\!/consteval_

Это предложение не было принято \- [ссылка на голосование](https://github.com/cplusplus/papers/issues/292)\.

## 2020: Долговечная constexpr\-память

Как уже обсуждалось, после принятия предложения [\[P0784\] Standard containers and constexpr](http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0784r7.html) в _constexpr_\-вычислениях стало возможно аллоцировать память, но ее всю необходимо освобождать до окончания _constexpr_\-вычисления: это так называемые _transient constexpr allocations_\.

Таким образом, нельзя создавать top\-level _constexpr_\-объекты почти всех STL\-контейнеров и многих других классов\.

Под "top\-level объектом" я имею в виду результат всего _constexpr_\-вычисления, например:

```cpp
constexpr TFoo CalcFoo();
constexpr TFoo FooObj = CalcFoo();
```

Здесь вызов _CalcFoo\(\)_ начинает _constexpr_\-вычисление, а _FooObj_ \- его результат и _top\-level_ _constexpr_\-объект\.

Предложение [\[P1974\] Non\-transient constexpr allocation using propconst](http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/p1974r0.pdf) находит путь к решению проблемы\. На мой взгляд, это самое интересное предложение из всех, что я привел в статье \(оно заслуживало бы отдельной статьи\)\. Ему дали "зеленый свет" и оно развивается \- [ссылка на тикет](https://github.com/cplusplus/papers/issues/867)\. Здесь я его перескажу в понятной форме\.

Что же мешает нам иметь _non\-transient allocations_? Вообще, проблема не в том, чтобы запихать куски памяти в static storage \(_\.bss_/_\.rodata_/их аналоги\), а в том, чтобы проверить, что вся схема обладает чёткой **консистентностью**\.

Допустим, что мы имеем некий _constexpr_\-объект, конструирование \(точнее, "вычисление"\) которого спровоцировало _non\-transient allocations_\. Значит, теоретическое деконструирование этого объекта \(т\.е\. вызов его деструктора\) должно освободить всю _non\-transient_ память\. Если вызов деструктора не освободил бы память, то это плохо \(**консистентности** нет\) и надо выдавать ошибку компиляции\.

Другими словами, вот что должен делать вычислитель констант:

1. Увидев запрос на _constexpr_\-вычисление, произвести его\.
1. В результате вычисления получить объект, скрывающий под собой пачку _constexpr_\-переменных литерального типа; и некоторый объём неосвобожденной памяти \(_non\-transient allocations_\)\.
1. _Имитировать_ вызов деструктора у данного объекта \(не вызывая его на самом деле\), и проверить что этот вызов _освободил бы_ всю _non\-transient_ память\.
1. Если все проверки прошли успешно, то **консистентность** доказана\. _Non\-transient allocations_ можно двигать в static storage\.

Это звучит логично, и допустим, что это всё реализовано\. Но тогда получим проблему с подобным кодом с _non\-transient_ памятью, которую стандарт не будет запрещать менять, и тогда проверка на вызов деструктора будет бессмысленной:

```cpp
constexpr unique_ptr<unique_ptr<int>> uui
    = make_unique<unique_ptr<int>>(make_unique<int>());

int main()
{
  unique_ptr<int>& ui = *uui;
  ui.reset();
}
```

**Note\.** В реальности такой код получил бы отпор от ОС за попытку записи в read\-only сегмент RAM, но это _физическая_ константность\. А в коде должна быть _логическая_ константность\.

Пометка _constexpr_ у объектов влечет за собой пометку их как _const_, и все их члены также становятся _const_\.

Однако если у объекта есть член указательного типа, то это ломает всю малину \- заставить его _указывать_ на другой объект станет нельзя, но менять объект, на который он _указывает_, не запрещено\.

У указательных типов есть два ортогональных параметра константности:

1. Можно ли начать указывать на другой объект?
1. Можно ли изменять объект, на который указывается?

И получается 4 варианта с разными свойствами \(_OK_ \- строка компилируется, _FAIL_ \- нет\):

```cpp
int *test1 { nullptr };
test1 = &dummy; // OK
*test1 = dummy; // OK

int const *test2 { nullptr };
test2 = &dummy; // OK
*test2 = dummy; // FAIL

int * const test3 { nullptr };
test3 = &dummy; // FAIL
*test3 = dummy; // OK

int const * const test4 { nullptr };
test4 = &dummy; // FAIL
*test4 = dummy; // FAIL
```

"Обычный" _const_ приводит к третьему варианту, но для _constexpr_ необходим четвертый\! То есть необходим так называемый _deep\-const_\.

Предложение на базе пары других, более старых предложений, предлагает для этого ввести новый [cv\-qualifier](https://en.cppreference.com/w/cpp/language/cv) _propconst_ \(_propagating const_\)\.

Этот квалификатор будет использоваться с указательными/ссылочными типами:

```cpp
T propconst *
T propconst &
```

И в зависимости от типа _T_ компилятор будет либо конвертировать это слово в _const_, либо удалять его\. Первый случай \- если _T_ константный, второй \- если нет:

```cpp
int propconst * ---> int *
int propconst * const ---> int const * const
```

В предложении приведена таблица конвертации _propconst_ в разных случаях\.

Таким образом, _constexpr_\-объекты могли бы обрести полную логическую константность \(_deep\-const_\):

```cpp
constexpr unique_ptr<unique_ptr<int propconst> propconst> uui = 
  make_unique<unique_ptr<int propconst> propconst>( 
    make_unique<int propconst>() 
  ); 

int main()
{
  // две строки ниже не скомпилируются
  unique_ptr<int propconst>& ui1 = *uui;
  ui1.reset();

  // строка ниже скомпилируется
  const unique_ptr<int propconst>& ui2 = *uui;
  // строка ниже не скомпилируется
  ui2.reset();
}

// P.S. Такая запись еще не принята Комитетом, я надеюсь, сделают лучше
```

## 2021: Constexpr\-классы

С появлением полностью _constexpr_\-классов, включая _std::vector_, _std::string_, _std::unique\_ptr_, в которых все методы помечены как _constexpr_, возникает желание сказать "пометь все методы класса как _constexpr_"\.

Это делает предложение [\[P2350\] constexpr class](http://open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2350r1.pdf):

```cpp
class SomeType
{
public:
  constexpr bool empty() const { /* */ }
  constexpr auto size() const { /* */ }
  constexpr void clear() { /* */ }
  // ...
};
// ^^^ ДО

class SomeType constexpr
{
public:
  bool empty() const { /* */ }
  auto size() const { /* */ }
  void clear() { /* */ }
  // ...
};
// ^^^ ПОСЛЕ
```

С этим предложением связана интересная история \- еще не зная о его существовании, я вкинул в [stdcpp\.ru](http://stdcpp.ru/) идею предложить такую же штуку: [ссылка на тикет](https://github.com/cpp-ru/ideas/issues/479) \(что сейчас не нужно\)\.

Многие почти одинаковые предложения в Стандарт могут появиться почти одновременно\. Это говорит в пользу [теории множественных открытий](https://ru.wikipedia.org/wiki/%D0%9C%D0%BD%D0%BE%D0%B6%D0%B5%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BE%D1%82%D0%BA%D1%80%D1%8B%D1%82%D0%B8%D0%B5): идеи "витают в воздухе", и в принципе не так важно, кто их открывает \- если комьюнити достаточно большое, то естественная эволюция происходит\.

## 2019\-∞: Интерпретатор констант в компиляторе

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

С 2019 года в Clang разрабывается [ConstantInterpeter](https://clang.llvm.org/docs/ConstantInterpreter.html), который в перспективе может полностью заменить вычислитель констант на синтаксическом дереве\. Он довольно интересен и заслуживал бы отдельной статьи\.

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

Документация для ConstantInterpeter хорошая, и также много интересного есть в [видео\-выступлении](https://youtu.be/LgrgYD4aibg) создателя интерпретатора на конференции LLVM\-разработчиков\.

## Что можно еще посмотреть?

Для того, чтобы больше расширить понимание, можно посмотреть замечательные выступления от экспертов\. В каждом выступлении авторы выходят за рамки рассказа про _constexpr_: это может быть построение _constexpr_\-библиотеки; рассказ про использование _constexpr_ в будущем [reflexpr](https://en.cppreference.com/w/cpp/experimental/reflect); или про устройство вычислителя констант и интерпретатора констант\.

* [constexpr ALL the things\!](https://youtu.be/HMB9oXFobJc), Ben Deane & Jason Turner, C\+\+Now 2017\. Уже немного устарело, но может быть интересно, про построение _constexpr_\-библиотеки\.
* [Compile\-time programming and reflection in C\+\+20 and beyond](https://youtu.be/CRDNPwXDVp0), Louis Dionne, CppCon 2018\. Много внимания уделяется будущей рефлексии в C\+\+\.
* [Полезный constexpr](https://youtu.be/MXEgTYDnfJU), Антон Полухин \([@antoshkka](https://habr.com/users/antoshkka)\), C\+\+ CoreHard Autumn 2018\. Есть про компиляторы, рефлексию и метаклассы\.
* [The clang constexpr interpreter](https://youtu.be/LgrgYD4aibg), Nandor Licker, 2019 LLVM Developers' Meeting\. Рокет саенс и интерпретатор кода для _constexpr_\.

Я хотел бы, чтобы здесь было выступление про киллер\-фичу \(по моему мнению\) [\[P1040\] std::embed](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1040r3.html), которая отлично работала бы в тандеме с _constexpr_\. Но, судя по [тикету](https://github.com/cplusplus/papers/issues/28), его планируют реализовать в C\+\+\-_каком\-то_\.