实验平台:Win7,VS2013 Community,GCC 4.8.3(在线版)
所谓元编程就是编写直接生成或操纵程序的程序,C++ 模板给 C++ 语言提供了元编程的能力,模板使 C++ 编程变得异常灵活,能实现很多高级动态语言才有的特性(语法上可能比较丑陋,一些历史原因见下文)。普通用户对 C++ 模板的使用可能不是很频繁,大致限于泛型编程,但一些系统级的代码,尤其是对通用性、性能要求极高的基础库(如 STL、Boost)几乎不可避免的都大量地使用 C++ 模板,一个稍有规模的大量使用模板的程序,不可避免的要涉及元编程(如类型计算)。本文就是要剖析 C++ 模板元编程的机制。
下面所给的所有代码,想做实验又懒得打开编译工具?一个在线运行 C++ 代码的网站(GCC 4.8)很好~
(本博文地址:http://www.cnblogs.com/liangliangh/p/4219879.html,转载版本将得不到作者维护)
1. C++模板的语法
函数模板(function template)和类模板(class template)的简单示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
#include
// 函数模板
template
<
typename
T>
bool
equivalent(
const
T& a,
const
T& b){
return
!(a < b) && !(b < a);
}
// 类模板
template
<
typename
T=
int
>
// 默认参数
class
bignumber{
T _v;
public
:
bignumber(T a) : _v(a) { }
inline
bool
operator<(
const
bignumber& b)
const
;
// 等价于 (const bignumber
};
// 在类模板外实现成员函数
template
<
typename
T>
bool
bignumber
const
bignumber& b)
const
{
return
_v < b._v;
}
int
main()
{
bignumber<> a(1), b(1);
// 使用默认参数,"<>"不能省略
std::cout << equivalent(a, b) <<
'\n'
;
// 函数模板参数自动推导
std::cout << equivalent<
double
>(1, 2) <<
'\n'
;
std::cin.get();
return
0;
}
|
程序输出如下:
1
2
|
1
0
|
关于模板(函数模板、类模板)的模板参数(详见文献[1]第3章):
模板特例化(template specialization,又称特例、特化)的简单示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 实现一个向量类
template
<
typename
T,
int
N>
class
Vec{
T _v[N];
// ... // 模板通例(primary template),具体实现
};
template
<>
class
Vec<
float
, 4>{
float
_v[4];
// ... // 对 Vec
};
template
<
int
N>
class
Vec<
bool
, N>{
char
_v[(N+
sizeof
(
char
)-1)/
sizeof
(
char
)];
// ... // 对 Vec
};
|
所谓模板特例化即对于通例中的某种或某些情况做单独专门实现,最简单的情况是对每个模板参数指定一个具体值,这成为完全特例化(full specialization),另外,可以限制模板参数在一个范围取值或满足一定关系等,这称为部分特例化(partial specialization),用数学上集合的概念,通例模板参数所有可取的值组合构成全集U,完全特例化对U中某个元素进行专门定义,部分特例化对U的某个真子集进行专门定义。
更多模板特例化的例子如下(参考了文献[1]第44页):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
template
<
typename
T,
int
i>
class
cp00;
// 用于模板型模板参数
// 通例
template
<
typename
T1,
typename
T2,
int
i,
template
<
typename
,
int
>
class
CP>
class
TMP;
// 完全特例化
template
<>
class
TMP<
int
,
float
, 2, cp00>;
// 第一个参数有const修饰
template
<
typename
T1,
typename
T2,
int
i,
template
<
typename
,
int
>
class
CP>
class
TMP<
const
T1, T2, i, CP>;
// 第一二个参数为cp00的实例且满足一定关系,第四个参数为cp00
template
<
typename
T,
int
i>
class
TMP
// 编译错误!,第四个参数类型和通例类型不一致
//template CP>
//class TMP
|
关于模板特例化(详见文献[1]第4章):
对模板的多个实例,类型等价(type equivalence)判断规则(详见文献[2] 13.2.4):同一个模板(模板名及其参数类型列表构成的模板签名(template signature)相同,函数模板可以重载,类模板不存在重载)且指定的模板实参等价(类型参数是等价类型,非类型参数值相同)。如下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include
// 识别两个类型是否相同,提前进入模板元编程^_^
template
<
typename
T1,
typename
T2>
// 通例,返回 false
class
theSameType {
public
:
enum
{ ret =
false
}; };
template
<
typename
T>
// 特例,两类型相同时返回 true
class
theSameType
public
:
enum
{ ret =
true
}; };
template
<
typename
T,
int
i>
class
aTMP { };
int
main(){
typedef
unsigned
int
uint;
// typedef 定义类型别名而不是引入新类型
typedef
uint uint2;
std::cout << theSameType
'\n'
;
// 感谢 C++11,连续角括号“>>”不会被当做流输入符号而编译错误
std::cout << theSameType
'\n'
;
std::cout << theSameType
std::cin.get();
return
0;
}
|
1
2
3
|
1
1
0
|
关于模板实例化(template instantiation)(详见文献[4]模板):
隐式实例化时,成员只有被引用到才会进行实例化,这被称为推迟实例化(lazy instantiation),由此可能带来的问题如下面的例子(文献[6],文献[7]):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include
template
<
typename
T>
class
aTMP {
public
:
void
f1() { std::cout <<
"f1()\n"
; }
void
f2() { std::ccccout <<
"f2()\n"
; }
// 敲错键盘了,语义错误:没有 std::ccccout
};
int
main(){
aTMP<
int
> a;
a.f1();
// a.f2(); // 这句代码被注释时,aTMP
std::cin.get();
return
0;
}
|
所以模板代码写完后最好写个诸如显示实例化的测试代码,更深入一些,可以插入一些模板调用代码使得编译器及时发现错误,而不至于报出无限长的错误信息。另一个例子如下(GCC 4.8 下编译的输出信息,VS2013 编译输出了 500 多行错误信息):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include
// 计算 N 的阶乘 N!
template
<
int
N>
class
aTMP{
public
:
enum
{ ret = N==0 ? 1 : N * aTMP
// Lazy Instantiation,将产生无限递归!
};
int
main(){
std::cout << aTMP<10>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
2
3
4
5
6
7
8
9
|
sh-4.2
# g++ -std=c++11 -o main *.cpp
main.cpp:7:28: error: template instantiation depth exceeds maximum of 900 (use -ftemplate-depth= to increase the maximum) instantiating
'class aTMP<-890>'
enum { ret = N==0 ? 1 : N * aTMP
^
main.cpp:7:28: recursively required from
'class aTMP<9>'
main.cpp:7:28: required from
'class aTMP<10>'
main.cpp:11:23: required from here
main.cpp:7:28: error: incomplete
type
'aTMP<-890>'
used
in
nested name specifier
|
上面的错误是因为,当编译 aTMP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include
// 计算 N 的阶乘 N!
template
<
int
N>
class
aTMP{
public
:
enum
{ ret = N * aTMP
};
template
<>
class
aTMP<0>{
public
:
enum
{ ret = 1 };
};
int
main(){
std::cout << aTMP<10>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
3228800
|
关于模板的编译和链接(详见文献[1] 1.3、文献[4]模板):
实例化,编译链接的简单例子如下(参考了文献[1]第10页):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// file: a.cpp
#include
template
<
typename
T>
class
MyClass { };
template
MyClass<
double
>::MyClass();
// 显示实例化构造函数 MyClass
template
class
MyClass<
long
>;
// 显示实例化整个类 MyClass
template
<
typename
T>
void
print(T
const
& m) { std::cout <<
"a.cpp: "
<< m <<
'\n'
; }
void
fa() {
print(1);
// print
print(0.1);
// print
}
void
fb();
// fb() 在 b.cpp 中定义,此处声明
int
main(){
fa();
fb();
std::cin.get();
return
0;
}
|
1
2
3
4
5
6
7
8
9
|
// file: b.cpp
#include
template
<
typename
T>
void
print(T
const
& m) { std::cout <<
"b.cpp: "
<< m <<
'\n'
; }
void
fb() {
print(
'2'
);
// print
print(0.1);
// print
}
|
1
2
3
4
|
a.cpp: 1
a.cpp: 0.1
b.cpp: 2
a.cpp: 0.1
|
上例中,由于 a.cpp 和 b.cpp 中的 print
关于 template、typename、this 关键字的使用(文献[4]模板,文献[5]):
一个例子如下(需要 GCC 编译,GCC 对 C++11 几乎全面支持,VS2013 此处总是在基类中查找名字,且函数模板前不需要 template):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include
template
<
typename
T>
class
aTMP{
public
:
typedef
const
T reType;
};
void
f() { std::cout <<
"global f()\n"
; }
template
<
typename
T>
class
Base {
public
:
template
<
int
N = 99>
void
f() { std::cout <<
"member f(): "
<< N <<
'\n'
; }
};
template
<
typename
T>
class
Derived :
public
Base
public
:
typename
T::reType m;
// typename 不能省略
Derived(
typename
T::reType a) : m(a) { }
void
df1() { f(); }
// 调用全局 f(),而非想象中的基类 f()
void
df2() {
this
->
template
f(); }
// 基类 f<99>()
void
df3() { Base
template
f<22>(); }
// 强制基类 f<22>()
void
df4() { ::f(); }
// 强制全局 f()
};
int
main(){
Derived
a.df1(); a.df2(); a.df3(); a.df4();
std::cin.get();
return
0;
}
|
1
2
3
4
|
global f()
member f(): 99
member f(): 22
global f()
|
C++11 关于模板的新特性(详见文献[1]第15章,文献[4]C++11):
在本文中,如无特别声明将不使用 C++11 的特性(除了 “>>”)。
2. 模板元编程概述
如果对 C++ 模板不熟悉(光熟悉语法还不算熟悉),可以先跳过本节,往下看完例子再回来。
C++ 模板最初是为实现泛型编程设计的,但人们发现模板的能力远远不止于那些设计的功能。一个重要的理论结论就是:C++ 模板是图灵完备的(Turing-complete),其证明过程请见文献[8](就是用 C++ 模板模拟图灵机),理论上说 C++ 模板可以执行任何计算任务,但实际上因为模板是编译期计算,其能力受到具体编译器实现的限制(如递归嵌套深度,C++11 要求至少 1024,C++98 要求至少 17)。C++ 模板元编程是“意外”功能,而不是设计的功能,这也是 C++ 模板元编程语法丑陋的根源。
C++ 模板是图灵完备的,这使得 C++ 成为两层次语言(two-level languages,中文暂且这么翻译,文献[9]),其中,执行编译计算的代码称为静态代码(static code),执行运行期计算的代码称为动态代码(dynamic code),C++ 的静态代码由模板实现(预处理的宏也算是能进行部分静态计算吧,也就是能进行部分元编程,称为宏元编程,见 Boost 元编程库即 BCCL,文献[16]和文献[1] 10.4)。
具体来说 C++ 模板可以做以下事情:编译期数值计算、类型计算、代码计算(如循环展开),其中数值计算实际不太有意义,而类型计算和代码计算可以使得代码更加通用,更加易用,性能更好(也更难阅读,更难调试,有时也会有代码膨胀问题)。编译期计算在编译过程中的位置请见下图(取自文献[10]),可以看到关键是模板的机制在编译具体代码(模板实例)前执行:
从编程范型(programming paradigm)上来说,C++ 模板是函数式编程(functional programming),它的主要特点是:函数调用不产生任何副作用(没有可变的存储),用递归形式实现循环结构的功能。C++ 模板的特例化提供了条件判断能力,而模板递归嵌套提供了循环的能力,这两点使得其具有和普通语言一样通用的能力(图灵完备性)。
从编程形式来看,模板的“<>”中的模板参数相当于函数调用的输入参数,模板中的 typedef 或 static const 或 enum 定义函数返回值(类型或数值,数值仅支持整型,如果需要可以通过编码计算浮点数),代码计算是通过类型计算进而选择类型的函数实现的(C++ 属于静态类型语言,编译器对类型的操控能力很强)。代码示意如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include
template
<
typename
T,
int
i=1>
class
someComputing {
public
:
typedef
volatile
T* retType;
// 类型计算
enum
{ retValume = i + someComputing
// 数值计算,递归
static
void
f() { std::cout <<
"someComputing: i="
<< i <<
'\n'
; }
};
template
<
typename
T>
// 模板特例,递归终止条件
class
someComputing
public
:
enum
{ retValume = 0 };
};
template
<
typename
T>
class
codeComputing {
public
:
static
void
f() { T::f(); }
// 根据类型调用函数,代码计算
};
int
main(){
someComputing<
int
>::retType a=0;
std::cout <<
sizeof
(a) <<
'\n'
;
// 64-bit 程序指针
// VS2013 默认最大递归深度500,GCC4.8 默认最大递归深度900(-ftemplate-depth=n)
std::cout << someComputing<
int
, 500>::retValume <<
'\n'
;
// 1+2+...+500
codeComputing
std::cin.get();
return
0;
}
|
1
2
3
|
8
125250
someComputing: i=99
|
C++ 模板元编程概览框图如下(取自文献[9]):
下面我们将对图中的每个框进行深入讨论。
3. 编译期数值计算
第一个 C++ 模板元程序是 Erwin Unruh 在 1994 年写的(文献[14]),这个程序计算小于给定数 N 的全部素数(又叫质数),程序并不运行(都不能通过编译),而是让编译器在错误信息中显示结果(直观展现了是编译期计算结果,C++ 模板元编程不是设计的功能,更像是在戏弄编译器,当然 C++11 有所改变),由于年代久远,原来的程序用现在的编译器已经不能编译了,下面的代码在原来程序基础上稍作了修改(GCC 4.8 下使用 -fpermissvie,只显示警告信息):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// Prime number computation by Erwin Unruh
template
<
int
i>
struct
D { D(
void
*); operator
int
(); };
// 构造函数参数为 void* 指针
template
<
int
p,
int
i>
struct
is_prime {
// 判断 p 是否为素数,即 p 不能整除 2...p-1
enum
{ prim = (p%i) && is_prime<(i>2?p:0), i-1>::prim };
};
template
<>
struct
is_prime<0, 0> {
enum
{ prim = 1 }; };
template
<>
struct
is_prime<0, 1> {
enum
{ prim = 1 }; };
template
<
int
i>
struct
Prime_print {
Prime_print
enum
{ prim = is_prime::prim };
// prim 为真时, prim?1:0 为 1,int 到 D 转换报错;假时, 0 为 NULL 指针不报错
void
f() { D d = prim?1:0; a.f(); }
// 调用 a.f() 实例化 Prime_print
};
template
<>
struct
Prime_print<2> {
// 特例,递归终止
enum
{ prim = 1 };
void
f() { D<2> d = prim?1:0; }
};
#ifndef LAST
#define LAST 10
#endif
int
main() {
Prime_print
// 必须调用 a.f() 以实例化 Prime_print
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
sh-4.2
# g++ -std=c++11 -fpermissive -o main *.cpp
main.cpp: In member
function
'void Prime_print<2>::f()'
:
main.cpp:17:33: warning: invalid conversion from
'int'
to
'void*'
[-fpermissive]
void f() { D<2> d = prim ? 1 : 0; }
^
main.cpp:2:28: warning: initializing argument 1 of
'D::D(void*) [with int i = 2]'
[-fpermissive]
template
^
main.cpp: In instantiation of
'void Prime_print::f() [with int i = 7]'
:
main.cpp:13:36: recursively required from
'void Prime_print::f() [with int i = 9]'
main.cpp:13:36: required from
'void Prime_print::f() [with int i = 10]'
main.cpp:25:27: required from here
main.cpp:13:33: warning: invalid conversion from
'int'
to
'void*'
[-fpermissive]
void f() { D d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of
'D::D(void*) [with int i = 7]'
[-fpermissive]
template
^
main.cpp: In instantiation of
'void Prime_print::f() [with int i = 5]'
:
main.cpp:13:36: recursively required from
'void Prime_print::f() [with int i = 9]'
main.cpp:13:36: required from
'void Prime_print::f() [with int i = 10]'
main.cpp:25:27: required from here
main.cpp:13:33: warning: invalid conversion from
'int'
to
'void*'
[-fpermissive]
void f() { D d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of
'D::D(void*) [with int i = 5]'
[-fpermissive]
template
^
main.cpp: In instantiation of
'void Prime_print::f() [with int i = 3]'
:
main.cpp:13:36: recursively required from
'void Prime_print::f() [with int i = 9]'
main.cpp:13:36: required from
'void Prime_print::f() [with int i = 10]'
main.cpp:25:27: required from here
main.cpp:13:33: warning: invalid conversion from
'int'
to
'void*'
[-fpermissive]
void f() { D d = prim ? 1 : 0; a.f(); }
^
main.cpp:2:28: warning: initializing argument 1 of
'D::D(void*) [with int i = 3]'
[-fpermissive]
template
|
上面的编译输出信息只给出了前一部分,虽然信息很杂,但还是可以看到其中有 10 以内全部素数:2、3、5、7(已经加粗显示关键行)。
到目前为止,虽然已经看到了阶乘、求和等递归数值计算,但都没涉及原理,下面以求和为例讲解 C++ 模板编译期数值计算的原理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include
template
<
int
N>
class
sumt{
public
:
static
const
int
ret = sumt
};
template
<>
class
sumt<0>{
public
:
static
const
int
ret = 0;
};
int
main() {
std::cout << sumt<5>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
|
当编译器遇到 sumt<5> 时,试图实例化之,sumt<5> 引用了 sumt<5-1> 即 sumt<4>,试图实例化 sumt<4>,以此类推,直到 sumt<0>,sumt<0> 匹配模板特例,sumt<0>::ret 为 0,sumt<1>::ret 为 sumt<0>::ret+1 为 1,以此类推,sumt<5>::ret 为 15。值得一提的是,虽然对用户来说程序只是输出了一个编译期常量 sumt<5>::ret,但在背后,编译器其实至少处理了 sumt<0> 到 sumt<5> 共 6 个类型。
从这个例子我们也可以窥探 C++ 模板元编程的函数式编程范型,对比结构化求和程序:for(i=0,sum=0; i<=N; ++i) sum+=i; 用逐步改变存储(即变量 sum)的方式来对计算过程进行编程,模板元程序没有可变的存储(都是编译期常量,是不可变的变量),要表达求和过程就要用很多个常量:sumt<0>::ret,sumt<1>::ret,…,sumt<5>::ret 。函数式编程看上去似乎效率低下(因为它和数学接近,而不是和硬件工作方式接近),但有自己的优势:描述问题更加简洁清晰(前提是熟悉这种方式),没有可变的变量就没有数据依赖,方便进行并行化。
4. 模板下的控制结构
模板实现的条件 if 和 while 语句如下(文献[9]):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 通例为空,若不匹配特例将报错,很好的调试手段(这里是 bool 就无所谓了)
template
<
bool
c,
typename
Then,
typename
Else>
class
IF_ { };
template
<
typename
Then,
typename
Else>
class
IF_<
true
, Then, Else> {
public
:
typedef
Then reType; };
template
<
typename
Then,
typename
Else>
class
IF_<
false
,Then, Else> {
public
:
typedef
Else reType; };
// 隐含要求: Condition 返回值 ret,Statement 有类型 Next
template
<
template
<
typename
>
class
Condition,
typename
Statement>
class
WHILE_ {
template
<
typename
Statement>
class
STOP {
public
:
typedef
Statement reType; };
public
:
typedef
typename
IF_
WHILE_
STOP
reType;
};
|
IF_<> 的使用示例见下面:
1
2
3
4
5
6
7
8
9
|
const
int
len = 4;
typedef
IF_<
sizeof
(
short
)==len,
short
,
IF_<
sizeof
(
int
)==len,
int
,
IF_<
sizeof
(
long
)==len,
long
,
IF_<
sizeof
(
long
long
)==len,
long
long
,
void
>::reType>::reType>::reType>::reType
int_my;
// 定义一个指定字节数的类型
std::cout <<
sizeof
(int_my) <<
'\n'
;
|
1
|
4
|
WHILE_<> 的使用示例见下面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// 计算 1^e+2^e+...+n^e
template
<
int
n,
int
e>
class
sum_pow {
template
<
int
i,
int
e>
class
pow_e{
public
:
enum
{ ret=i*pow_e::ret }; };
template
<
int
i>
class
pow_e{
public
:
enum
{ ret=1 }; };
// 计算 i^e,嵌套类使得能够定义嵌套模板元函数,private 访问控制隐藏实现细节
template
<
int
i>
class
pow
{
public
:
enum
{ ret=pow_e::ret }; };
template
<
typename
stat>
class
cond {
public
:
enum
{ ret=(stat::ri<=n) }; };
template
<
int
i,
int
sum>
class
stat {
public
:
typedef
stat
enum
{ ri=i, ret=sum }; };
public
:
enum
{ ret = WHILE_
};
int
main() {
std::cout << sum_pow<10, 2>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
385
|
为了展现编译期数值计算的强大能力,下面是一个更复杂的计算:最大公约数(Greatest Common Divisor,GCD)和最小公倍数(Lowest Common Multiple,LCM),经典的辗转相除算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
// 最小公倍数,普通函数
int
lcm(
int
a,
int
b){
int
r, lcm=a*b;
while
(r=a%b) { a = b; b = r; }
// 因为用可变的存储,不能写成 a=b; b=a%b;
return
lcm/b;
}
// 递归函数版本
int
gcd_r(
int
a,
int
b) {
return
b==0 ? a : gcd_r(b, a%b); }
// 简洁
int
lcm_r(
int
a,
int
b) {
return
a * b / gcd_r(a,b); }
// 模板版本
template
<
int
a,
int
b>
class
lcm_T{
template
<
typename
stat>
class
cond {
public
:
enum
{ ret=(stat::
div
!=0) }; };
template
<
int
a,
int
b>
class
stat {
public
:
typedef
stat Next;
enum
{
div
=a%b, ret=b }; };
static
const
int
gcd = WHILE_
public
:
static
const
int
ret = a * b / gcd;
};
// 递归模板版本
template
<
int
a,
int
b>
class
lcm_T_r{
template
<
int
a,
int
b>
class
gcd {
public
:
enum
{ ret = gcd::ret }; };
public
:
};
int
main() {
std::cout << lcm(100, 36) <<
'\n'
;
std::cout << lcm_r(100, 36) <<
'\n'
;
std::cout << lcm_T<100, 36>::ret <<
'\n'
;
std::cout << lcm_T_r<100, 36>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
2
3
4
|
900
900
900
900
|
上面例子中,定义一个类的整型常量,可以用 enum,也可以用 static const int,需要注意的是 enum 定义的常量的字节数不会超过 sizeof(int) (文献[2])。
5. 循环展开
文献[11]展示了一个循环展开(loop unrolling)的例子 — 冒泡排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
#include
// dynamic code, 普通函数版本
void
bubbleSort(
int
* data,
int
n)
{
for
(
int
i=n-1; i>0; --i) {
for
(
int
j=0; j
if
(data[j]>data[j+1]) std::swap(data[j], data[j+1]);
}
}
// 数据长度为 4 时,手动循环展开
inline
void
bubbleSort4(
int
* data)
{
#define COMP_SWAP(i, j) if(data[i]>data[j]) std::swap(data[i], data[j])
COMP_SWAP(0, 1); COMP_SWAP(1, 2); COMP_SWAP(2, 3);
COMP_SWAP(0, 1); COMP_SWAP(1, 2);
COMP_SWAP(0, 1);
}
// 递归函数版本,指导模板思路,最后一个参数是哑参数(dummy parameter),仅为分辨重载函数
class
recursion { };
void
bubbleSort(
int
* data,
int
n, recursion)
{
if
(n<=1)
return
;
for
(
int
j=0; j
bubbleSort(data, n-1, recursion());
}
// static code, 模板元编程版本
template
<
int
i,
int
j>
inline
void
IntSwap(
int
* data) {
// 比较和交换两个相邻元素
if
(data[i]>data[j]) std::swap(data[i], data[j]);
}
template
<
int
i,
int
j>
inline
void
IntBubbleSortLoop(
int
* data) {
// 一次冒泡,将前 i 个元素中最大的置换到最后
IntSwap
IntBubbleSortLoop
}
template
<>
inline
void
IntBubbleSortLoop<0, 0>(
int
*) { }
template
<
int
n>
inline
void
IntBubbleSort(
int
* data) {
// 模板冒泡排序循环展开
IntBubbleSortLoop
IntBubbleSort
}
template
<>
inline
void
IntBubbleSort<1>(
int
* data) { }
|
对循环次数固定且比较小的循环语句,对其进行展开并内联可以避免函数调用以及执行循环语句中的分支,从而可以提高性能,对上述代码做如下测试,代码在 VS2013 的 Release 下编译运行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include
#include
#include
int
main() {
double
t1, t2, t3;
const
int
num=100000000;
int
data[4];
int
inidata[4]={3,4,2,1};
t1 = omp_get_wtime();
for
(
int
i=0; i
t1 = omp_get_wtime()-t1;
t2 = omp_get_wtime();
for
(
int
i=0; i
t2 = omp_get_wtime()-t2;
t3 = omp_get_wtime();
for
(
int
i=0; i
t3 = omp_get_wtime()-t3;
std::cout << t1/t3 <<
'\t'
<< t2/t3 <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
2.38643 0.926521
|
上述结果表明,模板元编程实现的循环展开能够达到和手动循环展开相近的性能(90% 以上),并且性能是循环版本的 2 倍多(如果扣除 memcpy 函数占据的部分加速比将更高,根据 Amdahl 定律)。这里可能有人会想,既然循环次数固定,为什么不直接手动循环展开呢,难道就为了使用模板吗?当然不是,有时候循环次数确实是编译期固定值,但对用户并不是固定的,比如要实现数学上向量计算的类,因为可能是 2、3、4 维,所以写成模板,把维度作为 int 型模板参数,这时因为不知道具体是几维的也就不得不用循环,不过因为维度信息在模板实例化时是编译期常量且较小,所以编译器很可能在代码优化时进行循环展开,但我们想让这一切发生的更可控一些。
上面用三个函数模板 IntSwap<>()、 IntBubbleSortLoop<>()、 IntBubbleSort<>() 来实现一个排序功能,不但显得分散(和封装原理不符),还暴露了实现细节,我们可以仿照上一节的代码,将 IntBubbleSortLoop<>()、 IntBubbleSort<>() 嵌入其他模板内部,因为函数不允许嵌套,我们只能用类模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// 整合成一个类模板实现,看着好,但引入了 代码膨胀
template
<
int
n>
class
IntBubbleSortC {
template
<
int
i,
int
j>
static
inline
void
IntSwap(
int
* data) {
// 比较和交换两个相邻元素
if
(data[i]>data[j]) std::swap(data[i], data[j]);
}
template
<
int
i,
int
j>
static
inline
void
IntBubbleSortLoop(
int
* data) {
// 一次冒泡
IntSwap
IntBubbleSortLoop
}
template
<>
static
inline
void
IntBubbleSortLoop<0, 0>(
int
*) { }
public
:
static
inline
void
sort(
int
* data) {
IntBubbleSortLoop
IntBubbleSortC
}
};
template
<>
class
IntBubbleSortC<0> {
public
:
static
inline
void
sort(
int
* data) { }
};
int
main() {
int
data[4] = {3,4,2,1};
IntBubbleSortC<4>::sort(data);
// 如此调用
std::cin.get();
return
0;
}
|
上面代码看似很好,不仅整合了代码,借助类成员的访问控制,还隐藏了实现细节。不过它存在着很大问题,如果实例化 IntBubbleSortC<4>、 IntBubbleSortC<3>、 IntBubbleSortC<2>,将实例化成员函数 IntBubbleSortC<4>::IntSwap<0, 1>()、 IntBubbleSortC<4>::IntSwap<1, 2>()、 IntBubbleSortC<4>::IntSwap<2, 3>()、 IntBubbleSortC<3>::IntSwap<0, 1>()、 IntBubbleSortC<3>::IntSwap<1, 2>()、 IntBubbleSortC<2>::IntSwap<0, 1>(),而在原来的看着分散的代码中 IntSwap<0, 1>() 只有一个。这将导致代码膨胀(code bloat),即生成的可执行文件体积变大(代码膨胀另一含义是源代码增大,见文献[1]第11章)。不过这里使用了内联(inline),如果编译器确实内联展开代码则不会导致代码膨胀(除了循环展开本身会带来的代码膨胀),但因为重复编译原本可以复用的模板实例,会增加编译时间。在上一节的例子中,因为只涉及编译期常量计算,并不涉及函数(函数模板,或类模板的成员函数,函数被编译成具体的机器二进制代码),并不会出现代码膨胀。
为了清晰证明上面的论述,我们去掉所有 inline 并将函数实现放到类外面(类里面实现的成员函数都是内联的,因为函数实现可能被包含多次,见文献[2] 10.2.9,不过现在的编译器优化能力很强,很多时候加不加 inline 并不影响编译器自己对内联的选择…),分别编译分散版本和类模板封装版本的冒泡排序代码编译生成的目标文件(VS2013 下是 .obj 文件)的大小,代码均在 VS2013 Debug 模式下编译(防止编译器优化),比较 main.obj (源文件是 main.cpp)大小。
类模板封装版本代码如下,注意将成员函数在外面定义的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
#include
#include
// 整合成一个类模板实现,看着好,但引入了 代码膨胀
template
<
int
n>
class
IntBubbleSortC {
template
<
int
i,
int
j>
static
void
IntSwap(
int
* data);
template
<
int
i,
int
j>
static
void
IntBubbleSortLoop(
int
* data);
template
<>
static
void
IntBubbleSortLoop<0, 0>(
int
*) { }
public
:
static
void
sort(
int
* data);
};
template
<>
class
IntBubbleSortC<0> {
public
:
static
void
sort(
int
* data) { }
};
template
<
int
n>
template
<
int
i,
int
j>
void
IntBubbleSortC
int
* data) {
if
(data[i]>data[j]) std::swap(data[i], data[j]);
}
template
<
int
n>
template
<
int
i,
int
j>
void
IntBubbleSortC
int
* data) {
IntSwap
IntBubbleSortLoop
}
template
<
int
n>
void
IntBubbleSortC
int
* data) {
IntBubbleSortLoop
IntBubbleSortC
}
int
main() {
int
data[40] = {3,4,2,1};
IntBubbleSortC<2>::sort(data); IntBubbleSortC<3>::sort(data);
IntBubbleSortC<4>::sort(data); IntBubbleSortC<5>::sort(data);
IntBubbleSortC<6>::sort(data); IntBubbleSortC<7>::sort(data);
IntBubbleSortC<8>::sort(data); IntBubbleSortC<9>::sort(data);
IntBubbleSortC<10>::sort(data); IntBubbleSortC<11>::sort(data);
#if 0
IntBubbleSortC<12>::sort(data); IntBubbleSortC<13>::sort(data);
IntBubbleSortC<14>::sort(data); IntBubbleSortC<15>::sort(data);
IntBubbleSortC<16>::sort(data); IntBubbleSortC<17>::sort(data);
IntBubbleSortC<18>::sort(data); IntBubbleSortC<19>::sort(data);
IntBubbleSortC<20>::sort(data); IntBubbleSortC<21>::sort(data);
IntBubbleSortC<22>::sort(data); IntBubbleSortC<23>::sort(data);
IntBubbleSortC<24>::sort(data); IntBubbleSortC<25>::sort(data);
IntBubbleSortC<26>::sort(data); IntBubbleSortC<27>::sort(data);
IntBubbleSortC<28>::sort(data); IntBubbleSortC<29>::sort(data);
IntBubbleSortC<30>::sort(data); IntBubbleSortC<31>::sort(data);
#endif
std::cin.get();
return
0;
}
|
分散定义函数模板版本代码如下,为了更具可比性,也将函数放在类里面作为成员函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
#include
#include
// static code, 模板元编程版本
template
<
int
i,
int
j>
class
IntSwap {
public
:
static
void
swap(
int
* data);
};
template
<
int
i,
int
j>
class
IntBubbleSortLoop {
public
:
static
void
loop(
int
* data);
};
template
<>
class
IntBubbleSortLoop<0, 0> {
public
:
static
void
loop(
int
* data) { }
};
template
<
int
n>
class
IntBubbleSort {
public
:
static
void
sort(
int
* data);
};
template
<>
class
IntBubbleSort<0> {
public
:
static
void
sort(
int
* data) { }
};
template
<
int
i,
int
j>
void
IntSwap::swap(
int
* data) {
if
(data[i]>data[j]) std::swap(data[i], data[j]);
}
template
<
int
i,
int
j>
void
IntBubbleSortLoop::loop(
int
* data) {
IntSwap
IntBubbleSortLoop
}
template
<
int
n>
void
IntBubbleSort
int
* data) {
IntBubbleSortLoop
IntBubbleSort
}
int
main() {
int
data[40] = {3,4,2,1};
IntBubbleSort<2>::sort(data); IntBubbleSort<3>::sort(data);
IntBubbleSort<4>::sort(data); IntBubbleSort<5>::sort(data);
IntBubbleSort<6>::sort(data); IntBubbleSort<7>::sort(data);
IntBubbleSort<8>::sort(data); IntBubbleSort<9>::sort(data);
IntBubbleSort<10>::sort(data); IntBubbleSort<11>::sort(data);
#if 0
IntBubbleSort<12>::sort(data); IntBubbleSort<13>::sort(data);
IntBubbleSort<14>::sort(data); IntBubbleSort<15>::sort(data);
IntBubbleSort<16>::sort(data); IntBubbleSort<17>::sort(data);
IntBubbleSort<18>::sort(data); IntBubbleSort<19>::sort(data);
IntBubbleSort<20>::sort(data); IntBubbleSort<21>::sort(data);
IntBubbleSort<22>::sort(data); IntBubbleSort<23>::sort(data);
IntBubbleSort<24>::sort(data); IntBubbleSort<25>::sort(data);
IntBubbleSort<26>::sort(data); IntBubbleSort<27>::sort(data);
IntBubbleSort<28>::sort(data); IntBubbleSort<29>::sort(data);
IntBubbleSort<30>::sort(data); IntBubbleSort<31>::sort(data);
#endif
std::cin.get();
return
0;
}
|
程序中条件编译都未打开时(#if 0),main.obj 大小分别为 264 KB 和 211 KB,条件编译打开时(#if 1),main.obj 大小分别为 1073 KB 和 620 KB。可以看到,类模板封装版的对象文件不但绝对大小更大,而且增长更快,这和之前分析是一致的。
6. 表达式模板,向量运算
文献[12]展示了一个表达式模板(Expression Templates)的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
#include
#include
// 表达式类型
class
DExprLiteral {
// 文字量
double
a_;
public
:
DExprLiteral(
double
a) : a_(a) { }
double
operator()(
double
x)
const
{
return
a_; }
};
class
DExprIdentity {
// 自变量
public
:
double
operator()(
double
x)
const
{
return
x; }
};
template
<
class
A,
class
B,
class
Op>
// 双目操作
class
DBinExprOp {
A a_; B b_;
public
:
DBinExprOp(
const
A& a,
const
B& b) : a_(a), b_(b) { }
double
operator()(
double
x)
const
{
return
Op::apply(a_(x), b_(x)); }
};
template
<
class
A,
class
Op>
// 单目操作
class
DUnaryExprOp {
A a_;
public
:
DUnaryExprOp(
const
A& a) : a_(a) { }
double
operator()(
double
x)
const
{
return
Op::apply(a_(x)); }
};
// 表达式
template
<
class
A>
class
DExpr {
A a_;
public
:
DExpr() { }
DExpr(
const
A& a) : a_(a) { }
double
operator()(
double
x)
const
{
return
a_(x); }
};
// 运算符,模板参数 A、B 为参与运算的表达式类型
// operator /, division
class
DApDiv {
public
:
static
double
apply(
double
a,
double
b) {
return
a / b; } };
template
<
class
A,
class
B> DExpr
typedef
DBinExprOp
return
DExpr
}
// operator +, addition
class
DApAdd {
public
:
static
double
apply(
double
a,
double
b) {
return
a + b; } };
template
<
class
A,
class
B> DExpr
typedef
DBinExprOp
return
DExpr
}
// sqrt(), square rooting
class
DApSqrt {
public
:
static
double
apply(
double
a) {
return
std::
sqrt
(a); } };
template
<
class
A> DExpr
typedef
DUnaryExprOp
return
DExpr
}
// operator-, negative sign
class
DApNeg {
public
:
static
double
apply(
double
a) {
return
-a; } };
template
<
class
A> DExpr
typedef
DUnaryExprOp
return
DExpr
}
// evaluate()
template
<
class
Expr>
void
evaluate(
const
DExpr
double
start,
double
end,
double
step) {
for
(
double
i=start; i }
int
main() {
DExpr
evaluate( -x /
sqrt
( DExpr
std::cin.get();
return
0;
}
|
1
|
-0 -0.707107 -1.1547 -1.5 -1.78885 -2.04124 -2.26779 -2.47487 -2.66667 -2.84605
|
代码有点长(我已经尽量压缩行数),请先看最下面的 main() 函数,表达式模板允许我们以 “-x / sqrt( 1.0 + x )” 这种类似数学表达式的方式传参数,在 evaluate() 内部,将 0-10 的数依次赋给自变量 x 对表达式进行求值,这是通过在 template<> DExpr 类模板内部重载 operator() 实现的。我们来看看这一切是如何发生的。
在 main() 中调用 evaluate() 时,编译器根据全局重载的加号、sqrt、除号、负号推断“-x / sqrt( 1.0 + x )” 的类型是 Dexpr
上面代码函数实现写在类的内部,即内联,如果编译器对内联支持的好的话,上面代码几乎等价于如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include
#include
void
evaluate(
double
start,
double
end,
double
step) {
double
_temp = 1.0;
for
(
double
i=start; i
std::cout << -i / std::
sqrt
(_temp + i) <<
' '
;
}
int
main() {
evaluate(0.0, 10.0, 1.0);
std::cin.get();
return
0;
}
|
1
|
-0 -0.707107 -1.1547 -1.5 -1.78885 -2.04124 -2.26779 -2.47487 -2.66667 -2.84605
|
和表达式模板类似的技术还可以用到向量计算中,以避免产生临时向量变量,见文献[4] Expression templates 和文献[12]的后面。传统向量计算如下:
1
2
3
4
5
6
7
8
9
|
class
DoubleVec;
// DoubleVec 重载了 + - * / 等向量元素之间的计算
DoubleVec y(1000), a(1000), b(1000), c(1000), d(1000);
// 向量长度 1000
// 向量计算
y = (a + b) / (c - d);
// 等价于
DoubleVec __t1 = a + b;
DoubleVec __t2 = c - d;
DoubleVec __t3 = __t1 / __t2;
y = __t3;
|
模板代码实现向量计算如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
template
<
class
A> DVExpr;
class
DVec{
// ...
template
<
class
A>
};
DVec y(1000), a(1000), b(1000), c(1000), d(1000);
// 向量长度 1000
// 向量计算
y = (a + b) / (c - d);
// 等价于
for
(
int
i=0; i<1000; ++i) {
y[i] = (a[i] + b[i]) / (c[i] + d[i]);
}
|
不过值得一提的是,传统代码可以用 C++11 的右值引用提升性能,C++11 新特性我们以后再详细讨论。
我们这里看下文献[4] Expression templates 实现的版本,它用到了编译期多态,编译期多态示意代码如下(关于这种代码形式有个名字叫 curiously recurring template pattern, CRTP,见文献[4]):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 模板基类,定义接口,具体实现由模板参数,即子类实现
template
<
typename
D>
class
base {
public
:
void
f1() {
static_cast
this
).f1(); }
// 直接调用子类实现
int
f2()
const
{
static_cast
<
const
E&>(*
this
).f1(); }
};
// 子类
class
dirived1 :
public
base
public
:
void
f1() {
/* ... */
}
int
f2()
const
{
/* ... */
}
};
template
<
typename
T>
class
dirived2 :
public
base
public
:
void
f1() {
/* ... */
}
int
f2()
const
{
/* ... */
}
};
|
简化后(向量长度固定为1000,元素类型为 double)的向量计算代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#include
// A CRTP base class for Vecs with a size and indexing:
template
<
typename
E>
class
VecExpr {
public
:
double
operator[](
int
i)
const
{
return
static_cast
operator E
const
&()
const
{
return
static_cast
<
const
E&>(*
this
); }
// 向下类型转换
};
// The actual Vec class:
class
Vec :
public
VecExpr
double
_data[1000];
public
:
double
& operator[](
int
i) {
return
_data[i]; }
double
operator[](
int
i)
const
{
return
_data[i]; }
template
<
typename
E>
Vec
const
& operator=(VecExpr
const
& vec) {
E
const
& v = vec;
for
(
int
i = 0; i<1000; ++i) _data[i] = v[i];
return
*
this
;
}
// Constructors
Vec() { }
Vec(
double
v) {
for
(
int
i=0; i<1000; ++i) _data[i] = v; }
};
template
<
typename
E1,
typename
E2>
class
VecDifference :
public
VecExpr
E1
const
& _u; E2
const
& _v;
public
:
VecDifference(VecExpr
const
& u, VecExpr
const
& v) : _u(u), _v(v) { }
double
operator[](
int
i)
const
{
return
_u[i] - _v[i]; }
};
template
<
typename
E>
class
VecScaled :
public
VecExpr
double
_alpha; E
const
& _v;
public
:
VecScaled(
double
alpha, VecExpr
const
& v) : _alpha(alpha), _v(v) { }
double
operator[](
int
i)
const
{
return
_alpha * _v[i]; }
};
// Now we can overload operators:
template
<
typename
E1,
typename
E2> VecDifference
const
operator-(VecExpr
const
& u, VecExpr
const
& v) {
return
VecDifference
}
template
<
typename
E> VecScaled
const
operator*(
double
alpha, VecExpr
const
& v) {
return
VecScaled
}
int
main() {
Vec u(3), v(1);
double
alpha=9; Vec y;
y = alpha*(u - v);
std::cout << y[999] <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
|
“alpha*(u – v)” 的类型推断过程如下图所示,其中有子类到基类的隐式类型转换:
这里可以看到基类的作用:提供统一的接口,让 operator- 和 operator* 可以写成统一的模板形式。
7. 特性,策略,标签
利用迭代器,我们可以实现很多通用算法,迭代器在容器与算法之间搭建了一座桥梁。求和函数模板如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include
#include
template
<
typename
iter>
typename
iter::value_type mysum(iter begin, iter end) {
typename
iter::value_type sum(0);
for
(iter i=begin; i!=end; ++i) sum += *i;
return
sum;
}
int
main() {
std::vector<
int
> v;
for
(
int
i = 0; i<100; ++i) v.push_back(i);
std::cout << mysum(v.begin(), v.end()) <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
4950
|
我们想让 mysum() 对指针参数也能工作,毕竟迭代器就是模拟指针,但指针没有嵌套类型 value_type,可以定义 mysum() 对指针类型的特例,但更好的办法是在函数参数和 value_type 之间多加一层 — 特性(traits)(参考了文献[1]第72页,特性详见文献[1] 12.1):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 特性,traits
template
<
typename
iter>
class
mytraits{
public
:
typedef
typename
iter::value_type value_type;
};
template
<
typename
T>
class
mytraits
public
:
typedef
T value_type;
};
template
<
typename
iter>
typename
mytraits
typename
mytraits
for
(iter i=begin; i!=end; ++i) sum += *i;
return
sum;
}
int
main() {
int
v[4] = {1,2,3,4};
std::cout << mysum(v, v+4) <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
|
其实,C++ 标准定义了类似的 traits:std::iterator_trait(另一个经典例子是 std::numeric_limits) 。特性对类型的信息(如 value_type、 reference)进行包装,使得上层代码可以以统一的接口访问这些信息。C++ 模板元编程会涉及大量的类型计算,很多时候要提取类型的信息(typedef、 常量值等),如果这些类型的信息的访问方式不一致(如上面的迭代器和指针),我们将不得不定义特例,这会导致大量重复代码的出现(另一种代码膨胀),而通过加一层特性可以很好的解决这一问题。另外,特性不仅可以对类型的信息进行包装,还可以提供更多信息,当然,因为加了一层,也带来复杂性。特性是一种提供元信息的手段。
策略(policy)一般是一个类模板,典型的策略是 STL 容器(如 std::vector<>,完整声明是template
标签(tag)一般是一个空类,其作用是作为一个独一无二的类型名字用于标记一些东西,典型的例子是 STL 迭代器的五种类型的名字(input_iterator_tag, output_iterator_tag, forward_iterator_tag, bidirectional_iterator_tag, random_access_iterator_tag),std::vector
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include
#include
template
<
typename
T1,
typename
T2>
// 通例,返回 false
class
theSameType {
public
:
enum
{ ret =
false
}; };
template
<
typename
T>
// 特例,两类型相同时返回 true
class
theSameType
public
:
enum
{ ret =
true
}; };
int
main(){
std::cout << theSameType< std::vector<
int
>::iterator::iterator_category,
std::random_access_iterator_tag >::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
|
1
|
有了这样的判断,还可以根据判断结果做更复杂的元编程逻辑(如一个算法以迭代器为参数,根据迭代器标签进行特例化以对某种迭代器特殊处理)。标签还可以用来分辨函数重载,第5节中就用到了这样的标签(recursion)(标签详见文献[1] 12.1)。
8. 更多类型计算
在第1节我们讲类型等价的时候,已经见到了一个可以判断两个类型是否等价的模板,这一节我们给出更多例子,下面是判断一个类型是否可以隐式转换到另一个类型的模板(参考了文献[6] Static interface checking):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#include
// whether T could be converted to U
template
<
class
T,
class
U>
class
ConversionTo {
typedef
char
Type1[1];
// 两种 sizeof 不同的类型
typedef
char
Type2[2];
static
Type1& Test( U );
// 较下面的函数,因为参数取值范围小,优先匹配
static
Type2& Test(...);
// 变长参数函数,可以匹配任何数量任何类型参数
static
T MakeT();
// 返回类型 T,用这个函数而不用 T() 因为 T 可能没有默认构造函数
public
:
enum
{ ret =
sizeof
(Test(MakeT()))==
sizeof
(Type1) };
// 可以转换时调用返回 Type1 的 Test()
};
int
main() {
std::cout << ConversionTo<
int
,
double
>::ret <<
'\n'
;
std::cout << ConversionTo<
float
,
int
*>::ret <<
'\n'
;
std::cout << ConversionTo<
const
int
&,
int
&>::ret <<
'\n'
;
std::cin.get();
return
0;
}
|
1
2
3
|
1
0
0
|
下面这个例子检查某个类型是否含有某个嵌套类型定义(参考了文献[4] Substitution failure is not an erro (SFINAE)),这个例子是个内省(反射的一种):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include
#include
// thanks to Substitution failure is not an erro (SFINAE)
template
<
typename
T>
struct
has_typedef_value_type {
typedef
char
Type1[1];
typedef
char
Type2[2];
template
<
typename
C>
static
Type1& test(
typename
C::value_type*);
template
<
typename
>
static
Type2& test(...);
public
:
static
const
bool
ret =
sizeof
(test
sizeof
(Type1);
// 0 == NULL
};
struct
foo {
typedef
float
lalala; };
int
main() {
std::cout << has_typedef_value_type
std::cout << has_typedef_value_type
'\n'
;
std::cin.get();
return
0;
}
|
1
2
|
1
0
|
这个例子是有缺陷的,因为不存在引用的指针,所以不用用来检测引用类型定义。可以看到,因为只涉及类型推断,都是编译期的计算,不涉及任何可执行代码,所以类的成员函数根本不需要具体实现。
9. 元容器
文献[1]第 13 章讲了元容器,所谓元容器,就是类似于 std::vector<> 那样的容器,不过它存储的是元数据 — 类型,有了元容器,我们就可以判断某个类型是否属于某个元容器之类的操作。
在讲元容器之前,我们先来看看伪变长参数模板(文献[1] 12.4),一个可以存储小于某个数(例子中为 4 个)的任意个数,任意类型数据的元组(tuple)的例子如下(参考了文献[1] 第 225~227 页):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include
class
null_type {};
// 标签类,标记参数列表末尾
template
<
typename
T0,
typename
T1,
typename
T2,
typename
T3>
class
type_shift_node {
public
:
typedef
T0 data_type;
typedef
type_shift_node
// 参数移位了
static
const
int
num = next_type::num + 1;
// 非 null_type 模板参数个数
data_type data;
// 本节点数据
next_type next;
// 后续所有节点数据
type_shift_node() :data(), next() { }
// 构造函数
type_shift_node(T0
const
& d0, T1
const
& d1, T2
const
& d2, T3
const
& d3)
:data(d0), next(d1, d2, d3, null_type()) { }
// next 参数也移位了
};
template
<
typename
T0>
// 特例,递归终止
class
type_shift_node
public
:
typedef
T0 data_type;
static
const
int
num = 1;
data_type data;
// 本节点数据
type_shift_node() :data(), next() { }
// 构造函数
type_shift_node(T0
const
& d0, null_type, null_type, null_type) : data(d0) { }
};
// 元组类模板,默认参数 + 嵌套递归
template
<
typename
T0,
typename
T1=null_type,
typename
T2=null_type,
typename
T3=null_type>
class
my_tuple {
public
:
typedef
type_shift_node
static
const
int
num = tuple_type::num;
tuple_type t;
my_tuple(T0
const
& d0=T0(),T1
const
& d1=T1(),T2
const
& d2=T2(),T3
const
& d3=T3())
: t(d0, d1, d2, d3) { }
// 构造函数,默认参数
};
// 为方便访问元组数据,定义 get
template
class
type_shift_node_traits {
public
:
typedef
typename
type_shift_node_traits
typedef
typename
node_type::data_type data_type;
static
node_type& get_node(type_shift_node
{
return
type_shift_node_traits
};
template
<
typename
T0,
typename
T1,
typename
T2,
typename
T3>
class
type_shift_node_traits<0, T0, T1, T2, T3> {
public
:
typedef
typename
type_shift_node
typedef
typename
node_type::data_type data_type;
static
node_type& get_node(type_shift_node
{
return
node; }
};
template
typename
type_shift_node_traits::data_type
get(my_tuple
return
type_shift_node_traits::get_node(tup.t).data;
}
int
main(){
typedef
my_tuple<
int
,
char
,
float
> tuple3;
tuple3 t3(10,
'm'
, 1.2f);
std::cout << t3.t.data <<
' '
<< t3.t.next.data <<
' '
<< t3.t.next.next.data <<
'\n'
;
std::cout << tuple3::num <<
'\n'
;
std::cout << get<2>(t3) <<
'\n'
;
// 从 0 开始,不要出现 3,否则将出现不可理解的编译错误
std::cin.get();
return
0;
}
|
1
2
3
|
10 m 1.2
3
1.2
|
C++11 引入了变长模板参数,其背后的原理也是模板递归(文献[1]第 230 页)。
利用和上面例子类似的模板参数移位递归的原理,我们可以构造一个存储“类型”的元组,即元容器,其代码如下(和文献[1]第 237 页的例子不同):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#include
// 元容器
template
<
typename
T0=
void
,
typename
T1=
void
,
typename
T2=
void
,
typename
T3=
void
>
class
meta_container {
public
:
typedef
T0 type;
typedef
meta_container
static
const
int
size = next_node::size + 1;
// 非 null_type 模板参数个数
};
template
<>
// 特例,递归终止
class
meta_container<
void
,
void
,
void
,
void
> {
public
:
typedef
void
type;
static
const
int
size = 0;
};
// 访问元容器中的数据
template
<
typename
C, unsigned i>
class
get {
public
:
static_assert(i
typedef
typename
get
typedef
typename
c_type::type ret_type;
};
template
<
typename
C>
class
get
public
:
static_assert(0
typedef
C c_type;
typedef
typename
c_type::type ret_type;
};
// 在元容器中查找某个类型,找到返回索引,找不到返回 -1
template
<
typename
T1,
typename
T2>
class
same_type {
public
:
enum
{ ret =
false
}; };
template
<
typename
T>
class
same_type
public
:
enum
{ ret =
true
}; };
template
<
bool
c,
typename
Then,
typename
Else>
class
IF_ { };
template
<
typename
Then,
typename
Else>
class
IF_<
true
, Then, Else> {
public
:
typedef
Then reType; };
template
<
typename
Then,
typename
Else>
class
IF_<
false
, Then, Else> {
public
:
typedef
Else reType; };
template
<
typename
C,
typename
T>
class
find {
template
<
int
i>
class
number {
public
:
static
const
int
ret = i; };
template
<
typename
C,
typename
T,
int
i>
class
find_i {
public
:
static
const
int
ret = IF_< same_type
number, find_i
};
template
<
typename
C,
typename
T>
class
find_i
public
:
static
const
int
ret = -1;
};
public
:
static
const
int
ret = find_i
};
int
main(){
typedef
meta_container<
int
,
int
&,
const
int
> mc;
int
a = 9999;
get
std::cout << mc::size <<
'\n'
;
std::cout << aref <<
'\n'
;
std::cout << find
std::cout << find
std::cin.get();
return
0;
}
|
1
2
3
4
|
3
9999
2
-1
|
上面例子已经实现了存储类型的元容器,和元容器上的查找算法,但还有一个小问题,就是它不能处理模板,编译器对模板的操纵能力远不如对类型的操纵能力强(提示:类模板实例是类型),我们可以一种间接方式实现存储“模板元素”,即用模板的一个代表实例(如全用 int 为参数的实例)来代表这个模板,这样对任意模板实例,只需判断其模板的代表实例是否在容器中即可,这需要进行类型过滤:对任意模板的实例将其替换为指定模板参数的代表实例,类型过滤实例代码如下(参考了文献[1]第 241 页):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 类型过滤,meta_filter 使用时只用一个参数,设置四个模板参数是因为,模板通例的参数列表
// 必须能够包含特例参数列表,后面三个参数设置默认值为 void 或标签模板
template
<
typename
T>
class
dummy_template_1 {};
template
<
typename
T0,
typename
T1>
class
dummy_template_2 {};
template
<
typename
T0,
typename
T1 =
void
,
template
<
typename
>
class
tmp_1 = dummy_template_1,
template
<
typename
,
typename
>
class
tmp_2 = dummy_template_2>
class
meta_filter {
// 通例,不改变类型
public
:
typedef
T0 ret_type;
};
// 匹配任何带有一个类型参数模板的实例,将模板实例替换为代表实例
template
<
template
<
typename
>
class
tmp_1,
typename
T>
class
meta_filter
void
, dummy_template_1, dummy_template_2> {
public
:
typedef
tmp_1<
int
> ret_type;
};
// 匹配任何带有两个类型参数模板的实例,将模板实例替换为代表实例
template
<
template
<
typename
,
typename
>
class
tmp_2,
typename
T0,
typename
T1>
class
meta_filter
void
, dummy_template_1, dummy_template_2> {
public
:
typedef
tmp_2<
int
,
int
> ret_type;
};
|
现在,只需将上面元容器和元容器查找函数修改为:对模板实例将其换为代表实例,即修改 meta_container<> 通例中“typedef T0 type;”语句为“typedef typename meta_filter
1
2
3
4
5
6
|
template
<
typename
,
typename
>
class
my_tmp_2;
// 自动将 my_tmp_2
typedef
meta_container<
int
,
float
, my_tmp_2<
float
,
int
>> mc2;
// 自动将 my_tmp_2
std::cout << find |
1
|
2
|
10. 总结
博文比较长,总结一下所涉及的东西:
进一步学习
C++ 确实比较复杂,这可能是因为,虽然 C++ 语言层次比较低,但它却同时可以实现很多高级特性。进一步学习 C++ 模板元编程的途径很多:
参考文献:
注:参考文献中所给的链接,打开不了的,可以参见我的另一篇博客配置浏览器
Johannes Koskinen 论文,Stanford 课程 PPT,Todd Veldhuizen 论文我网盘保存的副本 -
链接: http://pan.baidu.com/s/1ntJstvF 密码: hogb