Разница между `constexpr` и`const`
В чем разница между constexpr
а также const
?
- Когда я могу использовать только один из них?
- Когда я могу использовать оба и как выбрать один?
10 ответов
Основное значение и синтаксис
Оба ключевых слова могут использоваться в объявлении объектов, а также функций. Основное отличие применительно к объектам заключается в следующем:
const
объявляет объект как константу. Это подразумевает гарантию того, что после инициализации значение этого объекта не изменится, и компилятор может использовать этот факт для оптимизации. Это также помогает запретить программисту писать код, который изменяет объекты, которые не должны были изменяться после инициализации.constexpr
объявляет объект пригодным для использования в том, что стандарт называет константными выражениями. Но учтите, чтоconstexpr
это не единственный способ сделать это.
Применительно к функциям основное отличие заключается в следующем:
const
может использоваться только для нестатических функций-членов, но не для функций в целом. Это дает гарантию, что функция-член не изменяет ни один из нестатических элементов данных.constexpr
может использоваться как с членами, так и с не членами, а также с конструкторами. Он объявляет функцию пригодной для использования в константных выражениях. Компилятор примет его, только если функция соответствует определенным критериям (7.1.5/3,4), наиболее важно (†):- Тело функции должно быть не виртуальным и чрезвычайно простым: кроме определения типов и статических утверждений, только один
return
Заявление разрешено. В случае конструктора разрешены только список инициализации, typedefs и static assert. (= default
а также= delete
разрешено тоже, хотя.) - Начиная с C++14, правила более смягчены, что разрешено с тех пор внутри функции constexpr:
asm
декларацияgoto
заявление, заявление с меткой, отличной отcase
а такжеdefault
try-block, определение переменной не-литерального типа, определение переменной статической или длительности хранения потока, определение переменной, для которой инициализация не выполняется. - Аргументы и возвращаемый тип должны быть литеральными типами (то есть, вообще говоря, очень простыми типами, обычно скалярами или агрегатами).
- Тело функции должно быть не виртуальным и чрезвычайно простым: кроме определения типов и статических утверждений, только один
Постоянные выражения
Как сказано выше, constexpr
объявляет оба объекта, а также функции, пригодные для использования в константных выражениях. Постоянное выражение не просто константа:
Он может использоваться в местах, где требуется оценка во время компиляции, например, параметры шаблона и спецификаторы размера массива:
template<int N> class fixed_size_list { /*...*/ }; fixed_size_list<X> mylist; // X must be an integer constant expression int numbers[X]; // X must be an integer constant expression
Но обратите внимание:
Объявить что-то как
constexpr
не обязательно гарантирует, что оно будет оценено во время компиляции. Он может использоваться для таких целей, но может использоваться и в других местах, которые оцениваются также во время выполнения.Объект может быть пригоден для использования в константных выражениях без объявления
constexpr
, Пример:int main() { const int N = 3; int numbers[N] = {1, 2, 3}; // N is constant expression }
Это возможно, потому что
N
, будучи постоянным и инициализированным во время объявления с литералом, удовлетворяет критериям для постоянного выражения, даже если оно не объявленоconstexpr
,
Так, когда я на самом деле должен использовать constexpr
?
Объект как
N
Выше можно использовать как константное выражение без объявленияconstexpr
, Это верно для всех объектов, которые:const
- целочисленного или перечислимого типа и
- инициализируется во время объявления выражением, которое само является константным выражением
[Это связано с §5.19/2: константное выражение не должно включать подвыражений, которые включают "модификацию lvalue-to-rvalue, если только […] glvalue целочисленного или перечислимого типа […]" Спасибо Ричарду Смиту за исправление моего ранее утверждали, что это верно для всех литеральных типов.]
Чтобы функция была пригодна для использования в константных выражениях, она должна быть явно объявлена
constexpr
; недостаточно просто удовлетворять критериям для функций с постоянным выражением. Пример:template<int N> class list { }; constexpr int sqr1(int arg) { return arg * arg; } int sqr2(int arg) { return arg * arg; } int main() { const int X = 2; list<sqr1(X)> mylist1; // OK: sqr1 is constexpr list<sqr2(X)> mylist2; // wrong: sqr2 is not constexpr }
Когда я могу / должен использовать оба, const
а также constexpr
все вместе?
А. В объявлениях объекта. Это никогда не требуется, когда оба ключевых слова ссылаются на один и тот же объект, который должен быть объявлен. constexpr
подразумевает const
,
constexpr const int N = 5;
такой же как
constexpr int N = 5;
Однако обратите внимание, что могут быть ситуации, когда каждое ключевое слово ссылается на разные части объявления:
static constexpr int N = 3;
int main()
{
constexpr const int *NP = &N;
}
Вот, NP
объявляется как адрес-константа-выражение, то есть указатель, который сам является константным выражением. (Это возможно, когда адрес генерируется путем применения оператора адреса к выражению статической / глобальной константы.) Здесь оба constexpr
а также const
являются обязательными: constexpr
всегда относится к объявленному выражению (здесь NP
), в то время как const
относится к int
(он объявляет указатель на const). Удаление const
сделает выражение недопустимым (потому что (a) указатель на неконстантный объект не может быть константным выражением, и (b) &N
фактически является указателем на константу).
Б. В объявлениях функций-членов. В C++11 constexpr
подразумевает const
в то время как в C++14 и C++17 это не так. Функция-член, объявленная в C++11 как
constexpr void f();
должен быть объявлен как
constexpr void f() const;
под C++14, чтобы все еще быть пригодным для использования в качестве const
функция.
const
применяется к переменным и предотвращает их изменение в вашем коде.
constexpr
сообщает компилятору, что это выражение приводит к значению постоянной времени компиляции, поэтому его можно использовать в таких местах, как длина массива, присваивая const
переменные и т. д. Ссылка, предоставленная Оли, имеет много отличных примеров.
По сути, они представляют собой две разные концепции и могут (и должны) использоваться вместе.
обзор
const
гарантирует, что программа не изменяет значение объекта. Тем не мение,const
не гарантирует, какой тип инициализации объект подвергается.Рассматривать:
const int mx = numeric_limits<int>::max(); // OK: runtime initialization
Функция
max()
просто возвращает буквальное значение. Однако, поскольку инициализатор является вызовом функции,mx
проходит инициализацию во время выполнения. Следовательно, вы не можете использовать его как константное выражение:int arr[mx]; // error: “constant expression required”
constexpr
это новое ключевое слово C++11, которое избавляет вас от необходимости создавать макросы и жестко закодированные литералы. Это также гарантирует, при определенных условиях, что объекты подвергаются статической инициализации. Он контролирует время оценки выражения. Обеспечивая оценку его выражения во время компиляции,constexpr
позволяет вам определять истинные константные выражения, которые важны для критичных ко времени приложений, системного программирования, шаблонов и, вообще говоря, в любом коде, который опирается на константы времени компиляции.
Функции с постоянными выражениями
Функция с постоянным выражением - это объявленная функция constexpr
, Его тело должно быть не виртуальным и состоять только из одного оператора return, кроме typedefs и static asserts. Его аргументы и возвращаемое значение должны иметь литеральные типы. Его можно использовать с аргументами без константных выражений, но когда это сделано, результат не является константным выражением.
Функция с постоянным выражением предназначена для замены макросов и жестко закодированных литералов без ущерба для производительности или безопасности типов.
constexpr int max() { return INT_MAX; } // OK
constexpr long long_max() { return 2147483647; } // OK
constexpr bool get_val()
{
bool res = false;
return res;
} // error: body is not just a return statement
constexpr int square(int x)
{ return x * x; } // OK: compile-time evaluation only if x is a constant expression
const int res = square(5); // OK: compile-time evaluation of square(5)
int y = getval();
int n = square(y); // OK: runtime evaluation of square(y)
Объекты с постоянным выражением
Объект с постоянным выражением является объявленным объектом constexpr
, Он должен быть инициализирован постоянным выражением или значением, созданным конструктором постоянного выражения с аргументами постоянного выражения.
Объект константного выражения ведет себя так, как если бы он был объявлен const
, за исключением того, что он требует инициализации перед использованием и его инициализатор должен быть константным выражением. Следовательно, объект константного выражения всегда можно использовать как часть другого константного выражения.
struct S
{
constexpr int two(); // constant-expression function
private:
static constexpr int sz; // constant-expression object
};
constexpr int S::sz = 256;
enum DataPacket
{
Small = S::two(), // error: S::two() called before it was defined
Big = 1024
};
constexpr int S::two() { return sz*2; }
constexpr S s;
int arr[s.two()]; // OK: s.two() called after its definition
Константные выражения
Конструктор с постоянным выражением - это объявленный конструктор constexpr
, Он может иметь список инициализации члена, но его тело должно быть пустым, кроме typedefs и статических утверждений. Его аргументы должны иметь буквенные типы.
Конструктор с постоянным выражением позволяет компилятору инициализировать объект во время компиляции, при условии, что все аргументы конструктора являются константными выражениями.
struct complex
{
// constant-expression constructor
constexpr complex(double r, double i) : re(r), im(i) { } // OK: empty body
// constant-expression functions
constexpr double real() { return re; }
constexpr double imag() { return im; }
private:
double re;
double im;
};
constexpr complex COMP(0.0, 1.0); // creates a literal complex
double x = 1.0;
constexpr complex cx1(x, 0); // error: x is not a constant expression
const complex cx2(x, 1); // OK: runtime initialization
constexpr double xx = COMP.real(); // OK: compile-time initialization
constexpr double imaglval = COMP.imag(); // OK: compile-time initialization
complex cx3(2, 4.6); // OK: runtime initialization
Советы из книги Скотта Мейерса " Эффективное современное С ++ " о constexpr
:
constexpr
объекты являются постоянными и инициализируются значениями, известными во время компиляции;constexpr
функции выдают результаты времени компиляции при вызове с аргументами, значения которых известны во время компиляции;constexpr
объекты и функции могут использоваться в более широком диапазоне контекстов, чемconstexpr
объекты и функции;constexpr
является частью интерфейса объекта или функции.
Источник: Использование constexpr для улучшения безопасности, производительности и инкапсуляции в C++.
И то и другое const
а также constexpr
может применяться к переменным и функциям. Хотя они похожи друг на друга, на самом деле это очень разные понятия.
И то и другое const
а также constexpr
означает, что их значения не могут быть изменены после их инициализации. Так, например:
const int x1=10;
constexpr int x2=10;
x1=20; // ERROR. Variable 'x1' can't be changed.
x2=20; // ERROR. Variable 'x2' can't be changed.
Принципиальная разница между const
а также constexpr
время, когда их значения инициализации известны (оценены). Хотя значения const
переменные могут быть оценены как во время компиляции, так и во время выполнения, constexpr
всегда оцениваются во время компиляции. Например:
int temp=rand(); // temp is generated by the the random generator at runtime.
const int x1=10; // OK - known at compile time.
const int x2=temp; // OK - known only at runtime.
constexpr int x3=10; // OK - known at compile time.
constexpr int x4=temp; // ERROR. Compiler can't figure out the value of 'temp' variable at compile time so `constexpr` can't be applied here.
Основным преимуществом для того, чтобы знать, известно ли значение во время компиляции или во время выполнения, является тот факт, что константы времени компиляции могут использоваться всякий раз, когда необходимы константы времени компиляции. Например, C++ не позволяет вам указывать C-массивы с переменной длиной.
int temp=rand(); // temp is generated by the the random generator at runtime.
int array1[10]; // OK.
int array2[temp]; // ERROR.
Итак, это означает, что:
const int size1=10; // OK - value known at compile time.
const int size2=temp; // OK - value known only at runtime.
constexpr int size3=10; // OK - value known at compile time.
int array3[size1]; // OK - size is known at compile time.
int array4[size2]; // ERROR - size is known only at runtime time.
int array5[size3]; // OK - size is known at compile time.
Так const
переменные могут определять обе константы времени компиляции, такие как size1
которые могут быть использованы для указания размеров массива и констант времени выполнения, таких как size2
которые известны только во время выполнения и не могут использоваться для определения размеров массива. С другой стороны constexpr
всегда определяйте константы времени компиляции, которые могут указывать размеры массива.
И то и другое const
а также constexpr
может также применяться к функциям. const
функция должна быть функцией-членом (метод, оператор), где применение const
Ключевое слово означает, что метод не может изменить значения своих членских (нестатических) полей. Например.
class test
{
int x;
void function1()
{
x=100; // OK.
}
void function2() const
{
x=100; // ERROR. The const methods can't change the values of object fields.
}
};
constexpr
это другая концепция. Он помечает функцию (член или не член) как функцию, которая может быть оценена во время компиляции, если в качестве аргументов переданы константы времени компиляции. Например, вы можете написать это.
constexpr int func_constexpr(int X, int Y)
{
return(X*Y);
}
int func(int X, int Y)
{
return(X*Y);
}
int array1[func_constexpr(10,20)]; // OK - func_constexpr() can be evaluated at compile time.
int array2[func(10,20)]; // ERROR - func() is not a constexpr function.
int array3[func_constexpr(10,rand())]; // ERROR - even though func_constexpr() is the 'constexpr' function, the expression 'constexpr(10,rand())' can't be evaluated at compile time.
Кстати constexpr
функции - это обычные функции C++, которые можно вызывать, даже если переданы непостоянные аргументы. Но в этом случае вы получаете значения не-constexpr.
int value1=func_constexpr(10,rand()); // OK. value1 is non-constexpr value that is evaluated in runtime.
constexpr int value2=func_constexpr(10,rand()); // ERROR. value2 is constexpr and the expression func_constexpr(10,rand()) can't be evaluated at compile time.
constexpr
может также применяться к функциям-членам (методам), операторам и даже конструкторам. Например.
class test2
{
static constexpr int function(int value)
{
return(value+1);
}
void f()
{
int x[function(10)];
}
};
Более "сумасшедший" образец.
class test3
{
public:
int value;
// constexpr const method - can't chanage the values of object fields and can be evaluated at compile time.
constexpr int getvalue() const
{
return(value);
}
constexpr test3(int Value)
: value(Value)
{
}
};
constexpr test3 x(100); // OK. Constructor is constexpr.
int array[x.getvalue()]; // OK. x.getvalue() is constexpr and can be evaluated at compile time.
По книге Бьярна Страуструпа "Язык программирования C++ 4-й редакции"
• const: примерно означает "Я обещаю не менять это значение" (§7.5). Это используется главным образом для указания интерфейсов, так что данные могут передаваться функциям, не опасаясь их изменения.
Компилятор выполняет обещание, данное const.
• constexpr: примерно означает "быть оцененным во время компиляции" (§10.4). Это используется в первую очередь для указания констант, чтобы позволить
Например:
const int dmv = 17; // dmv is a named constant
int var = 17; // var is not a constant
constexpr double max1 = 1.4*square(dmv); // OK if square(17) is a constant expression
constexpr double max2 = 1.4∗square(var); // error : var is not a constant expression
const double max3 = 1.4∗square(var); //OK, may be evaluated at run time
double sum(const vector<double>&); // sum will not modify its argument (§2.2.5)
vector<double> v {1.2, 3.4, 4.5}; // v is not a constant
const double s1 = sum(v); // OK: evaluated at run time
constexpr double s2 = sum(v); // error : sum(v) not constant expression
Чтобы функция могла использоваться в константном выражении, то есть в выражении, которое будет оцениваться компилятором, должно быть определено constexpr.
Например:
constexpr double square(double x) { return x∗x; }
Чтобы быть constexpr, функция должна быть довольно простой: просто оператор возврата, вычисляющий значение. Функция constexpr может использоваться для непостоянных аргументов, но когда это сделано, результат не является константным выражением. Мы разрешаем вызывать функцию constexpr с аргументами неконстантных выражений в контекстах, которые не требуют константных выражений, поэтому мы не должны определять по существу одну и ту же функцию дважды: один раз для константных выражений и один раз для переменных.
В некоторых местах константные выражения требуются правилами языка (например, границы массивов (§2.2.5,
§7.3), метки case (§2.2.4, §9.4.2), некоторые аргументы шаблона (§25.2) и константы, объявленные с помощью constexpr). В других случаях оценка времени компиляции важна для производительности. Независимо от проблем производительности, понятие неизменности (объекта с неизменяемым состоянием) является важной проблемой проектирования (§10.4).
const int var
можно динамически установить значение во время выполнения, и как только оно будет установлено на это значение, его уже нельзя будет изменить.
constexpr int var
не может быть динамически установлен во время выполнения, а во время компиляции. И как только оно будет установлено на это значение, его уже нельзя будет изменить.
Вот хороший пример:
int main(int argc, char*argv[]) {
const int p = argc;
// p = 69; // cannot change p because it is a const
// constexpr int q = argc; // cannot be, bcoz argc cannot be computed at compile time
constexpr int r = 2^3; // this works!
// r = 42; // same as const too, it cannot be changed
}
Фрагмент кода выше компилируется нормально, и я прокомментировал те, которые приводят к ошибке.
Как уже отмечалось @0x499602d2, const
только гарантирует, что значение не может быть изменено после инициализации, где как constexpr
(введено в C++11) гарантирует, что переменная является константой времени компиляции.
Рассмотрим следующий пример (из LearnCpp.com):
cout << "Enter your age: ";
int age;
cin >> age;
const int myAge{age}; // works
constexpr int someAge{age}; // error: age can only be resolved at runtime
Я не думаю, что какой-либо из ответов действительно проясняет, какие именно побочные эффекты он имеет, или действительно, что это такое.
constexpr
а также const
в пространстве имен / области файла идентичны при инициализации литералом или выражением; но с функцией,const
может быть инициализирован любой функцией, но constexpr
инициализированный не-constexpr (функция, не отмеченная constexpr или не constexpr-выражением) вызовет ошибку компилятора. Обеconstexpr
а также const
являются неявно внутренней связью для переменных (ну, на самом деле, они не доживают до стадии связывания при компиляции -O1 и выше, и static
не заставляет компилятор выдавать внутренний (локальный) символ компоновщика для const
или constexpr
при -O1 или выше; единственный раз, когда это происходит, если вы берете адрес переменной.const
а также constexpr
будет внутренним символом, если не выражено с помощью extern
т.е. extern constexpr/const int i = 3;
необходимо использовать). На функцииconstexpr
заставляет функцию никогда не доходить до стадии связывания (независимо от extern
или inline
в определении или -O0 или -Ofast), тогда как const
никогда не делает, и static
а также inline
действует только на -O1 и выше. Когдаconst
/constexpr
переменная инициализируется constexpr
функция, загрузка всегда оптимизируется с помощью любого флага оптимизации, но она никогда не оптимизируется, если функция только static
или inline
, или если переменная не const
/constexpr
.
Стандартная компиляция (-O0)
#include<iostream>
constexpr int multiply (int x, int y)
{
return x * y;
}
extern const int val = multiply(10,10);
int main () {
std::cout << val;
}
компилируется в
val:
.long 100 //extra external definition supplied due to extern
main:
push rbp
mov rbp, rsp
mov esi, 100 //substituted in as an immediate
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
.
.
.
Однако
#include<iostream>
const int multiply (int x, int y)
{
return x * y;
}
const int val = multiply(10,10); //constexpr is an error
int main () {
std::cout << val;
}
Компилируется в
multiply(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
pop rbp
ret
main:
push rbp
mov rbp, rsp
mov eax, DWORD PTR val[rip]
mov esi, eax
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
.
.
.
mov esi, 10
mov edi, 10
call multiply(int, int)
mov DWORD PTR val[rip], eax
Это ясно показывает, что constexpr
вызывает инициализацию const/constexpr
переменная области видимости файла возникает во время компиляции и не создает глобального символа, тогда как ее неиспользование вызывает инициализацию до main
во время выполнения.
Компиляция с использованием -Ofast
Даже -Ofast не оптимизирует нагрузку! https://godbolt.org/z/r-mhif, поэтому вам нужно constexpr
constexpr
функции также можно вызывать из других constexpr
функции для того же результата. constexpr
в функции также предотвращает использование всего, что не может быть выполнено во время компиляции функции; например, звонок в<<
оператор на std::cout
.
constexpr
at область видимости блока ведет себя так же, поскольку выдает ошибку, если инициализируется функцией, отличной от constexpr; значение также подставляется немедленно.
В конце концов, его основная цель похожа на встроенную функцию C, но она эффективна только тогда, когда функция используется для инициализации переменных файловой области (которые функции не могут выполнять на C, но они могут на C++, потому что она позволяет динамическую инициализацию файла- переменные области видимости), за исключением того, что функция не может экспортировать глобальный / локальный символ в компоновщик, даже используя extern/static
, что вы могли бы с inline
на C; функции назначения переменных блочной области могут быть встроены просто с помощью оптимизации -O1 безconstexpr
на C и C++.
Обзор ключевых слов const и constexpr
В C ++, если константный объект инициализируется константным выражением, мы можем использовать наш константный объект везде, где требуется константное выражение.
const int x = 10;
int a[x] = {0};
Например, мы можем сделать оператор case в switch.
constexpr можно использовать с массивами.
constexpr - это не тип.
Ключевое слово constexpr может использоваться вместе с ключевым словом auto.
constexpr auto x = 10;
struct Data { // We can make a bit field element of struct.
int a:x;
};
Если мы инициализируем константный объект константным выражением, выражение, сгенерированное этим константным объектом, теперь также будет константным выражением.
Постоянное выражение: выражение, значение которого может быть вычислено во время компиляции.
x*5-4 // Это постоянное выражение. Для компилятора нет разницы между вводом этого выражения и вводом 46 напрямую.
Инициализация обязательна. Его можно использовать только для чтения. Это не может быть изменено. До этого момента нет никакой разницы между ключевыми словами «const» и «constexpr».
ПРИМЕЧАНИЕ. Мы можем использовать constexpr и const в одном объявлении.
constexpr const int* p;
Функции Constexpr
Обычно возвращаемое значение функции получается во время выполнения. Но вызовы функций constexpr будут получены как константа во время компиляции при соблюдении определенных условий.
ПРИМЕЧАНИЕ. Аргументы, отправленные в переменную параметра функции при вызове функции или во все переменные параметра, если имеется более одного параметра, если CE, возвращаемое значение функции будет вычислено во время компиляции. !!!
constexpr int square (int a){
return a*a;
}
constexpr int a = 3;
constexpr int b = 5;
int arr[square(a*b+20)] = {0}; //This expression is equal to int arr[35] = {0};
Для того чтобы функция была функцией constexpr, тип возвращаемого значения функции и тип параметров функции должны быть в категории типов, называемой «литеральным типом».
Функции constexpr являются неявно встроенными функциями.
Важный момент:
Ни одну из функций constexpr не нужно вызывать с постоянным выражением, это не обязательно. Если это произойдет, вычисления не будут выполняться во время компиляции. Это будет рассматриваться как обычный вызов функции. Следовательно, там, где требуется постоянное выражение, мы больше не сможем использовать это выражение.
Условия, которые должны быть функцией constexpr, показаны ниже;
1) Типы, используемые в параметрах функции, и тип возвращаемого значения функции должны быть буквального типа.
2) Локальная переменная со статическим временем жизни не должна использоваться внутри функции.
3) Если функция допустима, когда мы вызываем эту функцию с постоянным выражением во время компиляции, компилятор вычисляет возвращаемое значение функции во время компиляции.
4) Компилятор должен видеть код функции, поэтому функции constexpr почти всегда будут в файлах заголовков.
5) Чтобы функция, которую мы создали, была функцией constexpr, определение функции должно быть в файле заголовка. Таким образом, какой бы исходный файл не включал этот файл заголовка, он увидит определение функции.
Бонус
Обычно при инициализации элемента по умолчанию статические элементы данных с константными и целочисленными типами могут быть инициализированы внутри класса. Однако для этого должны быть как «константные», так и «целочисленные» типы.
Если мы используем static constexpr, то он не обязательно должен быть интегральным типом для его инициализации внутри класса. Пока я инициализирую его постоянным выражением, проблем нет.
class Myclass {
const static int sx = 15; // OK
constexpr static int sy = 15; // OK
const static double sd = 1.5; // ERROR
constexpr static double sd = 1.5; // OK
};
Прежде всего, оба являются квалификаторами в C++. Переменная, объявленная как const, должна быть инициализирована и не может быть изменена в будущем. Следовательно, обычно переменная, объявленная как константа, будет иметь значение еще до компиляции.
Но для constexpr все несколько иначе.
Для constexpr вы можете указать выражение, которое может быть вычислено во время компиляции программы.
Очевидно, переменная, объявленная как constexper, не может быть изменена в будущем, как и const.