C++20: consteval & constinit
2/5/2021
Author's Profile Avatar
C++20添加了两个declaration specifier:constevalconstinit 。前者只能参与函数的声明,后者只能参与变量的声明。
在介绍 constevalconstinit 时,我们有必要简单回顾一下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>();