从引入 template 关键字开始,C++里就出现了泛型编程,而又泛型编程衍生出的模板元编程(template meta_programming,简称“元编程”)则是众多编程范式中最复杂、最强大和最具有权威的一种。所谓“元编程”——metaprogramming,有着完全不同于普通程序的许多特点,是一种全新的编程体验。下面将介绍模板元编程的一些基础概念,它们是现代C++和boost程序库组件的基础。
1. 概述
元编程(meta-programming)也被称为“超程序”,“超编程”或“产生式编程”,这样说法一定程度上反映了其本质——它是一种位于普通程序之上、超越普通程序的程序,可以操纵、产生程序的程序。模板元编程本质上是泛型编程的一个子集,从广义上来说,所有使用template 的泛型代码都可以称作元程序——因为泛型编程代码并不是真正可编译执行的代码,它们只是定义了代码的产生规则,是用来生成代码的“模板”。然而模板元编程又不完全等同于泛型编程,它是一种“函数式编程”,是图灵完备的,可以“计算”任何东西。
模板元编程的允许是在编译期,它把编译器变成了元程序的解释器。
2. 语法元素
模板元编程产生的元程序是在编译期执行的程序,操作对象也不是普通的变量,因此不能使用运行时的C++ 关键字(if、else、for),可以的语法元素相当有限,最常用的包括:
- enum、static 用来定义编译期的变量
- typedef、using,最重要的元编程关键字,用于定义元数据
- template,模板元编程的“起点”,主要用于定义元函数
- “::”,域运算符,用于解析类型作用域获取计算结果(元数据)
3. 元数据
元编程可操作的数据就称为“元数据”(meta date),也就是C++ 编译器在编译期可操作的数据,它是模板元编程的基础。元数据都是不可变的,不能够就地修改,最常见的元数据是整数和C++ 类型(type)。这些元数据不是运行时的普通变量,而是如 int、double、class(非模板类)这样的抽象数据类型。要是对元数据再细分归类,则它又可分为:
- 整数元数据
- 值型元数据(int、double等POD值类型)
- 函数元数据(函数类型)
- 类元数据(class、struct等用户自定义类型)
对于下面所提到的‘元数据“,特征非整数类型的元数据。
使用tyoedef 关键字可以任意定义(声明)元数据,很像运行时的变量定义,如:
typedef int mtes_data1; //元数据meta_data1, 值为 int
typedef std::vector<float> meta_data2; //元数据meta_data2,值为vector<float>
使用using 也可以达到同样的效果
using meta_data1 = int;
using meta_data2 = std:;vector<float>;
4. 元函数
元函数(meta function)是模板元编程中用于操作处理元数据的”构件“,可以在编译期被”调用“,因为其功能和形式类似运行时的函数而得名,是元编程里的核心概念。它实际上是一个类或者模板类,通常形式为:
template<typename arg1, typename arg2, ...> //元函数参数列表
struct meta_function //元函数名
{
typedef sone-define type; //元函数返回元数据
//using type = some-define;
static int const val = some-int; //元函数返回的整数
};//结束
编写元函数就像是编写一个普通的运行是函数,但形式上却是一个模板类:
- 函数参数列表的园括号”()“ 变成了模板列表的尖括号”<>“
- 函数的形参变成了模板参数(即元数据),并且要使用关键字 typedef 修饰
- 因为不能使用运行时关键字,所以元函数不能像其他普通函数那样使用return 返回计算结果,而是需要在内部使用typedef / using 定义一个名为 type 的类型(元数据)或者名为val 的值作为返回
- 最后以分号结束,因为它本质上是一个类
元函数也可以没有返回值(即不定义内部类型type),也可以有重载(模板特例/偏特化),也可以有缺省参数,也可以分为无参、单参、多参、可变参等类别。但元函数没有普通函数参数传值、传引用的区别,也没有函数指针的概念。如果有必要,元函数可以使用 typedef / using 关键字 “返回” 任意多个返回值,并且这些没有顺序关系,能够用 ”::“ 来任意获取。为表述方便,下面将只返回 ::type 的元函数称为标准元函数,而返回多个元数据的元函数称为非标准元函数。
下面给一段值元函数的代码:
template<int N, int M> //两个元数据
struct meta_func
{
static const int val = N + M; //编译期计算整数之和
};
cout << meta_func<10, 10>::val << endl; //计算结果 20
这里需要主要的是meta_func 的执行过程,它的计算在编译期的时候就已经完成了(即模板实例化),meta_func::val 实际上是一个编译期常量,程序运行时不会有任何计算动作而是直接使用结果。如果这是一个大型的元函数,那么在编译期节约的计算量就会相当可观,可以显著提高程序运行的效率。
由于元函数的计算发生在编译期,所以下列代码不能成立:(不能使用运行时的变量)
下面示范了另一个元函数,它返回元函数参数列表中的第一个元数据:
template<typename T1, typename T2> //两个形参
struct select1st
{
typedef T1 type; //返回T1,等价于using type = T1
};
5. 元函数转发
元函数转发是模板元编程中一个经常用到的惯用法,相当于运行时的函数转发调用,但在模板元编程中则要用 public 继承实现,模板参数传递给父类完成元函数的 ”调用“,这样的子类会自动获得父类的::type 定义,同时也完成了元函数的返回。例如,下面代码把元函数数据调换位置后,转发给之前定义的元函数 select1st ,相当于select2nd 的功能:
template<typename T1, typename T2>
struct forword: select1st: //元函数转发,默认是public继承
select1st<T2, T1> //参数位置变动
{};
template<typename T1, typename T2>
struct forward //不用转发
{
typedef typename select1st<T2, T1>::type type; //调用元函数计算
};
易知,元函数转发因为使用了类继承所以更加简洁
6. 工具宏
模板元编程是一种全新的C++ 编程范式,但仍然使用原有的语法,通篇的 typedef / using 、 template 关键字使元程序不易理解,所有完美可以定义一些工具宏,均以“mp_”开头(或者用增加习惯的方法定义),这样能够便于我们理解。
#define mp_arglist template //元函数参数列表
#define mp_arg typename //元函数参数声明
#define mp_function struct //元函数定义
#define mp_data typedef //元数据定义
#define mp_return(T) mp_data T type //元函数返回
//using type = T
#define mp_exec(Func) Func::type //获取元函数返回结果
#define eval(Func) Func::value //获取元函数返回值
这些分别把 template 、typename、struct 和 typedef 这四个模板元编程中最常用的关键字进行了重命名。
- mp_arglist 表示元函数的参数列表开始
- mp_arg 表示元函数的参数
- mp_function 表示定义一个元函数
- mp_data 表示定义一个元数据
- mp_return / mp_eval / mp_exec 定义了元编程中约定返回值用法,较原写法更清楚
mp_data int meta_data1; //元数据meta_data1,值为int
mp_arglist<mp_arg T1, mp_arg T2> //元函数的参数是T1、T2
mp_function select1st //元函数select1st
{
mp_return(T1); //返回T1 -> type
}
很明显,使用工具宏使元程序看起来更加清楚,易于区分。但是由于宏预处理机制自身的“缺陷”,后三个工具宏的作用有限,它们只能处理简单的参数,如果带有 “,”,那么模板类就会失效,但Boost 库里面的 BOOST_IDENTITY_TYPE 来解决。
7. 应用示例
下面通过两个例子来示范元编程的基本使用:
- 编译期比较大小
mp_arglist<int L, int R>
mp_function static_min //元函数 static_min
{
static const int value = (L < R) ? L : R;
};
assert((static_min<10, 20>::value == 10)); //编译期比较
- demo_func 输入元数据 T 是指针类型返回const T,否则 const T*
mp_arglist<mp_arg T> //单参元函数
mp_function demo_func
{
mp_return(const T*); //通常情况返回const T*
};
mp_arglist<mp_arg T>
mp_function demo_func<T*> //对T*情况进行模板实例化
{
mp_return(const T);
};
//这里用is_same元函数进行验证 #include<boost/type_traits/is_same>
assert((is_same<mp_exec(demo_func<int>), const int*>::value));
assert((is_same<mp_exec(demo_func<int*>), const int>::value));
//这里的 assert 必须用两对括号来包围断言
8. 总结
我们介绍了模板元编程的基础知识,包括元编程 / 元程序 / 元数据 和 元函数转发等概念。
- 元编程是一种超越普通程序的程序,在C++中元编程是使用模板技术实现的,所以它右被称为模板元编程。元程序可以由C++编译期解释执行,把部分计算量由运行时转移到编译时完成,提高程序运行效率,但元编程更大的用途是类型推导,操作C++类型体系。
- 元数据是元编程的操作对象,可以是整数(含bool)或任意的C++类型
- 元函数是元编程的核心,它表现为C++的一个模板类,我们必须使用元函数才能操作元数据。它以内部定义::type 或 ::value 返回计算结果,并可以使用public 继承的方式实现元函数转发
至此结束