C++20添加了两个declaration specifier:consteval
与 constinit
。前者只能参与函数的声明,后者只能参与变量的声明。
在介绍 consteval
与 constinit
时,我们有必要简单回顾一下C++11中引入的另一个declaration specifier:constexpr
。
回顾:constexpr
constexpr
既可用于函数声明,也可用于变量声明。constexpr
的主要作用是声明变量的值或函数的返回值可以在常量表达式(即编译期便可计算出值的表达式)中使用。
当一个变量的声明中包含了 constexpr
时,这个变量将自动成为以 const
声明的常量,这个变量的初始化表达式也必须是一个常量表达式(即可以在编译期进行计算的表达式),这个变量的值也可以参与到其他常量表达式中。
int foo(); constexpr const int x = 5; // OK, `5` is a constant expression constexpr const int y = foo(); // Error: `foo()` is not a constant expression
当一个函数的声明中包含了 constexpr
时, 则当这个函数的参数满足一定条件时(通常来说是当这个函数的所有参数的值都是常量表达式时),这个函数的返回值也是编译期可计算的,可以参与到其他的常量表达式中。声明为 constexpr
的函数有诸多限制条件以允许编译器在编译期执行函数的逻辑,具体情况可参考 cppreference 上的相关章节,在此不作说明。
consteval
使用 constexpr
声明的函数,其返回值可以被用于常量表达式中。但是,constexpr
没有限定函数只能被用于常量表达式中。 constexpr
函数仍然可以用于一般表达式中:
constexpr int sqr(int x) { return x * x; } int foo() { int x = doSomething(); return sqr(x); // OK, sqr will be called during runtime }
此时,程序在运行时仍然会调用 sqr
函数。另外,对于某些编译器(例如 clang 以及不带优化选项的 gcc),调用以 constexpr
声明的函数只有在常量表达式中才会被展开。在一般表达式中,即使传入函数的参数均为常量表达式,编译器仍然会产生运行时函数调用。例如:
constexpr int sqr(int x) { return x * x; } int foo() { return sqr(5); }
编译器生成的 foo
函数的代码中仍然包含对 sqr
的调用,即使 sqr(5)
能够在编译期进行计算。
consteval
则可以看作是更加严格的 constexpr
,它只能用于函数的声明。当某个函数使用consteval
声明后,则所有带有求值动作的调用这个函数的表达式必须为常量表达式。例如:
consteval int sqr(int x) { return x * x; } void foo() { constexpr const int x = 10; int y = doSomething(); sqr(x); // OK, sqr(x) is constant expression sqr(y); // Error: sqr(y) is not a constant expression } consteval int sqrsqr(int x) { return sqr(sqr(x)); // OK: sqr(sqr(x)) is not constant expression, however it will be // when evaluating sqrsqr in constant expressions }
有了上述保证,编译器在处理对 consteval
函数的调用时,将保证在编译期计算出函数返回值,从而在生成的代码中不包含任何对 consteval
函数的调用。例如:
consteval int fibo_consteval(int n) { if (n == 1 || n == 2) { return 1; } else { return fibo_consteval(n - 1) + fibo_consteval(n - 2); } } constexpr int fibo_constexpr(int n) { if (n == 1 || n == 2) { return 1; } else { return fibo_constexpr(n - 1) + fibo_constexpr(n - 2); } } bool fibo() { int a = fibo_consteval(6); int b = fibo_constexpr(6); return a == b; }
编译器为 fibo
函数生成的代码中,视编译器而定可能会包含对 fibo_constexpr
函数的调用,但一定不会包含 fibo_consteval
函数的调用。对 fibo_consteval
函数的调用将保证被常量值8代替。
需要注意到的是,不带有求值动作的调用 consteval
函数的表达式不需要为常量表达式。例如:
consteval int sqr(int x) { return x * x; } void foo() { int y = doSomething(); decltype(sqr(y)) z = doSomething(); // OK: sqr(y) is not a constant expression, // however sqr(y) is not evaluated so still OK }
不带有求值动作的 consteval
函数调用表达式不需要为常量表达式这一特性目前在不同编译器上的支持并不完善。例如,gcc 10.2 认为上述代码片段可以编译,但 clang 11.0.1 认为上述代码片段不能编译。
constinit
具有静态生命周期的变量有两种初始化方式:静态初始化(static initialization)以及动态初始化(dynamic initialization)。采用静态初始化初始化的变量,其初始化表达式是一个常量表达式,其值可以在编译期计算出。采用动态初始化初始化的变量,其初始化表达式将会在运行时以声明顺序求值。例如:
consteval int foo() { return 10; } int bar(); int a = 10; // Static initialization int b = foo(); // Static initialization int c = bar(); // Dynamic initialization
有关动态初始化的一个广为人知的缺陷是 static initialization order fiasco。例如:
// A.cpp int a = doSomething(); // B.cpp extern int a; int b = a + 1;
在上述的例子中,如果编译器决定首先初始化b而不是a,则将产生未定义行为(使用未初始化的变量)。
constinit
的作用在于显式地指定变量的初始化方式为静态初始化。所有使用 constinit
声明的变量,其生命周期必须为静态生命周期或线程本地(Thread-local)生命周期(即不能为局部变量),其初始化表达式必须是一个常量表达式。例如:
consteval int foo() { return 10; } int bar(); constinit int a = 10; // OK constinit int b = foo(); // OK constinit thread_local int c = 20; // OK constinit int d = bar(); // Error: `bar()` is not a constant expression void baz() { constinit int e = 20; // Error: e is not static }
使用 constexpr
声明的变量蕴含了 constinit
的条件。使用 constexpr
声明的变量还额外有如下的限制:
首先,使用constexpr
声明的变量蕴含了const
,即变量是常量,其值在初始化后不能改变。例如:
constexpr int a = 10; constinit int b = 10; void foo() { a = 20; // Error: constexpr implies const b = 20; // OK }
其次,从C++20开始,使用 constexpr
声明的变量必须满足 const destructable。例如:
// Error: std::shared_ptr<int> is not const destructable constexpr std::shared_ptr<int> a = std::shared_ptr<int>(); // OK constinit std::shared_ptr<int> a = std::shared_ptr<int>();