目录
术语、元函数
【元编程介绍】
- 元编程:将各种计算从运行期提前至编译器进行以达到提供程序运行时性能提升的目的,是一种增加程序编译时间从而提升程序运行效率的一种编程技术。
- 会涉及到很多与循环有关的代码,很多操作针对的是类型、常量,此种循环的实现往往会采用递归手段。
- 典型范例:typelist(类型列表)、tuple(元组)。
- Boost库有一个叫做MPL(Meta-Programming Library)库,用于辅助模板元编程。
- 参考书籍:《C++模板元编程》书籍,对MPL有比较详细的介绍。
- GitHub上的MPL11元编程库:使用了C++11新标准语法对Boost中的MPL进行了改写。
术语
【元编程概念】
- Meta Programming,也叫模板元编程(tmplate meataprogramming),可以理解为一种编程手法,用来实现一些比较特殊的功能。
- 一般和 “递归”这个词有比较密切的联系。
- 多数都会在元编程中使用到递归编程技术。
- 元编程随着C++新标准的推出,也在不断发展,能力越来越强大。
【模板编程主要两个方面】
- 泛型编程(generic programming),“通用”,程序员不需要关心具体类型
- 元编程(meat programming),突出一种程序设计技巧达到常规编程难以达到的效果。例如前面的std::remove_all_extents类模板的实现。
- 这种技巧注重模板在实例化过程中的一些推导过程,而这些推导过程恰恰是解决问题和体现程序设计技巧的过程。
- 此外,元编程手法还可以让某些计算在程序的编译期间完成,提高了程序运行的性能。
【建议】
- 将元编程直接理解为泛型编程。
- 元编程只是在编程方式上比传统的泛型编程更特殊一点。
元函数
【元函数概念】
- 元函数是能在程序编译期间被调用和执行的函数(编译期间就能得到结果)。
- 元编程的核心也正是这些元函数。
【小结】
- 所谓元编程就是用这些元函数进行编程,甚至可以说,书写和使用这些元函数的过程本身就是在进行元编程。
【C++11 引入 constexpr关键字用于在编译的时候求值】
- constexpr 自带inline属性 -- C++17中引入
【示例】
//元函数 constexpr int myfunc(int abc) { return abc * 2; }
- 编译期间调用
编译期间调用myfunc constexpr int var = 11 * myfunc(12); static_assert(var == 264, "std error"); //静态断言,编译期间断言
- 上面两个constexpr缺一个就会报错
- 运行期间调用
int var2 = 12; int var3 =myfunc(var2);
【VS中进行设置】
解决方案 --> 属性
数值元函数
- 有constexpr关键字,返回为数值型
//元函数(数值元函数) constexpr int myfunc(int abc) { return abc * 2; }
【数值元函数:编译期间能够被调用的类模板】
【案例1:enum枚举值方式】
template <int x_v ,int y_v> struct Calc { enum { addvalue = x_v + y_v, subvalue = x_v - y_v }; };
- 调用:
const int var3 = Calc<12, 15>::addvalue; //相当于函数调用一样 static_assert(var3 == 27, "sth error"); //编译期间类型断言 cout << var3 << endl; cout << Calc<12, 15>::subvalue << endl;
- 用枚举类型值当做常量来使用的一种工作机制
- 输出
【案例2:静态常量方式】
template <int x_v, int y_v> struct Calc { static const int addvalue = x_v + y_v; //const可以用constexpr代替 };
- 在某些情况下,在使用了Cal类模板后,编译器对Cal进行实例化。
- 如果通过静态常量方式,编译器可能为addvalue 分配内存空间;使用枚举方式,编译器不会为addvalue 变量分配内存空间。
void testfunc(const int &tmpvalue)//左值引用,传过来的变量必须有一个内存空间 { return; }
- 调用
testfunc(Calc<12, 15>::addvalue);
- 编译器实例化Cal,并为addvalue分配内存空间
- 如果使用枚举,编译器就不会为addvalue分配内存空间,因为枚举类型不是左值,不存在地址,编译器只会为形参tmpval分配临时地址,不会为addvalue分配地址,所以一般倾向于使用enum。
【在编译期间计算5的阶乘:1*2*3*4*5 = 120】
//计算n的阶乘的泛化版本 template <int n_v> struct Factorial { enum { value = n_v * Factorial<n_v - 1>::value }; }; //计算n的阶乘的特化版本,用于做递归调用的出口 template <> struct Factorial<1> { enum { value = 1 }; };
- 调用
cout << Factorial<5>::value << endl; // Factorial<5>::value理解成函数调用 static_assert(Factorial<5>::value == 120, "sth error"); //静态类型断言 int tempvalue = Factorial<5>::value; //此时value值已经计算出来了,通过Vs2017反汇编可以查看
【数值元函数:constexpr修饰的函数】
constexpr int Factorial(int n_v) { return n_v <= 1 ? 1 : (n_v * Factorial(n_v - 1)); }
- 调用
cout << Factorial(5) << endl; static_assert(Factorial(5) == 120, "sth error"); constexpr int tempvalue = Factorial(5);
- 如果tempvalue 前不加constexpr,则会实例化函数进行调用计算,加上constexpr,则在编译期间将值计算好。
- 也可以写成循环:
constexpr int Factorial(int n_v) { int result = 1; for (int i = 1; i <= n_v; ++i) { result *= i; } return result; }
【数值元函数:constexpr修饰的变量模板(C++17)】
//变量模板(其实result代表的是常量),看起来也像函数调用,也是元函数的一种形式 //泛化版本 template<int Arg> constexpr int result = Arg * result<Arg - 1>; //特化版本 template <> constexpr int result<0> = 1;
- 调用
cout << result<5> << endl;
【函数调用方式】
//泛化版本 template<int Arg> constexpr int result() { return Arg * result<Arg - 1>(); } //特化版本 template<> constexpr int result<0>() { return 1; }
- 调用
cout << result<5>() << endl;
【求和】
//泛化版本 template<int ...FTArgs> constexpr int resultsum = 0; //终止递归 //特化版本 template <int First, int ... Others> constexpr int resultsum<First, Others...> = First + resultsum<Others...>;
- 调用
cout << resultsum<1, 10, 100> << endl;
【求和:折叠表达式方式】
template <int ... FTArgs> constexpr int resultsum2() { return (... + FTArgs); //或者写成return (0 + ... + FTArgs); }
- 调用
cout << resultsum2<1, 10, 100>() << endl;
类型元函数
- std::remove_all_extents类模板:remove_all_extents类模板所实现的元编程就是靠递归模板实例化来驱动的。
【示例】
template <typename T> struct AddPoint //类型元函数 { using type = T * ; };
- 调用
AddPoint<const char>::type s = "I Love China!"; //AddPoint<const char>::type 等价于const char * printf("s的类型是:%s\n", typeid(decltype(s)).name());
- 输出
- AddPoint 可以看成函数名,类型T看成形参。
- 象AddPoint这种包含了using来定义类型别名的类模板,就可以称为类型元函数(fixed trait类模板)
【精简写法】
template <typename T> using AddPoint = T * ; //AddPoint是元函数
- 调用
AddPoint<const char> s = "I Love China!"; printf("s的类型是:%s\n", typeid(decltype(s)).name());
【元函数总结】
- 宽泛的概念,只要用于元编程之中,在编译期间能够被调用,都可以视为元函数,不必局限在数值元函数和类型元函数分类上