编译时数值积分






4.84/5 (19投票s)
你甚至还没来得及抬起手指,计算结果就摆在你面前了。
简要前言
这是我第二次尝试实现基本的编译时数值积分方法。在第一篇文章的版本中,积分是在 main()
函数之前完成的,但实际上并非在编译时完成。我非常感谢迄今为止收到的所有建设性的意见和反馈。我希望这次一切都会顺利……
引言
自 C++11 以来,我们可以在编译时执行相当复杂的计算。在本篇文章中,我将展示如何计算任意实函数在任意边界下的面积。我将使用标准且广泛的算法,例如梯形法则和辛普森法则。
编译时计算意味着,在程序加载到内存时,远在进入 main
函数之前,操作的结果就已经知道了。
main 函数的魔力
看看这段代码(仅自 C++11 起有效)
#include <cmath>
#include <iostream>
#include "simpsons_rule.h"
constexpr double PI=4.0*atan(1.0); // PI number
struct my_function
{
constexpr static double f(double x) {return cos(x);}
};
struct left_boundary
{
constexpr static double value=0.0;
};
struct right_boundary
{
constexpr static double value=PI/2.0;
};
int main(int argn, char *argc[])
{
constexpr double partition_size=100;
// Application of the Simpson's rule.
// my_function::f is integrated from left_boundary::value to
// right_boundary::value with a partition size of partition_size.
// A recursive technique is used.
constexpr double simpsons_rule_result=
simpsons_rule<
my_function,
left_boundary,
right_boundary,
partition_size
>::result;
std::cout << "Simpson's rule: " << simpsons_rule_result << std::endl;
static_assert(fabs(simpsons_rule_result-1.0)<0.001, "Failure"); // [1]
return 0;
}
上面的程序只是显示了属于 simpsons_rule<>
类的静态常量表达式 (constexpr) result
的值。这个值在编译时计算,并在访问 main()
之前存储在内存中。result
存储的是函数 my_function::f
在 0.0 (left_boundary::value
) 和 PI/2.0 (right_boundary::value
) 之间的积分。精确结果是 1。显示的结果也是 1。因此,在这种情况下,近似值几乎是完美的(考虑舍入误差)。
如果您现在更改 my_function::f
、left_boundary::value
或 right_boundary::value
并重新编译,您将获得 result
的不同值。
所以,这就是问题的关键和魔力所在:运行时不进行任何计算;唯一执行的任务就是显示结果。要证明这一点,只需像这样更改表达式 [1]
static_assert(fabs(simpsons_rule_result-1.0)>0.001, "Failure"); // [1] Changed "<" into ">".
如果您现在尝试重新编译,编译将失败,因为积分误差实际上小于 0.001。
现在是时候看看内部机制了,但与其深入研究梯形法则或辛普森法则的实现,不如展示一个更简单(但类似)的案例:阶乘。
实现阶乘 (1)
要计算整数 n 的阶乘(表示为 n!),只需应用以下公式
下面的代码实现了编译时阶乘计算(仅自 C++11 起有效)
#ifndef FACTORIAL_H #define FACTORIAL_H #include <cstddef> template <size_t N> // [1] struct factorial { constexpr static size_t result=N*factorial<N-1>::result;; }; template <> struct factorial<1> // [2] { constexpr static size_t result=1; }; #endif
上面的代码声明了一个名为 factorial<>
的类模板 [1] 以及一个特化版本 factorial<1>
[2]。在 [1] 中,为 factorial<>
定义了静态 constexpr result
。在 [2] 中,为 factorial<1>
定义了静态 constexpr result
。
现在假设您在 main 函数中写下这句话
std::cout << factorial<5>::result << std::endl;
如您在 [1] 中所见,编译器将 factorial<5>::result
解析为
factorial<5>::result = 5 * factorial<4>::result
同样在 [1] 中,factorial<4>::result
被解析为
factorial<4>::result = 4 * factorial<3>::result
沿着这个思路递归下去,我们得到
factorial<5>::result = 5 * 4 * 3 * 2 * factorial<1>::result
factorial<1>::result
在 [2] 中被解析为 1。我们因此得到
factorial<5>::result = 5 * 4 * 3 * 2 * 1
编译器最终执行了上面的乘法,factorial<5>::result
在编译时被解析为 120。
实现阶乘 (2)
下面的代码是一个不太为人所知的替代方案(仅自 C++11 起有效)
#ifndef FACTORIAL_H #define FACTORIAL_H #include <cstddef> // With the following definition, factorial<N> is recursively // resolved as factorial<N, 1, 1, 2, 3, ..., N>. template <size_t N, size_t M=N+1, size_t... Indices> struct factorial : public factorial<N, M-1, M-1, Indices...> // [1] {}; // The sequence above is split into the First index {1} plus // the Rest...: {2, 3, ..., N}. template <size_t N, size_t First, size_t... Rest> struct factorial<N, 1, First, Rest...> // [2] { constexpr static size_t result= First*factorial<N, 1, Rest...>::result; // (Second factor evaluated at [2]) }; // And finally, this specialization matches the case in which // all indices have been consumed. template <size_t N> struct factorial<N, 1> // [3] { constexpr static size_t result=1; }; #endif
在这种情况下,编译器如何解析 factorial<5>::result
?如果您看一下 [1],您会发现 factorial<5>
可以解析为 factorial<5, 5, 5>
,其中 Indices…
是一个空包。在不离开 [1] 的情况下,您可以沿着链条进行
factorial<5> -> factorial<5, 5, 5>, with Indices... empty.
factorial<5, 5, 5> -> factorial<5, 4, 4, 5>, with Indices... = {5}.
factorial<5, 4, 4, 5> -> factorial<5, 3, 3, 4, 5>, with Indices... = {4, 5}.
factorial<5, 3, 3, 4, 5> -> factorial<5, 2, 2, 3, 4, 5>, with Indices... = {3, 4, 5}.
factorial<5, 2, 2, 3, 4, 5> -> factorial<5, 1, 1, 2, 3, 4, 5>, with Indices... = {2, 3, 4, 5}.
一旦编译器获得 factorial<5, 1, 1, 2, 3, 4, 5>
,它就可以通过 [2] 中的特化版本进行解析。它变成了 factorial<5, 1, First, Rest…>
,其中 First
=1,Rest…
是包:{ 2, 3, 4, 5}。
上述技术非常有趣,因为它允许我们获取计算中涉及的操作数的索引包。对于阶乘,索引和操作数(要相乘的数字)是相同的,但这只是一个特例。
请注意,表达式 factorial<N, 1, Rest…>::result
在 [2] 中被求值。
当 Rest…
为空时,达到终止条件。这种情况在 [3] 中实现。
经过上述步骤,编译器最终将 factorial<5>::result
解析为 120。正如您所料,这两种方法都会得到相同的二进制输出。
一个晦涩的斐波那契数列
作为另一个说明性示例,您可以使用上述技术来计算斐波那契数列中的一个元素。如果它看起来很奇怪,请对我宽容一些
#ifndef FIBONACCI_H #define FIBONACCI_H #include <cstddef> template < size_t SequenceSize, // 1, 2, 3,... size_t CurrentSequenceSize = 0, size_t M = 1, size_t N = 0, size_t... Series > struct fibonacci : public fibonacci<SequenceSize, CurrentSequenceSize + 1, M + N, M, N, Series...> {}; template <size_t SequenceSize, size_t M, size_t N, size_t First, size_t... Rest> struct fibonacci<SequenceSize, SequenceSize, M, N, First, Rest...> { constexpr static size_t value=First; }; #endif
如果您在 main
函数中输入(例如)fibonacci<5>::value
,它将被解析为 3(序列中的第五个元素)。我同意,这当然不是计算斐波那契数列最直观的方法。
梯形法则
请注意,n 的阶乘可以示意性地写为
其中 f(x)=x,{i1, i2,…, in} 是一个索引集合,其值范围从 1 到 n。
梯形法则可以组织成类似的模式
其中:
在下面的代码中,summation<>
类模板负责 g() 的求和。
// trapezoidal_rule<> integrates a function F::f(x) between // boundaries A::value and B::value with a partition size of // N by means of the trapezoidal rule. See: // // http://en.wikipedia.org/wiki/Trapezoidal_rule // // Computations are intended to be done at compile time. // // Legend: // F::f: integrand (function to be integrated) // A::value: left boundary // B::value: right boundary // N: partition size // I: initially equal to N-1. It is then // recursively decreased down to 0. // #ifndef TRAPEZOIDAL_RULE_H #define TRAPEZOIDAL_RULE_H #include <cstddef> // // Class: summation<> // template <typename F, typename A, typename B, size_t N, size_t I=N-1> struct summation { constexpr static double result= F::f(A::value+I*(B::value-A::value)/N)+summation<F, A, B, N, I-1>::result; }; template <typename F, typename A, typename B, size_t N> struct summation<F, A, B, N, 0> { constexpr static double result=0.0; }; // // Class: trapezoidal_rule<> // template <typename F, typename A, typename B, size_t N> struct trapezoidal_rule { constexpr static double result= (B::value-A::value)/N*(0.5*(F::f(A::value)+F::f(B::value))+summation<F, A, B, N>::result); }; #endif
上面的代码与阶乘的第一种实现方式类似。如果您想寻找与第二种实现方式的相似之处,这里是代码
// trapezoidal_rule_2<> yields the same result as trapezoidal_rule<> // but it uses an indices unrolling technique instead of recursion. // // Legend: // F::f: integrand (function to be integrated) // A::value: left boundary // B::value: right boundary // N: partition size // M: initially equal to N. Later it is unrolled // into the sequence: {1, 1, 2, 3, ..., N-1}. // #ifndef TRAPEZOIDAL_RULE_2_H #define TRAPEZOIDAL_RULE_2_H #include <cstddef> // // Class: summation_<> // // With the following definition, summation_<F, A, B, N> is recursively // resolved as summation_<F, A, B, N, 1, 1, 2, 3, ..., N-1>. template <typename F, typename A, typename B, size_t N, size_t M=N, size_t... Indices> struct summation_ : public summation_<F, A, B, N, M-1, M-1, Indices...> // [1] {}; // The sequence above is split into the First index {1} plus the Rest...: {2, 3, ..., N-1}. template <typename F, typename A, typename B, size_t N, size_t First, size_t... Rest> struct summation_<F, A, B, N, 1, First, Rest...> // [2] { constexpr static double result= F::f(A::value+First*(B::value-A::value)/N)+summation_<F, A, B, N, 1, Rest...>::result; // (Second addend evaluated at [2]) }; // And finally, this specialization matches the case in which all indices have been consumed. template <typename F, typename A, typename B, size_t N> struct summation_<F, A, B, N, 1> { constexpr static double result=0.0; }; // // Class: trapezoidal_rule_2<> // template <typename F, typename A, typename B, size_t N> struct trapezoidal_rule_2 { constexpr static double result= (B::value-A::value)/N*(0.5*(F::f(A::value)+F::f(B::value))+summation_<F, A, B, N>::result); }; #endif
在我们理解了两种阶乘实现之后,我认为这两个最后一段代码是清晰、注释良好且自明了的。
辛普森法则(一种更精确的积分方法)的实现遵循相同的思路,但稍微冗长一些,因为它源于一个更复杂的公式。您可以在随附的 zip 文件中找到它。
源代码
梯形法则和辛普森法则的实现代码已附上,以及一个 main 示例代码。代码是用 Code::Blocks 编写的,并使用 MinGw gcc (v.4.8.1-4) 编译。
参考文献
- 梯形法则。( http://en.wikipedia.org/wiki/Trapezoidal_rule)。
- 辛普森法则。( http://en.wikipedia.org/wiki/Simpson's_rule)。
- 编译时函数执行。( http://en.wikipedia.org/wiki/Compile_time_function_execution)。
- Constexpr - C++11 中的通用常量表达式。( http://www.cprogramming.com/c++11/c++11-compile-time-processing-with-constexpr.html)。
- “解包”元组以调用匹配的函数指针。( http://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer)。