Стандарт C++20 принес нам два новых ключевых слова: consteval и constinit. Ключевое слово consteval объявляет функцию, результат которой вычисляется на этапе компиляции, а ключевое слово constinit гарантирует, что переменная будет инициализирована на этапе компиляции.
Прочитав предоставленное выше описание ключевых слов consteval и constinit, у вас может сложиться впечатление, что их действия очень напоминают действие спецификатора constexpr. Поэтому, прежде чем сравнивать между собой работу спецификаторов consteval, constinit, constexpr и старого доброго const, мы пройдемся по новым спецификаторам consteval и constinit.
Спецификатор consteval
Спецификатор consteval определяет так называемую безотлагательную функцию. Каждый вызов безотлагательной функции создает константу времени компиляции. Говоря простыми словами, consteval-функция выполняется во время компиляции.
1 2 3 |
consteval int sqr(int n) { return n * n; } |
Спецификатор consteval не может быть применен к деструкторам или функциям, которые выделяют/освобождают память. В объявлении функции допускается использование не более одного спецификатора consteval, constexpr или constinit. Безотлагательная функция неявно является встроенной функцией и должна удовлетворять требованиям constexpr-функции.
В C++14 есть свои требования к constexpr-функциям и, следовательно, consteval-функциям.
constexpr-функция может:
иметь инструкции условного перехода или инструкции цикла;
иметь больше, чем одну инструкцию;
вызывать constexpr-функции. В то же время consteval-функция может вызывать только constexpr-функцию, но не наоборот;
иметь фундаментальные типы данных, которые должны быть инициализированы с помощью константного значения.
сonstexpr-функции не могут иметь статических или thread_local (локальных для потока) данных. Также они не могут иметь ни блока try, ни оператора goto.
Ниже представлен пример использования consteval-функции sqr():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// constevalSqr.cpp #include <iostream> consteval int sqr(int n) { return n * n; } int main() { std::cout << "sqr(5): " << sqr(5) << std::endl; // (1) const int a = 5; // (2) std::cout << "sqr(a): " << sqr(a) << std::endl; int b = 5; // (3) // std::cout << "sqr(b): " << sqr(b) << std::endl; Ошибка } |
Результат выполнения программы:
sqr(5): 25
sqr(a): 25
Число 5
является константным значением и может использоваться в качестве аргумента для функции sqr() (вариант №1).
То же самое справедливо и для переменной a
(вариант №2). Константная переменная, такая как a
, может использоваться в константном выражении, когда она инициализируется константным значением.
Переменная b
(вариант №3) не является константной переменной. Следовательно, вызов функции sqr(b)
не является допустимым.
Спецификатор constinit
Спецификатор constinit может быть применен к переменным, имеющим статическую продолжительность жизни или продолжительность жизни потока:
Глобальные переменные, статические переменные или статические члены класса имеют статическую продолжительность жизни. Эти объекты создаются при запуске программы и освобождаются при её завершении.
Переменные типа thread_local имеют продолжительность жизни потока, т.е. создаются (в начале своего использования) для каждого потока, работающего с этими данными, и принадлежат исключительно потоку.
Спецификатор constinit гарантирует, что вышеперечисленные переменные будут инициализированы во время компиляции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// constinitSqr.cpp #include <iostream> consteval int sqr(int n) { return n * n; } constexpr auto res1 = sqr(5); constinit auto res2 = sqr(5); int main() { std::cout << "sqr(5): " << res1 << std::endl; std::cout << "sqr(5): " << res2 << std::endl; constinit thread_local auto res3 = sqr(5); std::cout << "sqr(5): " << res3 << std::endl; } |
Результат выполнения программы:
sqr(5): 25
sqr(5): 25
sqr(5): 25
Переменные res1
и res2
имеют статическую продолжительность жизни. Переменная res3
имеет продолжительность жизни потока.
Выполнение функций
Следующая программа имеет 3 версии функции возведения числа в квадрат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// consteval.cpp #include <iostream> int sqrRunTime(int n) { return n * n; } consteval int sqrCompileTime(int n) { return n * n; } constexpr int sqrRunOrCompileTime(int n) { return n * n; } int main() { // constexpr int prod1 = sqrRunTime(100); Ошибка №1 constexpr int prod2 = sqrCompileTime(100); constexpr int prod3 = sqrRunOrCompileTime(100); int x = 100; int prod4 = sqrRunTime(x); // int prod5 = sqrCompileTime(x); Ошибка №2 int prod6 = sqrRunOrCompileTime(x); } |
Рассмотрим вышеприведенные функции:
sqrRunTime() — это обычная функция, работающая во время выполнения программы;
consteval-функция sqrCompileTime() выполняется во время компиляции;
constexpr-функция sqrRunOrCompileTime() может работать во время компиляции или во время выполнения программы.
Запрос результата во время компиляции с помощью sqrRunTime() (ошибка №1) является ошибкой, как и использование неконстантного значения в качестве аргумента для sqrCompileTime() (ошибка №2).
Разница между constexpr-функцией sqrRunOrCompileTime() и consteval-функцией sqrCompileTime() заключается в том, что sqrRunOrCompileTime() будет выполняться во время компиляции только тогда, когда этого требует контекст:
1 2 3 4 5 6 7 8 9 10 |
static_assert(sqrRunOrCompileTime(100) == 100); // compile-time (1) int arrayNewWithConstExpressioFunction[sqrRunOrCompileTime(100)]; // compile-time (1) constexpr int prod = sqrRunOrCompileTime(100); // compile-time (1) int a = 100; int runTime = sqrRunOrCompileTime(a); // run-time (2) int runTimeOrCompiletime = sqrRunOrCompileTime(100); // run-time или compile-time (3) int allwaysCompileTime = sqrCompileTime(100); // compile-time (4) |
Первые три строки (1) требуют вычислений во время компиляции. Строка (2) может быть вычислена только во время выполнения программы, поскольку переменная a
не является константой. Наиболее важной для нас является строка (3) — функция может быть выполнена во время компиляции или во время выполнения программы. Решение о времени выполнения зависит от компилятора и применяемого уровня оптимизации. Это замечание не относится к строке (4) — consteval-функция всегда выполняется во время компиляции.
Инициализация переменных
В следующей программе происходит сравнение ключевых слов const, constexpr и constinit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// constexprConstinit.cpp #include <iostream> constexpr int constexprVal = 1000; constinit int constinitVal = 1000; int incrementMe(int val){ return ++val;} int main() { auto val = 1000; const auto res = incrementMe(val); // (1) std::cout << "res: " << res << std::endl; // std::cout << "res: " << ++res << std::endl; Ошибка (2) // std::cout << "++constexprVal++: " << ++constexprVal << std::endl; Ошибка (2) std::cout << "++constinitVal++: " << ++constinitVal << std::endl; // (3) constexpr auto localConstexpr = 1000; // (4) // constinit auto localConstinit = 1000; Ошибка } |
Результат выполнения программы:
res: 1001
++constinitVal++: 1001
Во время выполнения программы инициализируется только const-переменная (1). constexpr- и constinit-переменные инициализируются во время компиляции.
constinit (3) не подразумевает константности как const (2) или constexpr(2). Переменная, объявленная как constexpr (4) или const (1), может быть локальной, а переменная, объявленная как constinit — нет.
>constinit не подразумевает константности
Интересно, а почему она тогда так называется?
В данном случае подразумевается, что переменная будет статически инициализирована постоянным значением.