Что нужно знать о constexpr
На мое удивление, в большинстве мест, где я читал о constexpr, упоминаются самые очевидные и не интересные вещи. Например о том, какие ограничения существуют для constexpr функций, хотя по факту достаточно лишь сказать, что constexpr функция должна состоять из одного return стейтмента. А вот что на мой взгляд действительно важно знать.
DISCLARIMER: В посте вы часто встретите употребление английских слов и определений вместо русских, на мой взгляд так значительно удобнее для любого программиста, нежели маяться в попытках понять, что я имел ввиду под каким-нибудь дурацким переводом.
Место в грамматике
Любезный clang ясно дает понять, тип указателя p — int *const, вместо ожидаемого const int*. Так вот, собственно первый вывод: constexpr применяется к declaration в целом и не является частью типа. Если constexpr с указателями в общем-то редко используется, знать этот факт вдвойне важно при определении функций. Дело в том, что constexpr не является частью типа возвращаемого значения функции как это было бы в случае с const. Приведу опять пример с указателем и функциями, где этот факт четко иллюстрирован:
Вполне компилируется и работает. Согласен, что пример не особо полезен на практике, но позже я покажу почему важно это все знать. Бывают реально случаи, когда указатели используются в constexpr функциях. А пока давайте суммируем наконец то, как constexpr связан с const:
- При объявлении переменных, constexpr применяет const к самой переменной (а не к типу на который она указывает, если это указатель).
- При объявлении функций, constexpr не влияет на тип функции. В ход, однако, вступают ограничения, о которых все так любят писать.
- При объявлении функций-членов (они же методы), constexpr объявляет данную функцию-член константной. Не влияет на тип возвращаемого значения.
- При объявлении конструктора, constexpr как в случае с обычной функцией ничего не меняет, только включает ограничения.
Надеюсь, все выше сказанное помогло в какой-то мере понять каким образом constexpr применяется к сущностям языка и устронило недопомнимание на тему const vs constexpr.
Когда применять constexpr
Если коротко, то constexpr следует применять везде, где можно. И если достаточно понятно, когда применять constexpr к переменным и обычным функциям, то применение в классах требует небольшого пояснения. Рассмотрим пример:
1. С конструкторами я думаю понятно. Если у конструктора пустое тело и есть возможность сделать его constexpr — делайте.
2. Default и delete члены тоже можно объявлять как constexpr. Вполне возможно, что они становятся constexpr при возможности по умолчанию, но для целей самодокументирования никогда не повредит явно указать это.
3. Оператор присвоения обычно не определяется как constexpr, потому что традицонно он возвращает не константную ссылку на *this. А как мы знаем из первой секции этого поста, constexpr делает функцию-член константной (this становится const). Просто не уместно, да и область применения сомнительна, хотя вполне возможно и этот оператор сделать constexpr.
4. Если кто-то еще не понял, объявление constexpr float x() внутри класса эквивалентно float x() const , а поэтому везде где нам хотелось бы определить константную функцию-член и где есть возможность сделать ее constexpr функцией, надо делать.
5. Не забываем важную информацию из первой секции поста — constexpr не влияет на тип возвращаемого значения функции, а поэтому constexpr const сочетание очень даже имеет смысл. Кроме того, т.к. constexpr неявно делает функцию-член константной, мы не можем вернуть float* используя const float _v[3].
6. Еще раз, учитывая, что (5) является константной функией-членом, можно сделать перегрузку для не константного случая, как это традиционно делается в С++ для акцессоров возвращающих указатель или ссылку. Так что все тут вполне корректно.
C++ constexpr makes compile-time programming a breeze
Constant expression, constexpr , code in C++ aims to move non-changing repetitive computations at runtime to compile time. For example, you can write a function that calculates π² at compile time, so, whenever you run the program, π² value is already there. constexpr functions are also allowed to be called at runtime.
Prerequisites
Here I assume you are familiar with the concept of compile-time vs runtime, auto keyword, and templates.
I am using GCC 10.2 and Clang 14 with flag -std=c++20 .
constexpr values
We can store a compile-time value with constexpr :
Note that if you drop constexpr the variable is set at runtime and is not suitable for compile-time programming.
Therefore, something like this is accepted:
but this is NOT accepted:
So you can use constexpr values at runtime but you cannot use runtime variables at compile time.
constexpr variable neither can be changed at compile-time nor runtime:
To set a constexpr variable based on some logic, a constexpr function is employed. It is explained in the constexpr function section.
And probably you already noticed that literals like 1 , 2.5 and «hello» can be used in constant expressions.
To store a text, use const char[] like this:
And if you need a string, a runtime one can be made:
The memory usage of arrays are known at compile time, so, they are accepted as constexpr :
const vs constexpr
const and constexpr variables are immutable. constexpr variables are initialized at compile-time, however, initialization of const variables will be delayed to runtime if a compiler decides so.
This is an error:
However, this is successfully compiled with GCC 10.2:
Therefore, for clarity and avoiding bugs, instead of const use constexpr variables for compile-time programming.
constexpr and template
Like literals, constexpr variables can be used to instantiate a template function or class.
In the above example, drop constexpr to see what error you get. constexpr along with Concepts are powerful tools for meta-programming.
constexpr class
The object of a class can participate in constexpr statement if it has a constexpr constructor:
If a member function needs to be used in constexpr statement, it must be constexpr too:
Note, in the member function definition, the qualifier of this must be const .
And finally, a constexpr data member must be static .
if constexpr
To decide a condition at compile, if constexpr is used:
Do not miss constexpr for else if .
The conditions must be evaluated from a constant expression. Therefore, this is correct
But this NOT OK:
The final compiled program doesn’t include the sections of unmet conditions:
The above code compiles as if we had
It can be a powerful tool for eliminating a part of the code that must not be included at runtime.
constexpr function
We can have a function that is evaluated at compile-time:
and somewhere else you can write:
You can have more logic evaluated at compile-time, like calculating Pi number:
However, a constexpr function turns to a runtime (non-constexpr) function if arguments are not constexpr :
and you get an error if you do this:
Note that when a constexpr variable is initialized with a constexpr function, we force the function to return a constexpr . If it doesn’t so, we get an error.
constexpr function parameter
Currently, there is no support for a constexpr function parameter:
However, there is a proposal for this feature, see P1045.
Therefore, for now, the only choice is to use a template:
constexpr vector and string
std::vector and std::string objects have a dynamic memory allocation, they cannot be persistent at both compile and runtime. There is no support for them before C++20, however, since then, these objects are allowed to be in places like constexpr functions, so they are destructed at the end of compile time.
You get errors doing this:
However this is correct and supported in GCC 12 with flag -std=c++20 :
consteval vs constexpr function
consteval function mandates the function returns a compile-time constant but constexpr function, besides compile-time, can also be called at runtime.
However, a constexpr function can be called at both compile-time and runtime:
Therefore, if the function must be evaluated at compile-time only, use consteval and you get an error from the compiler if otherwise happens. However, if you already have a function in your code that is used at runtime, now you can also employ it at compile time by just adding constexpr specifier to it.
constexpr vs static constexpr
At the global scope, both are the same and static keyword is not necessary. However, in a function scope, their behavior is compiler-dependent.
In this example:
GCC copies the array to the stack every time the function is called. But Clang creates a table of the array in the compiled program without the need for copying into the stack.
If you change constexpr to static constexpr , both GCC and Clang create a table of data which is mostly desired. Therefore, in the function scope, we better use static constexpr .
Debug
While you may find some tricks on the internet, there is no standard debugging system for compile-time programming. Therefore, for complex calculations, the constexpr functions are better tested first at runtime and then used at compile time.
Constexpr limit
Because there is no debugging for compile-time cacluations, there are some limits on constexpr operations and loops. They can be edited via compiler flags: -fconstexpr-depth=n , -fconstexpr-loop-limit=n , -fconstexpr-ops-limit=n .
Case study 1
A program always needs 20th number of Fibonacci sequences. Can you write a function that calculates this at compile-time, so we don’t waste time computing this value at runtime?
Solution
Case study 2
In C++, you cannot overload a function based on its return type. So this will NOT compile:
Can you write a function using meta-programming that returns a string if a condition is met and otherwise it returns an int ?
Solution:
Note that while at compile-time they are the same, but at runtime, f<0> and f<1> are two different animals with different signatures.
Case study 3
Imagine we have a 3D box filled with ping-pong balls, we store each ball as
But we know if the thickness of the box is equal to the diameter of the ball, the model can be 2D
In usual programming, we have to write two libraries to handle 2D balls and 3D balls separately:
But is there a way that, keeping the same structure (POD), we write one piece of code that if the box is very thin, it is compiled to a 2D balls program otherwise 3D?
2.4.4. Переменные constexpr и константные выражения
Константное выражение (constant expression) — это выражение, значение которого не может измениться и вычисляется во время компиляции. Литерал — это константное выражение. Константный объект, инициализируемый константным выражением, также является константным выражением. Вскоре мы увидим, что в языке есть несколько контекстов, требующих константных выражений.
Является ли данный объект (или выражение) константным выражением, зависит от типов и инициализаторов. Например:
const int max_files = 20; // max_files — константное выражение
const int limit = max_files + 1; // limit — константное выражение
int staff_size = 27; // staff_size — неконстантное выражение
const int sz = get_size(); // sz — неконстантное выражение
Хотя переменная staff_size инициализируется литералом, это неконстантное выражение, поскольку он имеет обычный тип int, а не const int. С другой стороны, хоть переменная sz и константа, значение ее инициализатора неизвестно до времени выполнения. Следовательно, это неконстантное выражение.
В большой системе может быть трудно утверждать (наверняка), что инициализатор — константное выражение. Константная переменная могла бы быть определена с инициализатором, который мы полагаем константным выражением. Однако при использовании этой переменной в контексте, требующем константного выражения, может оказаться, что инициализатор не был константным выражением. Как правило, определение объекта и его использования в таком контексте располагаются довольно далеко друг от друга.
Согласно новому стандарту, можно попросить компилятор проверить, является ли переменная константным выражением, использовав в ее объявлении ключевое слово constexpr. Переменные constexpr неявно являются константой и должны инициализироваться константными выражениями.
constexpr int mf = 20; // 20 — константное выражение
constexpr int limit = mf + 1; // mf + 1 — константное выражение
constexpr int sz = size(); // допустимо, только если size() является
Хоть и нельзя использовать обычную функцию как инициализатор для переменной constexpr, как будет описано в разделе 6.5.2, новый стандарт позволяет определять функции как constexpr. Такие функции должны быть достаточно просты, чтобы компилятор мог выполнить их во время компиляции. Функции constexpr можно использовать в инициализаторе переменной constexpr.
Как правило, ключевое слово constexpr имеет смысл использовать для переменных, которые предполагается использовать как константные выражения.
Поскольку константное выражение обрабатывается во время компиляции, есть пределы для типов, которые можно использовать в объявлении constexpr. Типы, которые можно использовать в объявлении constexpr, известны как литеральные типы (literal type), поскольку они достаточно просты для литеральных значений.
Все использованные до сих пор типы — арифметический, ссылка и указатель — это литеральные типы. Наш класс Sales_item и библиотечный тип string не относятся к литеральным типам. Следовательно, нельзя определить переменные этих типов как constexpr. Другие виды литеральных типов рассматриваются в разделах 7.5.6 и 19.3.
Хотя указатели и ссылки можно определить как constexpr, используемые для их инициализации объекты жестко ограничены. Указатель constexpr можно инициализировать литералом nullptr или литералом (т.е. константным выражением) 0. Можно также указать на (или связать с) объект, который остается по фиксированному адресу.
По причинам, рассматриваемым в разделе 6.1.1, определенные в функции переменные обычно не хранятся по фиксированному адресу. Следовательно, нельзя использовать указатель constexpr для указания на такие переменные. С другой стороны, адрес объекта, определенного вне любой функции, является константным выражением и, таким образом, может использоваться для инициализации указателя constexpr. Как будет описано в разделе 6.1.1, функции могут определять переменные, существующие на протяжении нескольких вызовов этой функция. Как и объект, определенный вне любой функции, эти специальные локальные объекты также имеют фиксированные адреса. Поэтому и ссылка constexpr может быть связана с такой переменной, и указатель constexpr может содержать ее адрес.
Указатели и спецификатор constexpr
Важно понимать, что при определении указателя в объявлении constexpr спецификатор constexpr относится к указателю, а не к типу, на который указывает указатель.
const int *p = nullptr; // p — указатель на const int
constexpr int *q = nullptr; // q — константный указатель на int
Несмотря на внешний вид, типы p и q весьма различны; p — указатель на константу, тогда как q — константный указатель. Различие является следствием того факта, что спецификатор constexpr налагает на определяемый объект спецификатор const верхнего уровня (см. раздел 2.4.3).
Как и любой другой константный указатель, указатель constexpr может указать на константный или неконстантный тип.
constexpr int *np = nullptr; // np — нулевой константный указатель
constexpr int i = 42; // типом i является const int
// i и j должны быть определены вне любой функции
constexpr const int *p = &i; // p — константный указатель
constexpr int *p1 = &j; // p1 — константный указатель на int j
Упражнения раздела 2.4.4
Упражнение 2.32. Допустим ли следующий код? Если нет, то как его исправить?