CTTCG 23 Metaprogramming

分类

现在值元编程已经能够很好地被 constexpr 函数取代。

类型元编程例子:std::chrono 中的时间单位用分数表示,两个时间单位的加和的单位可以在运行时确定——尽管分母很可能会变化。

混合元编程例子:

  1. 时间的相加。时间单位可以最终确定,但是由于给单位施加的量在运行时确定,所以需要运行时计算。同时除了量之外,其他的表达式都可以在编译期得到简化。
  2. 强制循环展开。比如:
template <typename T, std::size_t N>
struct DotProductT {
    static inline T result(T *a, T *b) {
        return *a * *b + DotProductT<T, N-1>::result(a+1, b+1);
    }
}
template <typename T>
struct DotProductT<T, 0> { // 偏特化
    static inline T result(T *, T *) {
        return T{};
    }
}

在这个例子中尽管 ab 两个数组的值是不知道的,但是能够知道迭代的次数(N),从而将 result 函数展开为一个非常长的计算表达式(这个函数本身是内联函数)。当然手动循环展开可能不如编译器决定更有效,而且 inline 也不一定能保证必定内联。

模板递归实例化的代价

对于二分查找的模板,递归复杂度按理来为 O(log(N))。但决定分支的时候需要注意:

static inline constexpr auto value = std::conditional<(N < mid * mid),
    Sqrt<N, LO, mid-1>::value, Sqrt<N, mid, HI>::value>;

上面直接访问 value 的做法会导致两边都全实例化(full instantiation)。结果不只是实例化数量加倍,而是实例化数量变成 O(N) 级别!因为不被选中的一边会继续向下实例化。所以要尽可能避免这种写法。

嵌套递归实例化的代价更大。嵌套递归实例化指的是上层递归使用了下层作为其参数中的一个。这样会导致模板名变得很长,在以前编译器对内部表示名没有特殊优化时这会通过更长的字符串加重编译器的负担。

enum

早期的模板元编程中 enum 是生成编译期间常量的唯一方式,因为枚举项一定是纯右值,不用担心传入接受常引用的函数中时编译器老老实实传地址不给优化。

C++11 加入了 constexpr,被广泛使用。C++17 允许 static inline,这解决了地址问题。