C++ constexpr, consteval和 constinit简要介绍

constexpr

在现代 C++ 中, 编译时计算能够有效减少运行时开销, constexpr 是这一理念的核心工具, 自 C++11 引入以来, 其功能在 C++14 和 C++20 中不断增强.

constexpr 修饰值

constexpr 可以修饰变量. 此时, 变量必须在编译时就能确定其值.

int main() {
  // 初始化常量表达式
  constexpr int min_size = 10;             // OK, 字面量初始化
  constexpr int max_size = min_size + 10;  // OK, 常量表达式
  min_size = 20;                           // Error, 常量不能被修改

  return 0;
}

可以像使用常量一样使用 constexpr 变量.

#include <array>
#include <vector>
int main() {
  constexpr int size = 10;

  // 声明非常量类型
  int c_array[size] = {0};            // OK
  std::array<int, size> array = {0};  // OK
  std::vector<int> vec(size);         // OK

  // 声明常量类型
  constexpr int cpr_array_[size] = {0};             // OK
  constexpr std::array<int, size> cpr_array = {0};  // OK
  constexpr std::vector<int> cpr_vec(size);         // Error, std::vector 不支持常量构造函数

  return 0;
}

除了整型, 其他类型也可以使用 constexpr 修饰.

#include <string>
#include <string_view>

int main() {
  constexpr double root_of_2 = 1.41421356237;     // OK
  constexpr const char* hello = "Hello, World!";  // OK
  constexpr std::string_view str_view{hello};     // OK
  constexpr std::string str{hello};               // Error
  return 0;
}

constexpr 修饰类实例

如果你想定义一个constexpr的类变量. 你需要确保类的构造函数是constexpr的.

// constexpr 修饰的类实例
struct Point {
  constexpr Point(int x, int y) : x_(x), y_(y) {}
  constexpr int x() const { return x_; }
  constexpr int y() const { return y_; }

  private:
  int x_;
  int y_;
};
constexpr Point origin{0, 0};  // OK
constexpr Point dst{1, 1};     // OK

struct OldPoint {
  OldPoint(int x) : x_(x) {}

  private:
  int x_;
};
constexpr OldPoint old_origin{0};  // Error, OldPoint的构造函数不是constexpr的

constexpr 函数和 lambda

constexpr 修饰函数表示该函数在编译时就能计算出结果.

constexpr int square(int x) { return x * x; }

int main() {
  constexpr int a = 3;
  constexpr int b = 4;
  constexpr int c = square(a) + square(b);  // OK, a 和 b 是常量
  int d = square(5);                        // OK, d 不要求是常量

  int e = 3;
  constexpr int f = square(e);  // 错误, e是普通变量, 不能作为constexpr函数的参数.
                                // 非 constexpr 参数会导致编译错误

  auto abs = [](int x) constexpr -> int { return x < 0 ? -x : x; };
  constexpr int g = abs(-1);  // OK

  return abs(0);
}

if constexpr编译时分支

编译时分支决策

if constexpr允许在编译时根据模板参数或其他编译时可知的条件进行条件分支, 这意味着可以在编译时决定哪些代码会被编译进最终的程序中. 这对于模板元编程尤其重要, 因为它允许基于类型特性进行条件编译, 从而避免了运行时的分支判断, 提高了程序的效率.

#include <iostream>
#include <type_traits>

template <typename T>
void process(const T& value) {
  if constexpr (std::is_integral<T>::value) {
    std::cout << "integral type" << std::endl;
  } else if constexpr (std::is_floating_point<T>::value) {
    std::cout << "floating type" << std::endl;
  } else {
    std::cout << "other type" << std::endl;
  }
}

int main() {
  process(10);       // 输出: integral type
  process(10.0);     // 输出: floating type
  process("Hello");  // 输出: other type
  return 0;
}

简化模板代码

在引入if constexpr之前, 实现基于类型的条件编译通常需要使用模板特化或 SFINAE(替换失败不是错误)技术, 这些技术不仅代码复杂, 而且对于初学者来说难以理解. if constexpr简化了这一过程, 使得基于类型条件的代码分支更加直观和易于编写.

// 以前的写法
template <int N>
constexpr int fact_old() {
  return N * fact_old<N - 1>();
}

template <>
constexpr int fact_old<0>() {
  return 1;
}

// C++17 的写法
template <int N>
constexpr int factorial() {
  if constexpr (N == 0) {
    return 1;
  } else {
    return N * factorial<N - 1>();
  }
}

