元编程本质上是在编写程序的过程中创造新的程序,或者说,它涉及编写一段代码,这段代码将在编译阶段被执行,以生成实现真正所需功能的新代码。这里的“反射属性”意味着元编程部件(metaprogramming component)是它为之生成代码的那个程序的一部分,也就是说,元编程创建或修改了原程序本身的某个部分。
元编程之所以被推崇,原因与大多数其他编程技术相同,旨在以较少的努力获得更多的功能,其中努力的程度可以用代码量、维护成本等多种指标衡量。元编程的独特之处在于,一些用户自定义的计算过程发生在编译时(translation time)。
元编程的主要驱动力有两个方面:
性能优化: 编译时计算的特性意味着在编译阶段就能进行计算和优化,编译器可以对这类计算进行分析和优化,有些计算甚至可以被彻底消除,从而在运行时提高程序的性能。例如,通过模板元编程,可以预先计算出一些固定值或数据结构,避免了在运行时重复计算的开销。
接口简洁性: 通过元编程技术,程序员可以编写更简洁、更具一般性的接口,这些接口在编译时会展开成为更具体、更复杂的实现。比如,一个元编程辅助库可能只需要几行代码就可以定义出一个能处理多种类型和操作的通用接口,而不用为每一种类型或操作分别编写大量重复的代码。所以,元编程产生的代码通常比它展开后的代码更短小精悍,从而简化了接口设计和使用,降低了代码冗余和维护难度。
常用元编程方法
Value Metaprogramming
C++新特性的引入,让针对值的元编程变得更加简单,尤其是C++14以后,引入constexpr:
template<typename T>
constexpr T sqrt(T x) {
if(x <= 1) { return x; }
T lo = 0, hi = x;
while(true) {
auto mid = (hi+lo)/2, midSquared = mid*mid;
if (lo+1 >= hi || midSquared == x) {
return mid; //找到了平方根
}
// continue with the higher/lower half-interval:
if (midSquared < x) {
lo = mid;
}
else {
hi = mid;
}
}
}
int main() {
static_assert(sqrt(25) == 5, "1");
static_assert(sqrt(40) == 6, "2");
long long l = 53478;
std::cout << sqrt(l) << "\n"; // 运行时执行
}
对于一些编译期无法推断的情况,sqrt也会在运行期推理。
Type Metaprogramming
比如,对数组类型进行萃取,会用到递归调用的技巧:
#include <iostream>
// 主模板,用于生成给定的类型
template<typename T>
struct RemoveAllExtentsT {
using Type = T;
};
// 对于 array 的偏特化, 带边界的
template<typename T, std::size_t SZ>
struct RemoveAllExtentsT<T[SZ]> {
// 如果 T 是 array,则继续递归调用 RemoveAllExtentsT<T>
// 直到 T 不是 array,即 T 是基本类型
using Type = typename RemoveAllExtentsT<T>::Type;
// using Type = T;
};
// 对于 array 的偏特化, 不带边界的
template<typename T>
struct RemoveAllExtentsT<T[]> {
using Type = typename RemoveAllExtentsT<T>::Type; // why ?
// using Type = T;
};
template<typename T>
using RemoveAllExtents = typename RemoveAllExtentsT<T>::Type;
int main() {
// yields int
std::cout << "The type after removing all extents is: "
<< typeid(RemoveAllExtents<int[]>).name() << '\n';
// yields int
std::cout << "The type after removing all extents is: "
<< typeid(RemoveAllExtents<int[5][10]>).name() << '\n';
// yields int
std::cout << "The type after removing all extents is: "
<< typeid(RemoveAllExtents<int[][10]>).name() << '\n';
// yields int(*)[5]
// int(*)[5] 是一个指向固定大小的一维数组的指针
// 所以推理走的是主模板
std::cout << "The type after removing all extents is: "
<< typeid(RemoveAllExtents<int(*)[5]>).name() << '\n';
}
Hybrid Metaprogramming
我们可以在编译时以编程方式组装具有运行时效果的代码,即混合元编程。
比如,计算两个array的点积:
#include <array>
#include <iostream>
// 计算点积constexpr修饰,可以在编译期计算
template<typename T, std::size_t N>
constexpr auto dotProduct(std::array<T,N> const& x, std::array<T,N> const& y) {
// 现代编译器会将循环优化为对目标平台最有效的任何形式。
T result{};
for (std::size_t i = 0; i < N; ++i) {
result += x[i] * y[i];
}
return result;
}
int main() {
constexpr std::array<int, 3> x{1, 2, 3};
constexpr std::array<int, 3> y{4, 5, 6};
std::cout << dotProduct(x, y) << std::endl;
}
设计一个Ratio:
#include <array>
#include <iostream>
// 分数
template<unsigned N, unsigned D = 1>
struct Ratio {
static constexpr unsigned num = N; // 分子
static constexpr unsigned den = D; // 分母
using Type = Ratio<num, den>;
};
// add 操作
template<typename R1, typename R2>
struct RatioAddImpl {
private:
static constexpr unsigned den = R1::den * R2::den;
static constexpr unsigned num = R1::num * R2::den + R2::num * R1::den;
public:
using Type = Ratio<num, den> ;
};
template<typename R1, typename R2>
using RatioAdd = typename RatioAddImpl<R1, R2>::Type;
int main() {
using R1 = Ratio<1,1000>;
using R2 = Ratio<2,3>;
using RS = RatioAdd<R1,R2>;
std::cout << RS::num << "/" << RS::den << std::endl;
using RA = RatioAdd<Ratio<2,3>,Ratio<5,7>>; // RA has type Ratio<29,21>
std::cout << RA::num << "/" << RA::den << std::endl; // prints 29/21
}
#include <array>
#include <iostream>
// 分数
template<unsigned N, unsigned D = 1>
struct Ratio {
static constexpr unsigned num = N; // 分子
static constexpr unsigned den = D; // 分母
using Type = Ratio<num, den>;
};
// add 操作
template<typename R1, typename R2>
struct RatioAddImpl {
private:
static constexpr unsigned den = R1::den * R2::den;
static constexpr unsigned num = R1::num * R2::den + R2::num * R1::den;
public:
using Type = Ratio<num, den> ;
};
template<typename R1, typename R2>
using RatioAdd = typename RatioAddImpl<R1, R2>::Type;
// duration type for values of type T with unit type U:
template<typename T, typename U = Ratio<1>>
class Duration {
public:
using ValueType = T; // 值类型
using UnitType = typename U::Type; // 单位类型
private:
ValueType val;
public:
constexpr explicit Duration(ValueType v = 0): val(v) {}
constexpr ValueType value() const {return val;}
};
// operator+ 实现两个Duration 相加
template<typename T1, typename U1, typename T2, typename U2>
auto constexpr operator+(Duration<T1,U1> const& lhs, Duration<T2,U2> const& rhs) {
using VT = Ratio<1,RatioAdd<U1,U2>::den>; // 获取最终的比率
auto val = lhs.value() * VT::den / U1::den * U1::num \
+ rhs.value() * VT::den / U2::den * U2::num;
return Duration<decltype(val), VT>(val); // 返回两个Duration相加
}
int main() {
constexpr int x = 42;
constexpr int y = 77;
auto a = Duration<int,Ratio<1,1000>>(x);
auto b = Duration<int,Ratio<2,3>>(y);
auto c = a + b; // 编译期计算类型,运行期计算值,c = a*3 + b*2000
// 如果值都是constexpr修饰的话,其实值也可以在编译期计算出来的
std::cout << c.value() << std::endl;
}
反射元编程
元编程方案的三个维度:
- 计算
- 反射
- 生成
反射是以编程方式检查程序特征的能力。生成是指为程序生成额外代码的能力。
计算的两种实现方式:递归实例化和constexpr评估, 上文已经介绍
反射的实现:部分依靠traits
递归实例化的成本
模板实例化并不便宜:即使是相对适度的类模板也可以为每个实例分配超过千字节的存储空间,在编译完成之前,该存储无法回收。
可以使用if_then_else 来缓解上述问题,为类模板实例定义类型别名不会导致C++编译器实例化该实例的主体