C++元编程
元编程:编写可以让编译系统生成新代码的代码,而新生成的代码,正式我们想要的代码。元编程的目的,用尽量少的付出,换取尽可能多的功能,“付出”可以用代码成本,维护成本之类的事情来衡量。
c++元编程的特性之一就是编译期间执行用户自定义的运算。
现在C++元编程的现状
值元编程
牛顿迭代法求平方根,源码如下:
#include <type_traits>
#include <cstdlib>
#include <iostream>
constexpr double _sqrt(double x) {
if (x == 1.0)
return 1.0;
double a = 1.0;
while (a * a - x > 0.000001 || a * a - x < -0.000001) {
a = (a + x / a) / 2;
}
return a;
}
int main(int argc, char **argv) {
static_assert(_sqrt(2.0) - 1.41421 < 0.00001 || _sqrt(2.0) - 1.41421 > -0.00001, ""); //OK
static_assert(_sqrt(1.0) == 1.0, ""); //OK
static_assert(_sqrt(2.0) == 1.41421, ""); //failed
return 0;
}
编译器计算函数需要注意一下两点点:
- 函数可以不是函数模板,但必须是constexpr函数
- _sqrt实参必须使用constexpr修饰,下面的代码无法通过编译(使用constexpr修饰即可):
//...
double a = 2.0;
static_assert(_sqrt(a) - 1.41421 < 0.00001 || _sqrt(a) - 1.41421 > -0.00001, "");
//...
- _sqrt实现中用到的函数必须使用constexpr修饰,下面的代码无法通过编译(可以自己实现一个constexpr函数取代abs):
//...
constexpr double _sqrt(double x) {
if (x == 1.0)
return 1.0;
double a = 1.0;
while (abs(a * a - x) < 0.000001) {
a = (a + x / a) / 2;
}
return a;
}
//...
类型元编程
std::remove_cv,std::remove_reference等等都属于类型元编程,其主要功能就是接受一个类型,输出一个新类型,我们可以通过递归的模板实例化,实现更加复杂的类型元编程,如下:
#include <type_traits>
#include <iostream>
template <typename T>
struct remove_all_extents { using type = T; };
template <typename T, std::size_t N>
struct remove_all_extents<T[N]> { using type = typename remove_all_extents<T>::type; };
template <typename T>
struct remove_all_extents<T[]> { using type = typename remove_all_extents<T>::type; };
int main(int argc, char **argv) {
static_assert(std::is_same<remove_all_extents<int[]>::type, int>::value, "");
static_assert(std::is_same<remove_all_extents<int[5][10]>::type, int>::value, "");
static_assert(std::is_same<remove_all_extents<int[][10]>::type, int>::value, "");
//error: static assertion failed due to requirement 'std::is_same<int (*)[5], int>::value':
std::cout << typeid(remove_all_extents<int(*)[5]>::type).name() << std::endl;
return 0;
}
混合元编程
通过值元编程和类型元编程,可以在编译期间计算数据值和数据类型,但我们更关心的是运行期间的效果,因此在代码运行期间,我们将源程序用在哪些需要类型和常量的地方。利用元编程的特性,构建运行期执行代码的方式,称为混合元编程(怕自己对混合元编程,下面将原文附上)。
通过使用数值元编程和类型元编程,可以在编译期间计算数值和类型。但是最终我们关心的 还是在运行期间的效果,因此在运行期间的代码中,我们将元程序用在那些需要类型和常量 的地方。不过元编程能做的不仅仅是这些:我们可以在编译期间,以编程的方式组合一些有 运行期效果的代码。我们称之为混合元编程。
下面是一个混合元编程的实例,实现的功能是计算向量的点积。
- 普通实现
template<typename T, std::size_t N>
auto dot_product(const std::array<T, N> &x, const std::array<T, N> &y) {
T result{};
for (std::size_t k = 0; k<N; ++k) {
result += x[k] * y[k];
}
return result;
}
- 混恶元编程实现
template<typename T, std::size_t N>
struct dot_product_t {
static inline T result(const T *a, const T *b) {
return *a * *b + dot_product_t<T, N-1>::result(a+1, b+1);
};
};
template<typename T>
struct dot_product_t<T, 0> {
static inline T result(const T *, const T *) { return T{}; }
};
template<typename T, std::size_t N>
auto dot_product(const std::array<T, N> &x, const std::array<T, N> &y) {
return dot_product_t<T, N>::result(x.begin(), y.begin());
}
这只是混合元编程的一个例子,并没有实际应用价值。但通过这个例子,我们会发现这种方式不适用于大型数据组,原因如下:
- 代码膨胀