避免无效代码实例化

在模板编程中, 某些代码路径可能对于特定的模板参数是无效的. 使用if constexpr可以确保只有有效的代码路径会被实例化, 从而避免编译错误.

#include <iostream>
#include <string>
#include <type_traits>

template <typename T>
void add(T a, T b) {
  if constexpr (std::is_integral<T>::value || std::is_floating_point<T>::value) {
    std::cout << a + b << std::endl;
  } else if constexpr (std::is_same<T, std::string>::value) {
    a.append(b);
    std::cout << a << std::endl;
  } else if constexpr (std::is_same<T, const char*>::value) {
    auto c = std::string(a);
    c.append(b);
    std::cout << c << std::endl;
  } else {
    std::cout << "Not an integer or a floating point number" << std::endl;
  }
}

int main() {
  add(1, 2);              // 3
  add(1.1, 2.3);          // 3.4
  add("Hello", "World");  // HelloWorld
  return 0;
}

consteval

consteval 是 C++20 中的一个新关键字, 用于定义只能在编译时计算的函数. consteval 强制函数只能在编译时计算, 而 constexpr 函数则可以在运行时调用.

consteval修饰函数

#include <utility>

// fib 函数用来求第n个Fibonacci数
consteval int fib(int n) {
  int a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    a = std::exchange(b, a + b);
  }
  return a;
}

int main() {
  // 输入要求
  fib(10);  // OK, fib接受字面量作为参数

  const int K = 10;
  fib(K);  // OK, fib接受const 类型值作为参数

  constexpr int cpr = 10;
  fib(cpr);  // OK, fib接受常量表达式

  int a = 10;
  // fib(a);  // Error, fib不接受普通变量作为参数

  // 返回值可以赋值给常量
  int r1 = fib(1);            // OK
  const int r3 = fib(2);      // OK
  constexpr int r2 = fib(3);  // OK

  // consteval int g6 = 1;  // Error, consteval 不能修饰变量

  return 0;
}

constevalconstexpr 的区别

特性constevalconstexpr
调用时机仅支持编译时调用支持编译时或运行时
编译器强制性检查强制编译时计算编译器不强制运行时调用
灵活性限制较多, 仅用于特定场景更加灵活通用
// consteval 示例
consteval int must_compile_time(int x) { return x * x; }

// constexpr 示例
constexpr int flexible_function(int x) { return x * x; }

int main() {
  for (int i = 0; i < 5; ++i) {
    flexible_function(i);  // OK, 运行时调用
    must_compile_time(i);  // Error: consteval 仅支持编译时调用
  }
  return 0;
}

constinit

constinit 是 C++20 中引入的一个新关键字, 用于确保变量在程序启动前完成初始化. 这对于需要在编译时就确定其值的全局或静态变量特别有用.

constinit 关键字的提出是为了解决如下的问题:

  1. 确定性初始化: 确保全局或静态变量在程序启动前完成初始化, 提供了一种明确的方式来声明这些变量的初始化时机, 从而避免了静态初始化顺序问题(SIOF).

  2. 性能优化: 与动态初始化相比, constinit确保了变量的初始化可以在编译时进行, 减少了运行时的开销. 这对于性能敏感的应用程序来说是一个重要的优势.

  3. 编译时检查: constinit要求变量必须在编译时就能初始化. 这种强制性的编译时检查可以避免运行时错误和不确定的行为, 提高了代码的安全性和可靠性.

  4. constexprconsteval的互补: constinit与 C++20 中的其他两个关键字constexprconsteval一起, 提供了一套完整的工具, 用于控制变量和函数的编译时行为. constexpr允许在编译时或运行时计算, consteval强制函数必须在编译时计算, 而constinit确保变量在程序启动前初始化.

constinit 的使用要求:

  1. 变量必须是全局变量或静态变量, 但不一定具有常量属性.
  2. 变量必须是用常量初始化的(常量字面值, constexprconsteval).
/* 全局变量 */
constinit int global_var = 1;           // OK
constinit static int global_max = 100;  // OK
constinit const int global_min = 10;    // OK
constinit int init_var = global_var;    // Error, 需要使用常量初始化

int main() {
  constinit static int static_var = 9;  // OK
  constinit int local_var = 8;          // Error, local_var 不是 static
  return 0;
}

三者对比

特性编译时计算运行时调用强制初始化时机适用范围
constexpr函数/变量
consteval函数
constinit变量