- Это про compile-time вычисления
template <int N, int Mod>
struct Factorial {
const static int value = N * Factorial<N - 1, Mod>::value % Mod;
};
template <int Mod>
struct Factorial<1, Mod> {
const static int value = 1;
};
static_assert(Factorial<1, 1>::value == 1);
int main() {
return 0;
}Вычисления на шаблонах - тьюринг полны. Но есть некоторые недостатки:
- Можем использовать ограниченний функционал: рекурсия, ифы
- Не можем нормально эмулировать циклы - ограничены глубиной рекурсии
- Код не интуитивный
constexpr int factorial(int n, int mod) {
return n == 0 ? 1 : factorial(n - 1, mod) * n % mod;
}
static_assert(factorial(1, 1) == 1);- Добавили к функции модификатор
constexpr- Для функций - лишь указание, что ф-ция мб вычислена в compile-time
- Для переменный - объявляем compile-time
- В C++11 это уже работает. Но можно писать только
returnчто-то. Строчка может быть сколь угодно длинная, но должна быть всего одна - В C++11 это работало как синтаксический сахар - все превращалось в шаблоны
// В C++11 constexpr функция могла быть только одна строчка - с return
constexpr int foo1() {
return 1;
}Начиная с C++14 механизм constexpr-функций сильно расширили. Теперь факториал можно считать вот так:
// В C++14 появился процесс "constant evaluation" - исполняется интерпретатором
constexpr int factorial(int n, int mod) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
result %= mod;
}
return result;
}-
Грубо говоря: завезли интерпретатор внутрь компилятора
-
Этот процесс называется constant evaluation - отдельный этап компиляции, при котором запускается интерпретатор и отрабатывает на
constexpr-функциях- Прямо как asan (address sanitizer)
- В compile-time мы видим весь flow наших функций. В runtime - нет.
-
Понятно, что нельзя использовать RT-объекты:
std::cout
constexpr int foo2() {
if (x > 2) {
return x * x;
}
std::cout << x;
return 1;
}-
При этом
foo2(10)- OK - вычисляется интерпретатором сверху-вниз -
foo2(0)- CE -
С C++20 можно писать внутри конструкции с
tryиasm -
С C++23 вроде можно писать RT-инструкции - компилятор будет игнорить
- Все константы можно разделить на 2 категории:
- Константы времени компиляции
- Константы времени исполнения
#include <iostream>
int main() {
const int a = 5;
std::array<int, a> arr; // OK
int temp;
std::cin >> temp;
const int b = temp;
std::array<int, b> arr2; // CE
}На этапе компиляции известны следующие вещи:
- Литералы (
1,1.0,1ull,'c',"char") - Параметры шаблонов
- Результат
sizeof constexpr-переменные
Про последнее стоит отдельно поговорить
int main() {
constexpr int x = 2 * 2;
std::array<int, x> arr; // OK
}-
На
constexprпеременные накладывается следующее ограничение:constexprпеременная должна иметь литеральный тип -
Литеральный тип - это все базовые типы + некоторые другие, про которые поговорим позже
-
Note С C++20 даже
vectorявляется литеральным типом, но про это попозже
constexpr int arr[] = {1, 2, 3};
constexpr int* x = &arr[3];Непонятно: к чему в данном случае относится constexpr:
constexpr int* x->const int* xconstexpr int* x->int* const
constexpr int arr[] = {1, 2, 3};
constexpr const int* x = &arr[3]; // добавили constconstexpr int* x->const int* xconstexpr int* x->int* const- правильный ответ
Вывод: constexpr не является частью типа, это аннотация (спецификатор)
- То есть
constexprне меняет тип объекта. Например,constexpr int x = 10;все еще является переменной типаint, но с дополнительным свойством возможности компиляции во время компиляции
- Есть перечисление того, что можно делать в
constexprфункциях constexpr-функции можно вызывать в runtime. Тогда они просто работают как обычные функции.- Если функция используется в compile-time контексте, то ее вызов будет в compile-time. В любых других случаях гарантии не дается.
- Note
constexprметоды тоже можно (и во многих случаях даже нужно) объявлять. Про это будет позже
- const evaluation выполняется интерпретатором => известен весь flow функции => почему бы не вызывать
throw()? catchзапрещен- Но все равно хороший способ ловить ошибки
constexpr size_t Devide(size_t x, size_t y) {
if (y == 0) {
throw "y == 0";
}
return x / y;
}error: expression ‘<throw-expression>’ is not a constant expression
#include <iostream>
struct DivisionByZero {};
constexpr int foo(int x, int y) {
if (y == 0) {
throw DivisionByZero{};
}
return x / y;
}
static_assert(foo(3, 3) == 1);
static_assert(foo(3, 0) == 1); // CE: DivisionByZero is not a constant expression#include <iostream>
struct CheckIsNotZero {
consteval CheckIsNotZero(int y) : y(y) {
Check();
}
consteval void Check() {
if (y == 0) {
throw DevisionByZero{};
}
}
int y = y;
};
int bar(int x, CheckIsNotZero checked) {
std::cout << x << '\n';
return x / checked.y;
}
int main() {
bar(3, 0);
}- CT =
constexpr-context
constexpr int foo(int x) {
return x * x;
}
static_assert(foo(2) == 4); // CT
int main() {
constexpr int x = 10; // CT
std::size_t y = 10; // RT / CT
std::cin >> y;
int z = foo(y);
int t = foo(10);
return 0;
}- Переменная
xв листинге выше по идее лежит на стеке, однако стек - RT - На самом деле, как правило он просто подставляет
#include <iostream>
constexpr int foo(int x) { return x * x; }
int main() {
constexpr int x = foo(4);
std::cout << x << '\n';
}- Здесь будет тупая подстановка
16 - Если брать адрес
&x, то это будет адрес ROW-DATA
int foo(int N) { std::cout << N; return 0; }
constexpr int bar(int N) {
return N < 10 ? 9 : foo(N);
}-
Это
constepxr-функция приN < 10: путь для компилятора установлен корректно, невзирая на нечистотуconstexpr-выражения -
Продолжаем разговор
// continue
template <int N>
void F()
requires check<N>
{};
template <int N>
void F() {}
int main() {
F<5>(); // OK
F<100>(); // CE
}- Продолжаем разговор
// continue
template <int N>
constexpr bool IsValid = N >= 10 || check<N>;
template <int N>
void G() requires IsValid<N> {};
G<100>(); // CE все равно: невзирая на ленивость вычислений, компилятор все равно будет пытаться инстанцировать check<N=100> (вдруг там переопределен operator||).- Продолжаем разговор
// continue
template <int N>
constexpr bool IsValid = check<N>;
template <int N>
void G() requires N >= 10 || IsValid<N> {};
G<100>(); // OK-
Наконец, вводим понятие литерального типа
-
Def Тип является литеральным если:
- У него есть
constexprдеструктор - У него есть
constexprконструктор (причем это неcopy- и неmove- конструктор)
- У него есть
class Point {
public:
constexpr Point(double x, double y) : x_(x), y_(y) {}
constexpr double getX() const { return x_; }
constexpr double getY() const { return y_; }
public:
double x_;
double y_;
};
int main() {
constexpr Point p(10.5, 20.5);
static_assert(p.getX() == 10.5, "X coordinate should be 10.5");
static_assert(p.getY() == 20.5, "Y coordinate should be 20.5");
}- Вектор с C++20 является литеральным типом. А значит, в compile-time как-то поддержали аллокацию.
// При этом в global scope или в main можно создать только пустой вектор
constexpr std::vector<int> vec1; // OK
constexpr std::vector<int> vec2 = {1, 2, 3}; // CE- Действительно, в C++20 разрешили выделять память в compile-time (
new,delete) - Требования следующие: вы обязаны деаллоцировать всю память после выхода из главной внешней функции. Пример:
constexpr int* GetInt(int value) {
return new int(value);
}
constexpr int foo() {
auto new_value = GetInt(10);
delete new_value;
return 10;
}
constexpr int test = foo();constexpr int bar() {
int* ptr = new int[10];
for (std::size_t i = 0; i < 10; ++i) {
ptr[i] = i;
}
std::site_t res = 0;
for (std::size_t i = 0; i < 10; ++i) {
res += ptr[i];
}
delete[] ptr;
return res;
}
static_assert(bar() == 45);
int main() {
constexpr int x = foo(10);
}- Начиная с C++20 в compile-time можно использовать виртуальные функции (но не виртуальное наследование)
struct Base {
constexpr Base() = default;
constexpr virtual ~Base() = default;
constexpr virtual int foo() { return 1; }
};
struct Derived: public Base {
constexpr Derived() = default;
constexpr ~Derived() override = default;
constexpr int foo() override { return 2; }
};
constexpr int foo() {
Derived d;
Base& b = d;
return b.foo();
}
static_assert(foo() == 2);constexpr-функция предполагает, что она может вычислиться в runtime. Поэтому был придуман функционал для явного указания требования на вычисление в compile-time.- Чтобы гарантировать, что функция вызовется именно в compile-time, нужно явно сказать компилятору об этом: функции, помеченные
consteval, обязаны выполнятся только на этапе компиляции
consteval int foo(int n) { return n * n; }
int main() {
constexpr int y = foo(10); // OK
int x = 10;
int xx = foo(x); // CE
int xxx = foo(10); // OK - CT-контекст виден
}// Функция **обязана** быть вычислена в compile-time
consteval int f(int x) {
return x + x;
}- Настолько круто, что ее не существует в RT (т.е. в коде программы) -> у нее даже нельзя взять адрес
- С
constinitвсе сложней: этот модификатор указывает, что переменная должна быть инициализирована на этапе компиляции. При этом эту переменную можно менять.
The
constinitspecifier declares a variable with static or thread storage duration.
- То есть можно объявлять только статические и глобальные переменные.
#include <iostream>
#include <string>
struct LoggerConfig {
int logLevel;
std::string logPath;
};
// constinit указывает, что инициализация должна произойти на этапе старта программы
constinit LoggerConfig globalLoggerConfig{3, "/var/log/myapp.log"};
int main() {
// при запуске программы globalLoggerConfig уже инициализирован
std::cout << "Log Level: " << globalLoggerConfig.logLevel << std::endl;
std::cout << "Log Path: " << globalLoggerConfig.logPath << std::endl;
// так как это constinit, мы можем изменять значения после инициализации
globalLoggerConfig.logLevel = 4; // Допустимо
std::cout << "Updated Log Level: " << globalLoggerConfig.logLevel << std::endl;
// однако, следующий код не скомпилируется, если globalLoggerConfig был объявлен как constexpr
// constexpr LoggerConfig testConfig{1, "/test.log"};
// testConfig.logLevel = 2; // ошибка компиляции, т.к. constexpr не допускает изменений после инициализации
return 0;
}constexpr int foo(int x) { return x * x; }
// Можно только глобальные и static
constinit int x = foo(100); // Обязана честно инициализироваться
int main() {
constinit int x = foo(100); // CE: Запрещено использовать во фреймах функций
}- Рассмотрим пример
template <typename T>
consteval size_t foo(std::initializer_list<T> init) {
constexpr size_t init_size = init.size(); // CE
return init_size;
}
constexpr size_t n = foo({1, 2, 3}); // CE-
CE, потому что переменная
initне являетсяconstexpr(и не может, так как это параметр функции) -
Однако если убрать
constexpr, все заработает
template <typename T>
consteval size_t foo(std::initializer_list<T> init) {
size_t init_size = init.size();
init_size += 1; // Даже можно менять
return init_size;
}
constexpr size_t n = foo({1, 2, 3}); // OK- То есть переменная
init_sizeявляетсяconstexpr, но правилами языка запрещено объявлять её таковой. Более того, мы получилиconstexpr-переменную, которую можно менять
int foo(int a, int b) {
constexpr int z = blabla(); // тут рекурсивно запуститься новый интерпретатор => своя память должна быть очищена на этой же строчке
int y = 10;
if (y + a > 10) {
bar(x) {
baz() {
}
}
}
// delete всего должен быть здесь (при выходе из всей "пещеры", по завершении работы интерпретатора)
}
constexpr int x = foo(2, 3) /* на этом моменте начинается const evaluation - здесь стартует интерпретатор */;- Повышаем градус
foo(constexpr int& x) { // ссылка константная => side-эффекта не будет
}
// может вызываться только для того, что создано интерпретатором
constexpr foo(int& x) {
++x;
}
constexpr bar() {
int x = 0;
foo(x);
}-
По идее
constexpr-функции кэшируются (ведь при одинаковых аргументах => одинаковое возращаемое значение) -
Способ обойти кэширование - наши любимые лямбды!
template <auto V = [](){}>
constexpr bar() { ... }#include <iostream>
const int x = 10; // могут использоваться как CT - интегральный тип
constexpr int y = x + 3; // вообще CT
static_assert(y == 13);
consteval int foo(std::vector<int> vec) {
// constexpr int sz = vec.size(); // CE - vec is not constant expression (должно быть проинициализировано константным выражением)
std::size_t sz = vec.size(); // OK
return sz + 3;
}
constexpr int enter(int x) {
std::vector<int> v(x, 0);
return foo(v);
}
static_assert(enter(10) == 13);
- То, что известно в compile-time, и то, что
constexpr, - это разное
template <std::size_t I>
struct Predicate {
constexpr bool operator()(int x) { return x > I; }
};
template <typename Pred>
constexpr auto call(Pred pred, int x) {
// it's not a constant expression (pred is not an constant)
if constexpr (pred(x)) {
return 10;
} else {
return 2.2; // Это нормально - constexpr же???
}
}
constexpr auto x = call(Predicate<10>{}, 0); // CE
int main() {
std::cout << x;
}- Улучшим
std::advanceдля итераторов
#include <iterator>
template <typename Iterator>
void my_advance(Iterator& iter, int n) {
if constexpr (std::is_same_v<typename std::iterator_traits<Iterator>::iterator_category, std::random_access_iterator_tag>) {
iter += n;
} else {
for (int i = 0; i < n; ++i) {
++iter;
}
}
}