- 递归次数限制

或许,std::tuple是混合元编程最成功的一个例子。
混合元编程与计量单位换算
现实生活中,我们经常会遇到不同单位的数据进行混合运算。举个例子:一个流程分为两个子流程,第一个子流程预计耗时1.25小时,第二个子流程预计耗时45分钟,整个流程预计耗时多少小时?对于这样的问题,我们一般会分为两步计算:
- 统一单位,第一个子流程耗时1.25 * 60 = 75(分),
- 进行计算,总耗时75 + 45 = 120(分)
- 换算成指定单位,120 * 60 = 2(小时)
处理上面的问题时,我们必须先通过单位换算统一单位,然后才能进行计算。在单位换算中,有两个非常重要的概念:基本单位和换算比率。在本例中,我们把分钟当做了基本单位,时对分的换算比率为60,把1.25小时换算成75分钟,然后使用分钟作为单位进行计算,计算完后再把单位换算成小时。
换算比率
在国际单位制中,任何一种单位都会有一个基本单位,其他单位都可以表示成基本单位的倍数,例如,时间的基本单位是秒,1分为60/1秒,1毫秒为1/1000秒。60/1和1/1000就是换算比例。为了表示换算比例,我们可以定义一个模板类ratio,如下:
/* N: numerator, D: denominator*/
template <unsigned N, unsigned D = 1>
struct ratio {
static constexpr unsigned num = N;
static constexpr unsigned den = D;
using type = ratio<N, D>;
};
N表示分子,D表示分母,ratio<60, 1>表示60倍,ratio<1, 1000>表示1/1000。除了ratio类,还必须定义与之相对应的运算,以方便运算:
template <typename R1, typename R2>
struct ratio_add_impl {
private:
static constexpr unsigned num = R1::num * R2::den + R2::num * R1::den;
static constexpr unsigned den = R1::den * R2::den;
public:
using type = ratio<num, den>;
};
template <typename R1, typename R2>
using ratio_add = typename ratio_add_impl<R1, R2>::type;
template <typename R1, typename R2>
struct ratio_subtract_impl {
public:
using type = typename ratio_add<R1, ratio<-R2::num, R2::den>>::type;
};
template <typename R1, typename R2>
using ratio_subtract = typename ratio_subtract_impl<R1, R2>::type;
为了减少篇幅,此处只实现了加减运算。下面考虑几个问题:
- 为什么要用类模板?
为了使用方便,不定义ratio对象就可以访问ratio::num和ratio::den,这两成员必须定义为静态成员;对于普通类(struct或class),所有实例共享静态成员,一旦ratio::num和ratio::den发生变化,所有的ratio对象都会收到影响,为防止这种情况,ratio必须定义成模板类;换算比率一旦确定,其值不应该在发生改变,因此ratio::num和ratio::den必须并定义成常量(const或constexpr);没有构造函数,而ratio::num和ratio::den又必须在定义时初始化,将ratio::num和ratio::den的初始值作为模板参数传入便成了最佳的选择。
- ratio运算为什么定义成为一个类型ratio_add_impl而函数?
R1和R2运算后,会产生一个新的类型R3,定义成类型可以方便地保存和使用新类型,而函数只能返回值,不能返回类型。
- 为什么ratio_add_impl的num,den要定义成私有成员?
num和den仅仅是运算过程的中间变量,为定义ratio_add_impl::type而生,无需暴露。
- ratio_add_impl::type和ratio_subtract_impl::type的定义使用typedef还是using?
书中(《C++模板 II》)和gcc中使用的是typedef,本文使用的是using,个人感觉使用typedef和using都可以,没有什么区别。
时间单位
ratio可以用于表示秒和其它时间单位的换算比例,但如果直接用于表示时间段则非常不方便,例如5分钟表示为ratio<300, 1>,100毫秒表示为ratio<100, 1000>,既不直观,也不方便。为了表示时间段,还需要定义一个类——duration:
template <typename T, typename U = ratio<1>>
class duration {
public:
using value_type = T;
using unit_type = typename U::type;
private:
value_type m_value;
public:
duration(value_type v = 0) : m_value(v) {}
//template<typename D, typename = std::void_t<decltype(&D::unit_type)>>
template <typename T2, typename U2>
duration(const duration<T2, U2> &d) {
m_value = d.value() * duration<T2, U2>::unit_type::num * unit_type::den / duration<T2, U2>::unit_type::den / unit_type::num;
}
constexpr value_type value(void) const { return m_value; }
};
//定义时间单位
using nanoseconds = duration<double, ratio<1, 1000000000>>;
using microseconds = duration<double, ratio<1, 1000000>>;
using milliseconds = duration<double, ratio<1, 1000>>;
using seconds = duration<double>;
using minutes = duration<double, ratio< 60>>;
using hours = duration<double, ratio<3600>>;
template <typename T1, typename U1, typename T2, typename U2>
constexpr auto operator+(const duration<T1, U1> &lhs, const duration<T2, U2> &rhs) {
using value_type = ratio<1, ratio_add<U1, U2>::den>;
auto value = lhs.value() * value_type::den / U1::den * U1::num
+ rhs.value() * value_type::den / U2::den * U2::num;
return duration<decltype(value), value_type>(value);
}
template <typename T1, typename U1, typename T2, typename U2>
constexpr auto operator-(const duration<T1, U1> &lhs, const duration<T2, U2> &rhs) {
using value_type = ratio<1, ratio_add<U1, U2>::den>;
auto value = lhs.value() * value_type::den / U1::den * U1::num
- rhs.value() * value_type::den / U2::den * U2::num;
return duration<decltype(value), value_type>(value);
}
duration定义需要两个模板参数:数值类型和换算比率。为了方便使用,我们可以直接定义seconds,minutes,hours等时间单位。注意,数值类型一定要选择一个表示范围比较大的类型,否则很容易溢出。最后粘贴一下使用代码:
int main(int argc, char **argv) {
using R1 = ratio<60, 1>;
using R2 = ratio<1, 1000>;
using R3 = ratio_add<R1, R2>;
std::cout << R3::num << "\t" << R3::den << std::endl;
using R11 = ratio<4, 10>;
using R22 = ratio<1, 10>;
using R33 = ratio_subtract<R11, R22>;
std::cout << R33::num << "\t" << R33::den << std::endl;
auto sec = seconds(50) - seconds(30);
std::cout << "sec = " << sec.value() << std::endl;
auto min = minutes(sec);
std::cout << "min = " << min.value() << std::endl;
auto msec = milliseconds(10000) - milliseconds(3000);
std::cout << "msec = " << msec.value() << std::endl;
sec = seconds(msec);
std::cout << "sec = " << sec.value() << std::endl;
}
c++标准库的std::ratio,std::duration与上面的定义类似,但c++标准库更加严谨。通过std::ratio不仅可以构建时间单位系统,还可以构建其他的单位系统,如长度,货币等等。
更多推荐


所有评论(0)