递归(非缓存)斐波那契的玩具问题可以实现如下:
#include <iostream>
int fibonacci(int n) {
if (n <= 1)
return n;
else
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
int N = 5;
int result = fibonacci(N);
std::cout << "Fibonacci(" << N << ") = " << result << std::endl;
return 0;
}
这个程序的输出是
Fibonacci(5) = 5
。使用元编程,这可以在编译期间进行评估:
#include <iostream>
consteval int fibonacci(int n) {
if (n <= 1)
return n;
else
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
constexpr int N = 10;
constexpr int result = fibonacci(N);
std::cout << "Fibonacci(" << N << ") = " << result << std::endl;
return 0;
}
但是为什么有必要让程序更冗长呢?编译器不能分析第一个程序,并弄清楚输出总是 5 吗?
编译器经常 在编译时通过函数内联和持续传播,即使没有
consteval
强制它。但他们不是被迫的,所以它不会发生在未优化的调试版本中。
令人惊讶的是,在这种情况下,即使 N=3 或更高,GCC 或 clang 也不会发生这种情况。 https://godbolt.org/z/6j7T1c5WY
我想内联递归函数的默认启发式即使在
-O3
也不愿意走得足够远,尽管 GCC -O3
确实气球 fibonacci
到相当大的代码大小。 GCC 和 clang 将其中一个递归转换为循环,但 GCC 走得更远。我不确定它对所有这些代码到底做了什么,以及为什么它不将 fibonacci(3)
评估为没有 consteval
的编译时常量。
既然
constexpr
的存在是为了让程序员编写像 int arr[foo(N)]
这样的程序,那么将其扩展为一种获得 guaranteed 持续评估的方法是很自然的,即使在 not 需要它的情况下也是如此。 (例如,数组维度或模板参数以外的东西。)
以前程序员不得不使用模板元编程来保证他们想要计算的东西没有运行时开销,
consteval
让他们使用普通代码,利用 constexpr
所依赖的相同编译器功能,包括在调试版本中。
constexpr
本身存在是因为 C++ 委员会希望程序根据标准有效或无效,而不取决于给定编译器能够优化的程度。如果你想使用int arr[foo(N)]
,你需要一个保证返回值是一个常量表达式。一些编译器能够将 foo(N)
解析为编译时间常量而其他编译器不能的事实将是一个问题。甚至是非优化构建中的同一个编译器;只能在启用优化的情况下编译的代码不好。
那么为什么要使用
consteval
?您想要一个 guaranteed 常量表达式,还是仅仅检查您关心的某些编译器是否能够在启用优化时在您的用例中很好地优化?通常后者对于大多数用例来说就足够了。
这是一种告诉编译器通过某些代码进行常量传播的方法,如果它持续足够长的时间,肯定会产生编译时常量。通常编译器不知道这一点,所以他们会在一些启发式限制后退出。
(除了性能之外还有很多
constexpr
的用例,但我不太确定consteval
。)