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

发布于:2024-06-28 ⋅ 阅读:(22) ⋅ 点赞:(0)

constexpr

constexpr是C++中的一个关键字,用于定义常量表达式。这些表达式在编译时就可以计算出结果,而不是在运行时。

constexpr

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

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

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

constexpr int size = 10;
int c_arr[size] = {0};              // OK
std::array<int, size> array = {0};  // OK

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

constexpr double root_of_2 = 1.41421356237;
constexpr const char* hello = "Hello, World!";
constexpr std::string_view str_view{hello};

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

// constexpr 类实例
class Point {
 public:
  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};
constexpr Point dst{1, 1};

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);  // Error, e 不是常量. 参数必须是常量

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

  return abs(0);
}

if constexpr编译时分支

if constexpr引入的主要原因是为了提高C++模板编程的能力和效率,具体包括:

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

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

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

  4. 优化性能:由于if constexpr在编译时就决定了代码的执行路径,它可以帮助编译器生成更优化的代码。对于不满足条件的分支,由于它们根本不会被编译,因此可以减少最终程序的大小,并提高运行时性能。

  5. 增强代码可读性和维护性if constexpr使得条件编译的意图更加明显,提高了代码的可读性。同时,由于减少了模板特化和SFINAE的需要,也使得代码更容易维护。

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

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

template <typename T>
void printPositiveOrNegative(const T& value) {
  if constexpr (std::is_signed<T>::value) {
    if (value < 0) {
      std::cout << "Negative" << std::endl;
    } else {
      std::cout << "Positive or zero" << std::endl;
    }
  } else {
    std::cout << "Always positive or zero" << std::endl;
  }
}

template <int N>
constexpr int factorial() {
  if constexpr (N == 0) {
    return 1;
  } else {
    return N * factorial<N - 1>();
  }
}

template <typename T>
void printOrCompute(const T& value) {
  if constexpr (std::is_same<T, std::string>::value) {
    std::cout << "String: " << value << std::endl;
  } else if constexpr (std::is_arithmetic<T>::value) {
    std::cout << "Arithmetic result: " << (value + value) << std::endl;
  } else {
    std::cout << "Other type" << std::endl;
  }
}

int main() {
  process(1);        // Processing integral type: 1
  process(3.14);     // Processing floating point type: 3.14
  process("hello");  // Processing other types

  printPositiveOrNegative(-1);  // Negative
  printPositiveOrNegative(0);   // Positive or zero
  printPositiveOrNegative(1u);  // Always positive or zero

  constexpr int result = factorial<5>();
  std::cout << "5! = " << result << std::endl;  // 5! = 120

  using namespace std::string_literals;  // 使用 string 字面量, 下一行才能使用
                                         // "hello"s
  printOrCompute("hello"s);  // String: hello
  printOrCompute("world");   // Other type
  printOrCompute(1);         // Arithmetic result: 2
  printOrCompute(3.14);      // Arithmetic result: 6.28
  return 0;
}

consteval

consteval 是C++20中的一个新关键字,用于定义只能在编译时计算的函数。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;
}

const int K = 10;
constexpr int cx = fib(0);  // OK
const int c1 = fib(0);      // OK
int g1 = fib(10);           // OK
int g2 = fib(K);            // OK, K 是常量
int g3 = fib(cx);           // OK, cx 是常量表达式

int g5 = fib(g1);      // Error, g1 不是常量
consteval int g6 = 1;  // Error, consteval 不能修饰变量

int main() { return fib(0); }

constinit

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

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

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

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

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

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

consinit 的使用要求:

  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;
}