实现 constexpr 数学函数(元编程 / 改Clang源码)

动机

constexpr 是个好东西,到了C++20,它条件又放宽了很多,甚至动态分配内存都可以在编译时进行。然而,cmath(math.h),里面的数学函数因为有副作用(errno或fenv异常)导致不能是constexpr。

解决方案

写个编译期数学库

想要在编译期做数学运算的的话可以自己写库,简单搜索一下Github,已经有了gcem,static_math

只要熟悉数值算法,不需要太多的元编程知识也能写出上述的编译期数学函数。可惜的是constexpr的限制意味着无法使用汇编,所以写出来的数学函数不可能与高度优化的数学库相提并论。这不是什么大问题,因为C++程序员(尤其是酷爱模板的程序员)绝不会介意编译时间多一点。最大的问题在于,constexpr一个好处就是编译时运行时都可用,除非肯定这些函数只在编译期用,因为运行期运算回退到低效的数学函数是任何使用C++的程序员都无法接受的。

GCC的扩展 __builtin_constant_p 或者 C++20 的 is_constant_evaluated 可以用于解决这个问题,探测求值语境选择合适算法。

举个例子,写一个开平方函数,先请出牛顿法:方程\(f(x)=0\)的根的近似值x可以由\(x'= x-\frac{f(x)}{f'(x)}\)迭代求得,迭代次数越多精度越高,所以对于开平方函数sqrt(a),构造\(f(x)=x^2-a\)求根即可。

constexpr double sqrt_slow(double a) {
double x = a;
for (int i = 0; i < 8; i++)
x = (x + a / x) / 2; // 牛顿法迭代八次,因为constexpr的限制著名的卡马克魔法数也不能用
return x;
}

constexpr double sqrt_adapter(double a) {
if (__builtin_is_constant_evaluated())
return sqrt_slow(a);
else
return sqrt(a);
}

constexpr double sqrt_adapter_gcc_only(double a) { // 至少需要GCC支持C++14的constexpr,不推荐使用
if (__builtin_constant_p(a)) // Clang的这个函数和GCC行为不同 https://reviews.llvm.org/D35190
return sqrt_slow(a);
else
return sqrt(a);
}

int main() {
constexpr double q = sqrt_adapter(2.);
double d;
std::cin >> d;
std::cout << sqrt_adapter(d);
}

等新标准

p0533r0,p1383r0似乎没什么动静,且不说C++23,恐怕猴年马月也不一定有。

自己动手丰衣足食

尝试下面的代码对GCC来讲并没有什么压力:

int main() {
constexpr double q = sqrt(2); // 仅GCC可用
}

搜索了一下又是GCC扩展,<cmath>里的函数支持了constexpr,这些浮点函数在有浮点错误时由编译器报出is not a constant expression错误,这似乎一定程度上就解决了问题(p1383r0里讨论了其他方面的潜在问题)。

要命的是Clang不支持这个扩展。想要只能自己动手,事后发现,Clang源码比想象中的要好改。

repo拉下来,花上不少时间找到是这个文件clang/lib/AST/ExprConstant.cpp负责对常量表达式求值,于是照龙画蛇,找里面已经支持 constexpr 的 __builtin 函数观摩一番,然后把不支持的补上,浮点错误则让求值失败,就可以使内建函数支持 constexpr。这里有一个patch,可以参考里面的内容。

现在内建函数(如 __builtin_sqrt)是可以用在常量求值语境里了,但是如果希望<cmath>里的函数也可以,还要在libc++的<cmath>里受支持的函数签名加上constexpr,如果clang使用--stdlib=libstdc++则不需要此修改。

int main() {
constexpr double q = __builtin_sqrt(2); // 修改版Clang也能用
// constexpr double t = sqrt(2); // 还需要改libc++
}

如何构建 Clang 参照官方文档即可。

分享到