Статическая и динамическая инициализация
Известно, как можно ускорить свою программу до предела - считать что-то нужное аж на этапе компиляции. Рассмотрим следующий пример:
int f() {
int result;
std::cin >> result;
return result;
}
int a = 42;
int b = f();
Ничто не мешает переменной a
быть подсчитанной на этапе компиляции, поскольку мы знаем, чему равно выражение справа. С другой стороны, переменная b
не может быть посчитана на этапе компиляции по очевидным причинам - ей нужен пользовательский ввод.
Говорят, что переменная a
инициализируется статически (на этапе компиляции), а переменная b
- динамически, в runtime.
Динамическая инициализация глобальных переменных происходит в момент запуска программы до входа в функцию main
, а разрушаются они после выхода из main
, при этом имеются недостатки:
- При запуске программы тратится время на инициализацию
- В одной единице трансляции переменные инициализируются сверху вниз, а между разными порядок не гарантирован. Это становится проблемой, когда инициализация переменной в одной единице трансляции обращается к переменной в другой, а та ещё не инициализирована.
- Исключение при динамической инициализации глобальной переменной аварийно завершает программу, так как до входа в
main
его негде поймать.
Спецификатор constexpr
Спецификатор constexpr
можно приписывать к возвращаемому типу функций, методов, а также к переменным.
constexpr T f(...) {
/* ... */
}
constexpr T name = expr;
Для переменной это значит, что выражение справа обязано уметь считаться на этапе компиляции, иначе получим ошибку компиляции.
Для функции же это значит, что она может считаться как на этапе компиляции, так и в runtime (не будет выведена ошибка компиляции), если какой-то из переданных параметров не может быть посчитан в compile-time.
В constexpr
функции нельзя использовать выражения, не являющиеся константой времени компиляции, например, использование std::cin
или вызов не constexpr
-функции.
Простой пример использования ниже:
int sum(int a, int b) {
return a + b;
}
constexpr int c_sum(int a, int b) {
return a + b;
}
constexpr int a1 = c_sum(5, 12); // посчитается при компиляции
constexpr int a2 = sum(5, 12); // ошибка: sum() не является constexpr выражением
int a3 = c_sum(5, 12); // будет посчитано в runtime
int a4 = sum(5, 12); // так же
Сейчас constexpr
позволяется очень многое, и компилятору нужно тщательно следить за тем, чтобы не допустить UB. К примеру, он подскажет о переполнении чисел во время подсчета или делении на ноль, а также о пределе вложенности вызовов.
error: constexpr variable 'foo' must be initialized by a constant expression
constexpr int foo = 13 + 2147483647;
^ ~~~~~~~~~~~~~~~
С++ понемногу становился интерпретируемым языком, если можно так сказать. Но сначала ограничений было много и программировать можно было только в стиле, похожем на функциональный.
Вот так можно было считать факториал в compile-time вместо шаблонов.
constexpr int factorial(int n) {
return n == 0 ? 1 : n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
Затем добавили возможность использовать константные ссылки (принимать и возвращать), if
, typedef
, using
и static_assert
:
template <typename T>
constexpr T self(const T& a) {
return *(&a);
}
template <typename T>
constexpr const T& self_ref(const T& a) {
return *(&a);
}
constexpr auto a1 = self(123); // OK
constexpr auto a2 = self_ref(123); // OK
Также разрешили объявлять типы (class
, enum
и т.д.) и возвращать void
, а еще создавать инстансы классов с constexpr
-конструктором, имеющих также тривиальный деструктор.
Главной проблемой оставался запрет на изменение какой-либо переменной в контексте constexpr
-функций. Таким образом мы не имели for
и while
. Но вскоре и это разрешили делать, но с небольшой ремаркой.
Изменять объект в рамках вычисления constexpr
-выражения можно только если он был создан в процессе вычисления этого выражения:
constexpr int f(int a) {
int n = a;
++n;
return n * a;
}
int k = f(4); // OK
// переменная 'n' внутри 'f' может быть
// модифицирована, так как она была создана
// во время вычисления выражения
constexpr int k2 = ++k; // ошибка.
// нельзя модифицировать переменную k, потому
// что она была создана до начала вычисления
// выражения
Позднее разрешили делать constexpr
-лямбды.
Начиная с С++20 в constexpr
-контексте можно использовать std::vector
и некоторые его методы, например, push_back
, operator[]
или size
.
В будущем могут разрешить и другие STL контейнеры (да прибудет с вами constexpr
-аллокатор), но с ремаркой, что такую память нужно освобождать по завершению вычисления constexpr
-выражения. Впрочем, есть предложения уметь не просто освобождать ее, а, например, еще возвращать (non-transient constexpr allocations using propconst).
Таким образом мы движемся к интерпретатору на уровне компилятора.
Бонус: подсчет факториала в compile-time с сохранением результатов для использования в runtime. Как написать это на шаблонах мне не известно.
#include <iostream>
#include <array>
template <size_t N>
constexpr std::array<int, N> factorials() {
std::array<int, N> a{1};
for (size_t i = 1; i < N; ++i) {
a[i] = a[i - 1] * i;
}
return a;
}
int main() {
constexpr auto f = factorials<10>();
for (int i : f) {
std::cout << i << std::endl;
}
}
Спецификатор consteval (C++20)
Как проверить, что значение constexpr
-функции точно будет вычислено в compile-time?
Можно присвоить ее в constexpr
-переменную, потому что, как мы знаем, выражение справа от такой переменной обязано быть constexpr
.
Но есть и более прямой способ сказать, что функция может вызываться только и только на этапе компиляции. Для этого ввели спецификатор consteval
.
consteval int sqr(int n) {
return n*n;
}
constexpr int r = sqr(100); // OK
int x = 100; // non-constexpr variable
int r2 = sqr(x); // error: x is not usable in a constant expression
if constexpr (C++17)
Позднее появился особенный вариант для if
, который позволил разгрузить страшный шаблонный код, который пишут, используя SFINAE
.
Конструкция if constexpr
требует, чтобы проверяемое условие было constexpr
выражением, и делает бранчинг на этапе компиляции, опуская из кода ветку, которая не подходит условию. Таким образом данный код будет компилироваться.
struct A {
static constexpr bool has_foo = true;
void foo();
};
struct B {
static constexpr bool has_foo = false;
void bar();
};
template <typename T>
void f(T obj) {
if constexpr (T::has_foo) {
obj.foo();
} else {
obj.bar();
}
}
Очевидно, что с использованием просто if
произойдет ошибка компиляции. Класс A не имеет метода bar
, а класс B не имеет метода foo
.
Еще раз стоит отметить, что данная конструкция не является заменой обычному if
, она принимает только constexpr
-выражения.
Пример кода, который не скомпилируется:
constexpr bool is_even(int a) {
if constexpr (a & 1) {
return false;
} else {
return true;
}
}
Заимствования:
Дизайн и эволюция constexpr в C++ / Хабр (habr.com)
Спецификатор constexpr в C++11 и в C++14 / Хабр (habr.com)
cpp-notes/22_constexpr.md at master · lejabque/cpp-notes (github.com)