- PVSM.RU - https://www.pvsm.ru -
constexpr
- одно из самых магических ключевых слов в современном C++. Оно дает возможность создать код, который будет выполнен еще до окончания процесса компиляции, что является абсолютным пределом для быстродействия программ.
У constexpr
с каждым годом становится больше возможностей. Сейчас использовать в compile-time вычислениях можно почти всю стандартную библиотеку. Пример вычисления числа до 1000 с наибольшим количеством делителей: ссылка на код [1].
История constexpr
насчитывает долгую историю эволюции с ранних версий C++. Исследуя предложения в стандарт и исходники компиляторов, можно понять, как слой за слоем создавалась эта часть языка, почему именно так она выглядит, как на практике вычисляются constexpr
-выражения, какие возможности ждут нас в будущем, а какие - могли бы быть, но не были приняты в Стандарт.
Эта статья подходит как тем, кто еще не знает, что такое constexpr
, так и тем, кто уже долгое время его использует.
В C++ в некоторых местах нужно использовать целочисленные константы, значения которых должны быть известны в compile-time. Стандарт разрешает записывать константы в виде несложных выражений, как в этом коде:
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. Они могут содержать только:
Литералы [2] (туда входят целые числа, это интегральные типы)
Значения enum
-ов
Параметр шаблона интегрального или enum
-типа (напр., значение V
из template<int V>
)
sizeof
-выражение
const-переменные, инициализированные constant-expression - интересный пункт
Все пункты, кроме последнего, неудивительны - они точно известны во время компиляции. Переменные - более интересный случай.
Если у переменной static storage, то в обычной ситуации память под нее заполняется нулями и изменяется во время работы программы. Но для переменных из списка выше это слишком поздно - вычислять их значения нужно еще до окончания процесса компиляции.
В стандартах C++98/03 есть два вида static initialization:
zero-initialization, память заполняется нулями, затем в ходе программы изменяется
initialization with a constant expression, память (если нужна) сразу содержит вычисленное значение
[Note: Все остальные инициализации называются dynamic initialization, мы их не рассматриваем. — end note]
[Note: zero-initialized переменная во время работы программы в свою очередь может быть проинициализирована еще раз "нормальным" значением, это уже будет dynamic initialization (пусть даже до старта main
). — end note]
Рассмотрим пример с обоими видами переменных:
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 переменной, то скомпилированной программе разрешено не резервировать для нее память, поэтому "заставим" ее это сделать. Выведем значения переменных и их адреса:
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
Скомпилируем объектный файл и посмотрим на таблицу символов:
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 [3], остальные в секцию .rodata
.
Загрузчик операционной системы перед запуском загружает программу так, что секция .rodata
оказывается в сегменте с read-only режимом, который защищен от записи на уровне ОС.
Попробуем через const_cast
изменить данные по адресу переменных. Стандарт дает уклончивый ответ на тему того, когда запись в результат const_cast
-а может спровоцировать Undefined Behaviour. По крайней мере, этого не происходит, если мы убираем const с объекта/указателя на объект, который не является фундаментально константным изначально. Т.е. надо различать физическую константность и логическую.
UB-санитайзер поймает UB (программа крашится), если попытаться изменить .rodata
-переменную, и его не будет, если записывать в .bss
или автоматические переменные.
const int& ref = testX;
const_cast<int&>(ref) = 13; // OK for test3, test4; SEGV for test1, test2, test3
std::cout << ref << std::endl;
Таким образом, одни const-переменные "константнее" других. Насколько известно, в то время не было простого способа проверить или как-то проконтролировать, что переменная была initialized with a const. expr.
Чтобы представлять, как константые выражения вычисляются во время компиляции, нужно понимать устройство компилятора.
Компиляторы идейно похожи друг на друга, я опишу процесс вычисления на основе компилятора Clang/LLVM. Я скопировал базовую информацию про этот компилятор из своей прошлой статьи [4]:
Про само устройство Clang и LLVM написано уже много статей. На хабре я бы посоветовал эту статью [5], чтобы понять их краткую историю и общую схему.
Количество стадий компиляций зависит от того, кто объясняет устройство компилятора. Анатомия компилятора многоуровнева и на самом абстрактном уровне выглядит так, что есть три разные программы:
Front-end: переводит исходник из C/C++/Ada/Rust/Haskell/... в LLVM IR [6] - особое промежуточное представление. Фронтендом для C-like языков является Clang.
Middle-end: LLVM IR оптимизируется в зависимости от настроек.
Back-end: LLVM IR переводится в машинный код под нужную платформу - x86/Arm/PowerPC/...
Для простых языков реально написать компилятор под 1000 строк [7] и получить всю мощь фреймворка LLVM - для этого нужно реализовать фронтенд. Также можно использовать lex/yacc - готовые синтаксические парсеры.
На менее абстрактном уровне находится фронтенд Clang, который выполняет такие действия (не рассматривая препроцессор и прочие "микро"-шаги):
Лексический анализ [8]: перевод символов в токены, например []() { return 13 + 37; }
преобразуются в (l_square) (r_square) (l_paren) (r_paren) (l_brace) (return) (numeric_constant:13) (plus) (numeric_constant:37) (semi) (r_brace)
.
Синтаксический анализ [9]: создание AST (Abstract Syntax Tree), то есть перевод токенов из предыдущего пункта в вид (lambda-expr (body (return-expr (plus-expr (number 13) (number 37)))))
.
Кодогенерация: создание LLVM IR по данному AST.
Итак, вычисление константных выражений (и близкородственных ему вещей, как инстанцирование шаблонов) происходит строго в фронтенде компилятора C++ (в нашем случае в Clang), а LLVM такими вещами не занимается.
"Микросервис", который занимается вычислением константных выражений (от самых простых в C++98 до сложных в C++23), назовем условно вычислитель констант.
Если согласно Стандарту в каком-то месте кода ожидается константное выражение; и выражение, которое там находится, действительно удовлетворяет требованиям на константное выражение, то Clang должен "не отходя от кассы" в 100% случаев уметь вычислять его.
Ограничения на константные выражения на протяжении лет постоянно смягчались, а вычислитель констант Clang соответсвенно постоянно усложнялся, вплоть до управления моделью памяти.
Есть старая документация [10] 9-летней давности, описывающая вычисление констант для C++98/03. Так как константные выражения тогда были очень простыми, они выполнялись с помощью обычного constant folding [11] через анализ синтаксического дерева (AST). Так как в синтаксическом дереве все арифметические выражения уже разобраны в виде под-деревьев, то вычисление константы - просто элементарный обход под-дерева.
Исходник вычислителя констант находится в lib/AST/ExprConstant.cpp [12] и на момент написания статьи разросся до почти 16 тысяч строк. С годами он научился интерпретировать много всего, например циклы (EvaluateLoopBody [13]), и всё это на синтаксическом дереве.
У константных выражений есть важное отличие от кода, выполняющегося в рантайме - они обязаны не допускать undefined behaviour. Если вычислитель констант наткнется на UB, компиляция провалится.
c.cpp:15:19: error: constexpr variable 'foo' must be initialized by a constant expression
constexpr int foo = 13 + 2147483647;
^ ~~~~~~~~~~~~~~~
Вычислитель констант используется не только для константных выражений, но также для поиска потенциальных багов в остальном коде. Это побочная выгода от технологии. Вот так находится переполнение для не-константного кода (могут максимум бросить warning):
c.cpp:15:18: warning: overflow in expression; result is -2147483636 with type 'int' [-Winteger-overflow]
int foo = 13 + 2147483647;
^
Изменения в стандарт происходят через предложения.
Все предложения в Стандарт находятся на open-std.org [14]. Практически все написаны понятно - чаще всего есть:
Краткий обзор области со ссылками на разделы Стандарта;
Текущие проблемы;
Предлагаемое решение проблем;
Предлагаемые изменения в текст Стандарта;
Ссылки на предложения-предшественники и прошлые ревизии предложения;
В особо продвинутых предложениях - ссылки на реализацию в форке компилятора. В тех, что я видел, реализацию делали в форке clang-a.
По ссылкам на предыдущие предложения можно отследить эволюцию для каждого куска C++.
Далеко не все предложения из архива были в итоге приняты (хотя и могли быть использованы как база для других, принятых предложений), поэтому стоит понимать, что они описывают некий альтернативный тому времени вариант C++, а не кусок современного C++.
Принять участие в эволюции C++ может любой - для русскоязычных экспертов есть stdcpp.ru [15].
Предложение от 2003 года [N1521] Generalized Constant Expressions [16] указывает на проблему того, что если часть выражения вычисляется с использованием вызова метода, то выражение не является constant-expression. Это заставляет злоупотреблять макросами, если нужно получить более-менее сложное константное выражение:
#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 не прижился end note].
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
из прошлого раздела становились бы "фундаментально" константными, без изменения в коде.
Предложение считает, что можно пойти еще дальше и надо рассмотреть вариант, что такой код тоже должен компилироваться:
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 метода. То есть, согласно общей логике предложения, этот код можно свести примерно к такому (вынос переменных и методов вне структуры):
// имитация вызова конструктора 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: Однако предложения не существуют в отрыве от компиляторов - если предложение нереально реализовать в разумный срок, его вряд ли примут end note].
Как с переменными, программист не может проконтролировать, что метод является constant-valued.
К счастью, через 3 года в следующих ревизиях этого предложения ([N2235] [17]) признали, что слишком много неявности это плохо. В список проблем добавили невозможность контроля за инициализацией:
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
не компилируется, возможно этот код так никогда и не начал работать):
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
-ами из предложения:
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
-переменными могли являться не только числа и перечисления, а литеральные типы [18], которых ввели в этом предложении (пока еще без reference type
). Литеральный тип - такой, который может быть передан в constexpr
-метод, и/или изменен и/или возвращен из нее. Это достаточно простые типы, чтобы компиляторы могли его поддерживать в вычислителе констант.
Ключевое слово constexpr
стало спецификатором, нужным компилятору - примерно как override
в классах. После обсуждения предложения, для этого ключевого слова решили не делать новый storage class [19] (хотя было бы законно), новый type qualifier [20], и также его решили не разрешать использовать для аргументов методов, чтобы не переусложнять правила перегрузки методов.
В этом году вышло предложение [N2349] Constant Expressions in the Standard Library [21], где пометили как constexpr
некоторые методы и константы, а также некоторые методы контейнеров, например:
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
и почти всегда не очень интересны - будем рассматривать изменения в сам язык.
constexpr
-методы изначально не предполагалось разрешать делать рекурсивными по причине отсутствия убедительных доводов в наличие рекурсии, но затем это ограничение убрали, что отметили в [N2826] Issues with Constexpr [22].
constexpr unsigned int factorial( unsigned int n )
{ return n==0 ? 1 : n * factorial( n-1 ); }
У компилятора существует некий предел вложенности вызовов (в clang это 512 вложенных вызовов), при превышении он откажется считать выражение.
Подобные пределы также есть, например, для инстанцирования шаблонов (если мы бы считали в compile-time через шаблоны, а не через constexpr
-методы)
На данный момент многие методы не могут быть помечены constexpr из-за имеющихся в аргументах константных ссылок. Во все constexpr
-методы параметры передаются по значению, т.е. копируются.
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) [23] разрешает константные ссылки в аргументах и как возвращаемое значение.
Это опасное изменение: до этого вычислитель констант имел дело с простыми выражениями и constexpr
-переменными (объект литерального класса - по сути набор из constexpr
-переменных); но введение ссылок пробивает "четвертую стену", потому что это понятие относится к модели памяти, которой в вычислителе нет.
В общем случае работа со ссылками и указателями в constant-expression превращает C++-компилятор в C++-интерпретатор, поэтому накладываются различные ограничения.
Если вычислитель констант может обработать метод с аргументом типа T, то типа const T& тоже сможет - если будет "представлять себе", что для этого аргумента создается "временный объект".
Более-менее сложная работа со ссылками и попытки что-либо поломать не скомпилируются
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
Предложение [N3268] static_assert and list-initialization in constexpr functions [24] вводит возможность писать "статические" объявления, не вляющие на результат работы метода: typedef
, using
, static_assert
. Это небольшое развинчивание гаек для constexpr
-методов.
В 2012 году произошел большой рывок вперед с предложением [N3444] Relaxing syntactic constraints on constexpr functions [25]. Есть множество простых методов, которых желательно уметь вычислять в compile-time, например степень :
// 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
-вариант:
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: таких переменных не будет сильно много из-за жесткого ограничения на "глубину вызовов" end note].
В методах можно объявлять статические переменные. Они могут иметь не-литеральный тип (чтобы, например, возвращать ссылки на них из метода; сами ссылки - тип как раз литеральный), но у них не должно быть dynamic initialization (т.е. должна быть хотя бы zero-initialization) и нетривиального деструктора. Предложение приводит пример, где это могло бы быть полезно (получение ссылки на нужный объект в compile-time):
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
.
Однако Комитет решил, что поддержка циклов (хотя бы for
) в constexpr
-методах это must have. В 2013 году вышла поправленная версия предложения [N3597] Relaxing constraints on constexpr functions [26].
Для реализации "constexpr for
"-а рассматривалось четыре варианта.
Самым далеким от "остального C++" варианта было создание совершенно новой конструкции для итераций, который подходил бы для функционального стиля тогдашнего constexpr
-кода; но это бы фактически создало новый под-язык constexpr C++
функционального стиля.
Самым близким к "остальному C++" вариантом было не заменять качество количеством, а просто стараться поддерживать в constexpr
-вычислениях широкое подмножество C++ (в идеале - его весь). Этот вариант был выбран. Это сильно повлияло на дальнейшую историю constexpr
.
Поэтому возникла необходимость в мутабельности объектов в рамках constexpr-вычислений. Согласно предложению, объект, созданный в рамках вычисления constexpr-выражения, теперь можно изменять в течение процесса вычисления, до тех пор, пока не закончится процесс вычисления или лайфтайм [27] объекта.
Эти вычисления до сих пор происходят внутри своей "песочницы", ничто снаружи на них не влияет, поэтому, по идее, вычисление constexpr-выражения с одними и теми же аргументами будет давать один и тот же результат (не считая погрешностей в float- и double-вычислениях).
Для лучшего понимания я скопировал кусок кода из предложения:
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()'.
От себя замечу, что теперь компилируется такой код:
constexpr void add(X& x) {
x.n++;
}
constexpr int g() {
X x;
add(x);
return x.n;
}
Теперь в constexpr
-методах работает значимая часть С++, и в методах разрешены сайд-эффекты, локальные в рамках constexpr-вычисления. Вычислитель констант стал сложнее, но все еще справлялся с задачей.
constexpr
-методы класса на данный момент автоматически помечаются как const
-методы.
В предложении [N3598] constexpr member functions and implicit const [28] обратили внимание, что constexpr
-методы класса не обязательно неявно делать const
-методами.
Это стало актуальнее с мутабельностью в constexpr-вычислениях; но и до этого мешало использовать один и тот же метод в constexpr и не-constexpr коде:
struct B {
constexpr B() : a() {}
constexpr const A &getA() const /*implicit*/ { return a; }
A &getA() { return a; } // дублирование кода
A a;
};
Интересно, что предложение давало на выбор три опции, из них был выбран второй:
Статус-кво; минус: дублирование кода
constexpr
не будет неявно значить const
; минус: ломает ABI [29] (const
является частью mangled-имени [30] метода)
Добавить новый квалификатор и писать constexpr A &getA() mutable { return a; }
; минус: новый баззворд в конце объявления
В метапрограммировании шаблонов обычно перегружают методы, если в теле требуется разная обработка в зависимости от свойств типа. Пример страшного кода:
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 [31] вводит выражение static_if
(позаимствовав из языка D), чтобы код стал менее страшным:
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
в следующих ревизиях решили переименовать:
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)...});
}
Затем еще немного...
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)...});
}
И финальный вариант:
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)...});
}
В очень хорошем предложении [N4487] Constexpr Lambda [32] подробно проработали вопрос использования closure type в constexpr
-вычислениях (и написали поддержку в форкнутом clang).
Чтобы понять, как возможно иметь constexpr-лямбды, нужно понимать, как они устроены "внутри". Есть статья про историю лямбд [33], где описано, как прото-лямбды существовали уже в C++03, и в сегодняшних лямбда-выражениях создается похожий класс, скрытый за чертогами компилятора.
#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());
}
Если все "захваченные" переменные являются литеральными типами, то closure type тоже предложено считать литеральным типом, а operator()
пометить constexpr
. Работающий пример constexpr-лямбд:
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, "");
В предложении [P0595] The constexpr Operator [34] рассмотрели возможность "знать" внутри метода, где сейчас выполняется метод - в вычислителе констант или в рантайме. Автор предложил использовать для этого вызов constexpr()
, которые будет соответственно возвращать true
или false
.
constexpr double hard_math_function(double b, int x) {
if (constexpr() && x >= 0) {
// медленная формула, более точная (compile-time)
} else {
// быстрая формула, менее точная (run-time)
}
}
Затем оператор был заменен на "магическую" функцию std::is_constant_evaluated()
([P0595R2] [35]) и в таком виде принят в Стандарт С++20.
Если предложение разрабатывается долго, то авторы иногда делают "rebase" (аналогично как в проектах в git/svn), приводя его в соответствие с обновившимся состоянием.
Так и здесь - авторы [P1938] if consteval [36] (про consteval
будет позже) обнаружили, что лучше создать новую запись:
if consteval { }
if (std::is_constant_evaluated()) { }
// ^^^ аналогичные записи
Это решение было принято в C++23 - ссылка на голосование [37].
В constexpr-методах во время constexpr-вычислений пока нельзя использовать дебаггер и выводить логи. Предложение [P0596] std::constexpr_trace and std::constexpr_assert [38] рассматривает введение специальных методов для этих целей.
Это предложение было благосклонно принято - ссылка на голосование [39], но пока еще не доработано.
На данный момент std::vector
, который желательно затащить в compile-time, не может работать в constexpr
-вычислениях, в основном из-за недоступности там операторов new/delete
.
Идея о допуске операторов new
и delete
в вычислитель констант на тот момент выглядела слишком амбициозно, поэтому в довольно странном предложении [P0597] std::constexpr_vector [40] рассматривается введение магического std::constexpr_vector<T>
.
Он является противоположносью std::vector<T>
- может быть создан и изменен только во время constexpr
-вычислений.
constexpr constexpr_vector<int> x; // Okay.
constexpr constexpr_vector<int> y{ 1, 2, 3 }; // Okay.
const constexpr_vector<int> xe; // Invalid: not constexpr
Как вычислитель констант должен работать с памятью - не описано. @antoshkka [41] и @ZaMaZaN4iK [42] (авторы многих предложений) в [P0639R0] Changing attack vector of the constexpr_vector [43] выявили большое количество минусов подхода, и предложили поменять направление работы в сторону абстрактного магического constexpr allocator
, который не дублирует всю стандартную библиотеку.
В презентации Constexpr ALL the thing! [44] демонстрировался пример constexpr-библиотеки для работы с JSON-объектами (то же самое, но в бумажном виде, есть в [P0810] constexpr in practice [45]):
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 [46] исследует возможность ввода STL-контейнеров в constexpr
-вычисления.
[Note: Важно знать, что такое аллокатор. STL-контейнеры работают через него с памятью. Какой именно аллокатор - задается через аргумент шаблона [47]. Чтобы войти в тему, можно почитать эту статью [48]. end note]
Что же мешает разрешить STL-контейнеры в constexpr
-вычислениях? Есть три проблемы:
Деструкторы не могут быть объявлены constexpr
(у constexpr
-объектов он обязан быть тривиальным).
Недоступна динамическая аллокация/деаллокация памяти.
Недоступен placement-new для вызова конструктора в аллоцированной памяти.
Первая проблема. С этим разобрались быстро - авторы предложения обсуждали проблему с разработчиками фронтенда MSVC++, GCC, Clang, EDG, и они подтвердили, что ограничение можно легко ослабить. Теперь от литеральных типов можно требовать наличие constexpr-деструктора, а не строго тривиального деструктора.
Вторая проблема. Работа с памятью не очень проста. Как уже упоминалось, вычислитель констант обязан отлавливать undefined behaviour в любом виде и останавливать компиляцию в случае его наличия.
Это значит, что вместе с многими объектами нужно трекать их "метаданные", которые держат руку на пульсе и не дают сломать выполнение программы. Пара примеров таких метаданных:
Информация, какое поле в union
-е активно ([P1330] [49]) (примеры undefined behavior: запись в член неактивного поля)
Жесткая связь между указателем или ссылкой и соответствующим ему реальным ранее созданным объектом (примеры undefined behavior: бесконечное множество)
Из-за этого не имеет смысла использовать подобные методы:
void* operator new(std::size_t);
Так как нет никакого обоснования приводить void*
к T*
. Короче говоря, новая ссылка/указатель либо могут начать указывать на существующий объект, либо быть созданным "одновременно" с ним, и никак иначе.
Поэтому есть две опции работы с памятью, которые будут законны в constexpr
-вычислениях:
Простые new- и delete-выражения: int* i = new int(42);
Использование стандартного аллокатора: std::allocator [50] (его подпилили напильником)
Третья проблема. Стандартные койтейнеры разделяют аллокацию памяти и конструирование объектов в этой памяти. С аллокацией уже разобрались - с условием на метаданные ее обеспечить возможно.
Контейнеры полагаются на std::allocator_traits [51], в частности для конструирования - на его метод construct [52]. До предложения он имел вид
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 запрещены). А в предложении преобразовался в
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 [53] - это метод, который в рантайме работает аналогично старому коду (с приведением к void*
), а в constexpr
-вычислениях он:
.∧_∧
( ・ω・。)つ━☆・*。
⊂ ノ ・゜+.
しーJ °。+ *´¨)
.· ´¸.·*´¨) ¸.·*¨)
(¸.·´ (¸.·'* ☆ Вжуххххх, и просто работает! ☆
То есть компиляторный вычислитель констант обработает его особым образом: по видимости, вызвав конструктор у объекта, который связан с T* p
, без бюрократии.
Этого достаточно, чтобы контейнеры стало возможно использовать в constexpr
-вычислениях.
На первых порах на аллоцированную память наложили ограничения: она должна быть деаллоцирована в рамках этого же constexpr
-вычисления, не выходя за рамки "песочницы".
Такой новый тип аллокации памяти назван transient constexpr allocations. Слово transient приблизительно можно перевести как "мимолетный" или "недолговечный".
В этом предложении еще был кусок про non-transient allocation - дать возможность освобождать не всю аллоцированную память, тогда неосвобожденная память "вываливается" из "песочницы" и конвертируется бы в static storage (другими словами, в секцию .rodata
). Но Комитет посчитал эту возможность "слишком хрупкой" ("too brittle") по многим причинам, и пока его не принял.
В остальном, это предложение было принято.
Предложение [P1002] Try-catch blocks in constexpr functions [54] вводит try-catch блоки в constexpr
-вычисления.
Это предложение немного сбивает с толку, так как throw
на тот момент был запрещен в constexpr
-вычислениях (значит, catch-кусок кода никогда не запускается).
Судя по документу, это ввели, чтобы пометить все методы std::vector
как constexpr
- в libc++ (реализация STL) в методе vector::insert
используется try-catch блок.
Из личного опыта знакомо, что двойственность constexpr
-методов (могут выполняться и в compile-time, и в run-time) приводит к тому, что вычисления проваливаются в рантайм там, где не ожидаешь: пример кода [55]; и для того, чтобы гарантировать нужный этап, нужно фокусничать: пример кода [56].
Предложение [P1073] constexpr! functions [57] вводит новое ключевое слово constexpr!
для методов, которые должны работать только в compile-time. Эти методы называются immediate-методами.
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
-методов), то программа не скомпилируется:
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 [55], пример с consteval [58].
В это время большое количество предложений состоит только из добавлений спецификатора constexpr
разнообразным частям стандартной библиотеки (которые мы не обсуждаем в этой статье, так как там один и тот же шаблон).
Предложение [P1235] Implicit constexpr [59] предлагает по умолчанию помечать все методы, имеющие определение, как constexpr
. Но можно и запретить выполнять метод в compile-time:
<нет спецификатора> - метод помечается как constexpr, если возможно.
constexpr
- работает как сейчас
constexpr(false)
- не может быть вызван в compile-time
constexpr(true)
- может быть вызван только в compile-time, т.е. аналогично constexpr!
/consteval
Это предложение не было принято - ссылка на голосование [60].
Как уже обсуждалось, после принятия предложения [P0784] Standard containers and constexpr [46] в constexpr
-вычислениях стало возможно аллоцировать память, но ее всю необходимо освобождать до окончания constexpr
-вычисления: это так называемые transient constexpr allocations.
Таким образом, нельзя создавать top-level constexpr
-объекты почти всех STL-контейнеров и многих других классов.
[Note: Под "top-level объектом" я имею в виду результат всего constexpr
-вычисления, например:
constexpr TFoo CalcFoo();
constexpr TFoo FooObj = CalcFoo();
Здесь вызов CalcFoo()
начинает constexpr
-вычисление, а FooObj
- его результат и top-level constexpr
-объект. end note]
Предложение [P1974] Non-transient constexpr allocation using propconst [61] находит путь к решению проблемы. На мой взгляд, это самое интересное предложение из всех, что я привел в статье (оно заслуживало бы отдельной статьи). Ему дали "зеленый свет" и оно развивается - ссылка на тикет [62]. Здесь я его перескажу в понятной форме.
Что же мешает нам иметь non-transient allocations? Вообще, проблема не в том, чтобы запихать куски памяти в static storage (.bss
/.rodata
/их аналоги), а в том, чтобы проверить, что вся схема обладает чёткой консистентностью.
Допустим, что мы имеем некий constexpr
-объект, конструирование (точнее, "вычисление") которого спровоцировало non-transient allocations. Значит, теоретическое деконструирование этого объекта (т.е. вызов его деструктора) должно освободить всю non-transient память. Если вызов деструктора не освободил бы память, то это плохо (консистентности нет) и надо выдавать ошибку компиляции.
Другими словами, вот что должен делать вычислитель контант:
Увидев запрос на constexpr
-вычисление, произвести его.
В результате вычисления получить объект, скрывающий под собой пачку constexpr
-переменных литерального типа; и некоторый объём неосвобожденной памяти (non-transient allocations).
Имитировать вызов деструктора у данного объекта (не вызывая его на самом деле), и проверить что этот вызов освободил бы всю non-transient память.
Если все проверки прошли успешно, то консистентность доказана. Non-transient allocations можно двигать в static storage.
Это звучит логично, и допустим, что это всё реализовано. Но тогда получим проблему с подобным кодом с non-transient памятью, которую стандарт не будет запрещать менять, и тогда проверка на вызов деструктора будет бессмысленной:
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, но это физическая константность. А в коде должна быть логическая константность. end note]
Пометка constexpr
у объектов влечет за собой пометку их как const
, и все их члены также становятся const
.
Однако если у объекта есть член указательного типа, то это ломает всю малину - заставить его указывать на другой объект станет нельзя, но менять объект, на который он указывает, не запрещено.
У указательных типов есть два ортогональных параметра константности:
Можно ли начать указывать на другой объект?
Можно ли изменять объект, на который указывается?
И получается 4 варианта с разными свойствами (OK
- строка компилируется, FAIL
- нет):
int dummy = 13;
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 [20] propconst
(propagating const).
Этот квалификатор будет использоваться с указательными/ссылочными типами:
T propconst*
T propconst&
И в зависимости от типа T
компилятор будет либо конвертировать это слово в const
, либо удалять его. Первый случай - если T
константный, второй - если нет:
int propconst * ---> int *
int propconst * const ---> int const * const
В предложении приведена таблица конвертации propconst
в разных случаях.
Таким образом, constexpr
-объекты могли бы обрести полную логическую константность (deep-const):
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>& ui = *uui;
ui.reset();
// ^^^ FAIL
const unique_ptr<int propconst>& ui = *uui;
// ui.reset();
// ^^^ OK
}
// P.S. Такая запись еще не принята Комитетом, я надеюсь, сделают лучше
С появлением полностью constexpr-классов, включая std::vector
, std::string
, std::unique_ptr
, в которых все методы методы помечены как constexpr
, возникает желание сказать "пометь все методы класса как constexpr
".
Это делает предложение [P2350] constexpr class [63]:
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 [64] идею предложить такую же штуку: ссылка на тикет [65] (что сейчас не нужно).
Многие почти одинаковые предложения в Стандарт могут появиться почти одновременно. Это говорит в пользу теории множественных открытий [66]: идеи "витают в воздухе", и в принципе не так важно, кто их открывает - если комьюнити достаточно большое, то естественная эволюция происходит.
constexpr
-вычисления могут быть очень медленными, потому что вычислитель констант на синтаксическом дереве развивался итеративным образом (начиная с constant folding) и сейчас делает множество лишних вещей, которые можно было делать более эффективно.
С 2019 года в Clang разрабывается ConstantInterpeter [67], который в перспективе может полностью заменить вычислитель констант на синтаксическом дереве. Он довольно интересен и заслуживал бы отдельной статьи.
Его идея состоит в том, что на основе синтаксического дерева можно сгенерировать "байткод", который затем выполнить на интерпретаторе. Интерпретатор поддерживает в себе стек, фреймы вызовов, модель памяти (с метаданными, о которых говорилось ранее).
Документация для ConstantInterpeter хорошая, и также много интересного есть в видео-выступлении [68] создателя интерпретатора на конференции LLVM-разработчиков.
Для того, чтобы больше расширить понимание, можно посмотреть замечательные выступления от экспертов. В каждом выступлении авторы выходят за рамки рассказа про constexpr: это может быть построение constexpr-библиотеки; рассказ про использование constexpr в будущем reflexpr [69]; или про устройство вычислителя констант и интерпретатора констант.
constexpr ALL the things! [44], Ben Deane & Jason Turner, C++Now 2017. Уже немного устарело, но может быть интересно, про построение constexpr-библиотеки.
Compile-time programming and reflection in C++20 and beyond [70], Louis Dionne, CppCon 2018. Много внимания уделяется будущей рефлексии в C++.
Полезный constexpr [71], Антон Полухин (@antoshkka [41]), C++ CoreHard Autumn 2018. Есть про компиляторы, рефлексию и метаклассы.
The clang constexpr interpreter [68], Nandor Licker, 2019 LLVM Developers’ Meeting. Рокет саенс и интерпретатор кода для constexpr
.
Я хотел бы, чтобы здесь было выступление про киллер-фичу (по моему мнению) [P1040] std::embed [72], которая отлично работала бы в тандеме с constexpr
. Но, судя по тикету [73], его планируют реализовать в C++-каком-то.
Автор: Евгений
Источник [74]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/368385
Ссылки в тексте:
[1] ссылка на код: https://godbolt.org/z/MYTbbsqvT
[2] Литералы: https://eel.is/c++draft/lex.literal
[3] .bss: https://en.wikipedia.org/wiki/.bss
[4] прошлой статьи: https://habr.com/ru/post/576052/
[5] эту статью: https://habr.com/ru/company/huawei/blog/511854/
[6] LLVM IR: https://llvm.org/docs/LangRef.html
[7] 1000 строк: https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/index.html
[8] Лексический анализ: 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
[9] Синтаксический анализ: 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
[10] старая документация: https://clang.llvm.org/docs/InternalsManual.html#constant-folding-in-the-clang-ast
[11] constant folding: https://en.wikipedia.org/wiki/Constant_folding
[12] lib/AST/ExprConstant.cpp: https://clang.llvm.org/doxygen/ExprConstant_8cpp_source.html#l04890
[13] EvaluateLoopBody: https://clang.llvm.org/doxygen/ExprConstant_8cpp.html#a1ef92016d7de585132f7a3e452f34afc
[14] open-std.org: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/
[15] stdcpp.ru: https://stdcpp.ru/
[16] [N1521] Generalized Constant Expressions: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n1521.pdf
[17] [N2235]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2235.pdf
[18] литеральные типы: https://en.cppreference.com/w/cpp/named_req/LiteralType
[19] storage class: https://en.cppreference.com/w/cpp/language/storage_duration
[20] type qualifier: https://en.cppreference.com/w/cpp/language/cv
[21] [N2349] Constant Expressions in the Standard Library: http://open-std.org/JTC1/SC22/WG21/docs/papers/2007/n2349.pdf
[22] [N2826] Issues with Constexpr: http://open-std.org/JTC1/SC22/WG21/docs/papers/2009/n2826.html
[23] [N3039] Constexpr functions with const reference parameters (a summary): http://open-std.org/JTC1/SC22/WG21/docs/papers/2010/n3039.pdf
[24] [N3268] static_assert and list-initialization in constexpr functions: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3268.htm
[25] [N3444] Relaxing syntactic constraints on constexpr functions: http://open-std.org/JTC1/SC22/WG21/docs/papers/2012/n3444.html
[26] [N3597] Relaxing constraints on constexpr functions: http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3597.html
[27] лайфтайм: http://eel.is/c++draft/basic.life
[28] [N3598] constexpr member functions and implicit const: http://open-std.org/JTC1/SC22/WG21/docs/papers/2013/n3598.html
[29] ABI: https://habr.com/ru/post/490222/
[30] mangled-имени: https://en.wikipedia.org/wiki/Name_mangling#C++
[31] [N4461] Static if resurrected: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4461.html
[32] [N4487] Constexpr Lambda: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4487.pdf
[33] статья про историю лямбд: https://habr.com/ru/company/otus/blog/444524/
[34] [P0595] The constexpr Operator: http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0595r0.html
[35] [P0595R2]: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0595r2.html
[36] [P1938] if consteval: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1938r0.html
[37] ссылка на голосование: https://github.com/cplusplus/papers/issues/677
[38] [P0596] std::constexpr_trace and std::constexpr_assert: http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0596r0.html
[39] ссылка на голосование: https://github.com/cplusplus/papers/issues/602
[40] [P0597] std::constexpr_vector: http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0597r0.html
[41] @antoshkka: https://www.pvsm.ru/users/antoshkka
[42] @ZaMaZaN4iK: https://www.pvsm.ru/users/zamazan4ik
[43] [P0639R0] Changing attack vector of the constexpr_vector: http://open-std.org/JTC1/SC22/WG21/docs/papers/2017/p0639r0.html
[44] Constexpr ALL the thing!: https://youtu.be/HMB9oXFobJc
[45] [P0810] constexpr in practice: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0810r0.pdf
[46] [P0784] Standard containers and constexpr: http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0784r7.html
[47] аргумент шаблона: https://en.cppreference.com/w/cpp/container/vector
[48] эту статью: https://habr.com/ru/post/505632/
[49] [P1330]: http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1330r0.pdf
[50] std::allocator: https://en.cppreference.com/w/cpp/memory/allocator
[51] std::allocator_traits: https://en.cppreference.com/w/cpp/memory/allocator_traits
[52] construct: https://en.cppreference.com/w/cpp/memory/allocator_traits/construct
[53] std::construct_at: https://en.cppreference.com/w/cpp/memory/construct_at
[54] [P1002] Try-catch blocks in constexpr functions: http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1002r1.pdf
[55] пример кода: https://godbolt.org/z/f8xY7T9xn
[56] пример кода: https://godbolt.org/z/9Prs41nhj
[57] [P1073] constexpr! functions: http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1073r0.html
[58] пример с consteval: https://godbolt.org/z/x6ds7vM8r
[59] [P1235] Implicit constexpr: http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1235r0.pdf
[60] ссылка на голосование: https://github.com/cplusplus/papers/issues/292
[61] [P1974] Non-transient constexpr allocation using propconst: http://open-std.org/JTC1/SC22/WG21/docs/papers/2020/p1974r0.pdf
[62] ссылка на тикет: https://github.com/cplusplus/papers/issues/867
[63] [P2350] constexpr class: http://open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2350r1.pdf
[64] stdcpp.ru: http://stdcpp.ru
[65] ссылка на тикет: https://github.com/cpp-ru/ideas/issues/479
[66] теории множественных открытий: 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
[67] ConstantInterpeter: https://clang.llvm.org/docs/ConstantInterpreter.html
[68] видео-выступлении: https://youtu.be/LgrgYD4aibg
[69] reflexpr: https://en.cppreference.com/w/cpp/experimental/reflect
[70] Compile-time programming and reflection in C++20 and beyond: https://youtu.be/CRDNPwXDVp0
[71] Полезный constexpr: https://youtu.be/MXEgTYDnfJU
[72] [P1040] std::embed: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1040r3.html
[73] тикету: https://github.com/cplusplus/papers/issues/28
[74] Источник: https://habr.com/ru/post/579490/?utm_source=habrahabr&utm_medium=rss&utm_campaign=579490
Нажмите здесь для печати.