当前位置:   article > 正文

46. C++ 模板元编程

c++ 模板元编程
1 C++模板的语法

函数模板(function template)和类模板(class template)的简单示例如下:

#include <iostream>

// 函数模板
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<T>& b)
};
// 在类模板外实现成员函数
template<typename T>
bool bignumber<T>::operator<(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
  • 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

程序输出如下:

1
0
  • 1
  • 2

关于模板(函数模板、类模板)的模板参数

  • 类型参数(type template parameter),用 typename 或 class 标记;
  • 非类型参数(non-type template parameter)可以是:整数及枚举类型、对象或函数的指针、对象或函数的引用、对象的成员指针,非类型参数是模板实例的常量;
  • 模板型参数(template template parameter),如“template<typename T, template class A> someclass {};”;
  • 模板参数可以有默认值(函数模板参数默认是从 C++11 开始支持);
  • 函数模板的和函数参数类型有关的模板参数可以自动推导,类模板参数不存在推导机制;
  • C++11 引入变长模板参数,请见下文。

模板特例化(template specialization,又称特例、特化)的简单示例如下:

// 实现一个向量类
template<typename T, int N>
class Vec{
    T _v[N];
// ... // 模板通例(primary template),具体实现
};

template<>
class Vec<float, 4>{
    float _v[4];
// ... // 对 Vec<float, 4> 进行专门实现,如利用向量指令进行加速
};

template<int N>
class Vec<bool, N>{
    char _v[(N+sizeof(char)-1)/sizeof(char)];
// ... // 对 Vec<bool, N> 进行专门实现,如用一个比特位表示一个bool
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

所谓模板特例化即对于通例中的某种或某些情况做单独专门实现,最简单的情况是对每个模板参数指定一个具体值,这成为完全特例化(full specialization),另外,可以限制模板参数在一个范围取值或满足一定关系等,这称为部分特例化(partial specialization),用数学上集合的概念,通例模板参数所有可取的值组合构成全集U,完全特例化对U中某个元素进行专门定义,部分特例化对U的某个真子集进行专门定义。

更多模板特例化的例子如下(参考了文献[1]第44页):

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<cp00<T, i>, cp00<T, i+10>, i, cp00>;
// 编译错误!,第四个参数类型和通例类型不一致
//template<template<int i> CP>
//class TMP<int, float, 10, CP>;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

关于模板特例化:

  • 在定义模板特例之前必须已经有模板通例(primary template)的声明;
  • 模板特例并不要求一定与通例有相同的接口,但为了方便使用(体会特例的语义)一般都相同;
  • 匹配规则,在模板实例化时如果有模板通例、特例加起来多个模板版本可以匹配,则依据如下规则:对版本AB,如果 A 的模板参数取值集合是B的真子集,则优先匹配 A,如果 AB 的模板参数取值集合是“交叉”关系(AB 交集不为空,且不为包含关系),则发生编译错误,对于函数模板,用函数重载分辨(overload resolution)规则和上述规则结合并优先匹配非模板函数。

对模板的多个实例,类型等价(type equivalence)判断规则(详见文献[2] 13.2.4):同一个模板(模板名及其参数类型列表构成的模板签名(template signature)相同,函数模板可以重载,类模板不存在重载)且指定的模板实参等价(类型参数是等价类型,非类型参数值相同)。如下例子:

#include <iostream>
// 识别两个类型是否相同,提前进入模板元编程^_^
template<typename T1, typename T2> // 通例,返回 false
class theSameType       { public: enum { ret = false }; };
template<typename T>               // 特例,两类型相同时返回 true
class theSameType<T, T> { public: enum { ret = true }; };

template<typename T, int i> class aTMP { };

int main(){
    typedef unsigned int uint; // typedef 定义类型别名而不是引入新类型
    typedef uint uint2;
    std::cout << theSameType<unsigned, uint2>::ret << '\n';
    // 感谢 C++11,连续角括号“>>”不会被当做流输入符号而编译错误
    std::cout << theSameType<aTMP<unsigned, 2>, aTMP<uint2, 2>>::ret << '\n';
    std::cout << theSameType<aTMP<int, 2>, aTMP<int, 3>>::ret << '\n';
    std::cin.get(); 
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
1
1
0
  • 1
  • 2
  • 3

关于模板实例化(template instantiation):

  • 指在编译或链接时生成函数模板或类模板的具体实例源代码,即用使用模板时的实参类型替换模板类型参数(还有非类型参数和模板型参数);
  • 隐式实例化(implicit instantiation):当使用实例化的模板时自动地在当前代码单元之前插入模板的实例化代码,模板的成员函数一直到引用时才被实例化;
  • 显式实例化(explicit instantiation):直接声明模板实例化,模板所有成员立即都被实例化;
  • 实例化也是一种特例化,被称为实例化的特例(instantiated (or generated) specialization)。

隐式实例化时,成员只有被引用到才会进行实例化,这被称为推迟实例化(lazy instantiation),由此可能带来的问题如下面的例子:

#include <iostream>

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<int>::f2() 不被实例化,从而上面的错误被掩盖!
    std::cin.get(); return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

所以模板代码写完后最好写个诸如显示实例化的测试代码,更深入一些,可以插入一些模板调用代码使得编译器及时发现错误,而不至于报出无限长的错误信息。另一个例子如下(GCC 4.8 下编译的输出信息,VS2013 编译输出了 500 多行错误信息):

#include <iostream>

// 计算 N 的阶乘 N!
template<int N>
class aTMP{
public:
    enum { ret = N==0 ? 1 : N * aTMP<N-1>::ret }; // Lazy Instantiation,将产生无限递归!
};

int main(){
    std::cout << aTMP<10>::ret << '\n';
    std::cin.get();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
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<N-1>::ret };
                            ^
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

上面的错误是因为,当编译 aTMP 时,并不判断 N==0,而仅仅知道其依赖 aTMP(lazy instantiation),从而产生无限递归,纠正方法是使用模板特例化,如下:

#include <iostream>

// 计算 N 的阶乘 N!
template<int N>
class aTMP{
public:
  enum { ret = N * aTMP<N-1>::ret };
};
template<>
class aTMP<0>{
public:
  enum { ret = 1 };
};

int main(){
    std::cout << aTMP<10>::ret << '\n';
    std::cin.get(); 
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
3228800
  • 1

关于模板的编译和链接(详见文献[1] 1.3、文献[4]模板):

  • 包含模板编译模式:编译器生成每个编译单元中遇到的所有的模板实例,并存放在相应的目标文件中;链接器合并等价的模板实例,生成可执行文件,要求实例化时模板定义可见,不能使用系统链接器;
  • 分离模板编译模式(使用 export 关键字):不重复生成模板实例,编译器设计要求高,可以使用系统链接器;
  • 包含编译模式是主流,C++11 已经弃用 export 关键字(对模板引入 extern 新用法),一般将模板的全部实现代码放在同一个头文件中并在用到模板的地方用 #include 包含头文件,以防止出现实例不一致(如下面紧接着例子);

实例化,编译链接的简单例子如下:

// file: a.cpp
#include <iostream>
template<typename T>
class MyClass { };
template MyClass<double>::MyClass(); // 显示实例化构造函数 MyClass<double>::MyClass()
template class MyClass<long>;        // 显示实例化整个类 MyClass<long>

template<typename T>
void print(T const& m) { std::cout << "a.cpp: " << m << '\n'; }

void fa() {
    print(1);   // print<int>,隐式实例化
    print(0.1); // print<double>
}
void fb(); // fb() 在 b.cpp 中定义,此处声明

int main(){
    fa();
    fb();
    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
// file: b.cpp
#include <iostream>
template<typename T>
void print(T const& m) { std::cout << "b.cpp: " << m << '\n'; }

void fb() {
    print('2'); // print<char>
    print(0.1); // print<double>
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
a.cpp: 1a.cpp: 0.1b.cpp: 2a.cpp: 0.1
  • 1

上例中,由于 a.cpp 和 b.cpp 中的 print 实例等价(模板实例的二进制代码在编译生成的对象文件 a.obj、b.obj 中),故链接时消除了一个(消除哪个没有规定,上面消除了 b.cpp 中的)。

关于 templatetypenamethis 关键字的使用(文献[4]模板,文献[5]):

  • 依赖于模板参数(template parameter,形式参数,实参英文为 argument)的名字被称为依赖名字(dependent name),C++标准规定,如果解析器在一个模板中遇到一个嵌套依赖名字,它假定那个名字不是一个类型,除非显式用 typename 关键字前置修饰该名字;
  • 和上一条 typename 用法类似,template 用于指明嵌套类型或函数为模板;
  • this 用于指定查找基类中的成员(当基类是依赖模板参数的类模板实例时,由于实例化总是推迟,这时不依赖模板参数的名字不在基类中查找,文献[1]第 166 页)。

一个例子如下(需要 GCC 编译,GCC 对 C++11 几乎全面支持,VS2013 此处总是在基类中查找名字,且函数模板前不需要 template):


#include <iostream>

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<T> {
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<T>::template f<22>(); } // 强制基类 f<22>()
    void df4() { ::f(); }                     // 强制全局 f()
};

int main(){
    Derived<aTMP<int>> a(10);
    a.df1(); a.df2(); a.df3(); a.df4();
    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
global f()member f(): 99member f(): 22global f()
  • 1

C++11 关于模板的新特性(详见文献[1]第15章,文献[4]C++11):

  • “>>” 根据上下文自动识别正确语义;
  • 函数模板参数默认值;
  • 变长模板参数(扩展 sizeof…() 获取参数个数);
  • 模板别名(扩展 using 关键字);
  • 外部模板实例(拓展 extern 关键字),弃用 export template。

在本文中,如无特别声明将不使用 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++ 属于静态类型语言,编译器对类型的操控能力很强)。代码示意如下:


#include <iostream>

template<typename T, int i=1>
class someComputing {
public:
  typedef volatile T* retType; // 类型计算
  enum { retValume = i + someComputing<T, i-1>::retValume }; // 数值计算,递归
  static void f() { std::cout << "someComputing: i=" << i << '\n'; }
};
template<typename T> // 模板特例,递归终止条件
class someComputing<T, 0> {
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<someComputing<int, 99>>::f();
    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
8125250someComputing: i=99
  • 1

C++ 模板元编程概览框图如下(取自文献[9]):

图片

下面我们将对图中的每个框进行深入讨论。

3 编译器数值计算

第一个 C++ 模板元程序是 Erwin Unruh 在 1994 年写的(文献[14]),这个程序计算小于给定数 N 的全部素数(又叫质数),程序并不运行(都不能通过编译),而是让编译器在错误信息中显示结果(直观展现了是编译期计算结果,C++ 模板元编程不是设计的功能,更像是在戏弄编译器,当然 C++11 有所改变),由于年代久远,原来的程序用现在的编译器已经不能编译了,下面的代码在原来程序基础上稍作了修改(GCC 4.8 下使用 -fpermissvie,只显示警告信息):


// 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<i-1> a;
    enum { prim = is_prime<i, i-1>::prim };
    // prim 为真时, prim?1:0 为 1,int 到 D<i> 转换报错;假时, 0 为 NULL 指针不报错
    void f() { D<i> d = prim?1:0; a.f(); } // 调用 a.f() 实例化 Prime_print<i-1>::f()
};
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<LAST> a; a.f(); // 必须调用 a.f() 以实例化 Prime_print<LAST>::f()

  • 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

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<i>::D(void*) [with int i = 2]' [-fpermissive]
 template<int i> struct D { D(void*); operator int(); };
                            ^
main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 7]':
main.cpp:13:36:   recursively required from 'void Prime_print<i>::f() [with int i = 9]'
main.cpp:13:36:   required from 'void Prime_print<i>::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<i> d = prim ? 1 : 0; a.f(); }
                                 ^
main.cpp:2:28: warning:   initializing argument 1 of 'D<i>::D(void*) [with int i = 7]' [-fpermissive]
 template<int i> struct D { D(void*); operator int(); };
                            ^
main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 5]':
main.cpp:13:36:   recursively required from 'void Prime_print<i>::f() [with int i = 9]'
main.cpp:13:36:   required from 'void Prime_print<i>::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<i> d = prim ? 1 : 0; a.f(); }
                                 ^
main.cpp:2:28: warning:   initializing argument 1 of 'D<i>::D(void*) [with int i = 5]' [-fpermissive]
 template<int i> struct D { D(void*); operator int(); };
                            ^
main.cpp: In instantiation of 'void Prime_print<i>::f() [with int i = 3]':
main.cpp:13:36:   recursively required from 'void Prime_print<i>::f() [with int i = 9]'
main.cpp:13:36:   required from 'void Prime_print<i>::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<i> d = prim ? 1 : 0; a.f(); }
                                 ^
main.cpp:2:28: warning:   initializing argument 1 of 'D<i>::D(void*) [with int i = 3]' [-fpermissive]
 template<int i> struct D { D(void*); operator int(); };                           ^
  • 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

上面的编译输出信息只给出了前一部分,虽然信息很杂,但还是可以看到其中有 10 以内全部素数:2、3、5、7(已经加粗显示关键行)。

到目前为止,虽然已经看到了阶乘、求和等递归数值计算,但都没涉及原理,下面以求和为例讲解 C++ 模板编译期数值计算的原理:

#include <iostream>

template<int N>
class sumt{
public: static const int ret = sumt<N-1>::ret + N;
};
template<>
class sumt<0>{
public: static const int ret = 0;
};

int main() {
    std::cout << sumt<5>::ret << '\n';
    std::cin.get(); return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
15
  • 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 模板下的控制结构

模板实现的条件 ifwhile 语句如下:

// 通例为空,若不匹配特例将报错,很好的调试手段(这里是 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_<Condition<Statement>::ret,
        WHILE_<Condition, typename Statement::Next>,
        STOP<Statement>>::reType::reType
    reType;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

IF_<> 的使用示例见下面:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
4
  • 1

WHILE_<> 的使用示例见下面:

// 计算 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<i,e-1>::ret }; };
template<int i> class pow_e<i,0>{ public: enum{ ret=1 }; };
// 计算 i^e,嵌套类使得能够定义嵌套模板元函数,private 访问控制隐藏实现细节
template<int i> class pow{ public: enum{ ret=pow_e<i,e>::ret }; };
template<typename stat>
class cond { public: enum{ ret=(stat::ri<=n) }; };
template<int i, int sum>
class stat { public: typedef stat<i+1, sum+pow<i>::ret> Next;
enum{ ri=i, ret=sum }; };
public:
enum{ ret = WHILE_<cond, stat<1,0>>::reType::ret };
};

int main() {
    std::cout << sum_pow<10, 2>::ret << '\n';
    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
385
  • 1
**9 **元容器

所谓元容器,就是类似于 std::vector<> 那样的容器,不过它存储的是元数据 – 类型,有了元容器,我们就可以判断某个类型是否属于某个元容器之类的操作。

在讲元容器之前,我们先来看看伪变长参数模板(文献[1] 12.4),一个可以存储小于某个数(例子中为 4 个)的任意个数,任意类型数据的元组(tuple)的例子如下(参考了文献[1] 第 225~227 页):


#include <iostream>

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<T1, T2, T3, null_type> next_type; // 参数移位了
    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<T0, null_type, null_type, null_type> {
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<T0, T1, T2, T3> tuple_type;
    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<unsigned>(tuple) 函数模板
template<unsigned i, typename T0, typename T1, typename T2, typename T3>
class type_shift_node_traits {
public:
    typedef typename
        type_shift_node_traits<i-1,T0,T1,T2,T3>::node_type::next_type node_type;
    typedef typename node_type::data_type data_type;
    static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node)
{ return type_shift_node_traits<i-1,T0,T1,T2,T3>::get_node(node).next; }
};
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<T0,T1,T2,T3> node_type;
    typedef typename node_type::data_type data_type;
    static node_type& get_node(type_shift_node<T0,T1,T2,T3>& node)
{ return node; }
};
template<unsigned i, typename T0, typename T1, typename T2, typename T3>
typename type_shift_node_traits<i,T0,T1,T2,T3>::data_type
get(my_tuple<T0,T1,T2,T3>& tup) {
    return type_shift_node_traits<i,T0,T1,T2,T3>::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
  • 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
10 m 1.231.2
  • 1

C++11 引入了变长模板参数,其背后的原理也是模板递归(文献[1]第 230 页)。

利用和上面例子类似的模板参数移位递归的原理,我们可以构造一个存储“类型”的元组,即元容器,其代码如下:


#include <iostream>

// 元容器
template<typename T0=void, typename T1=void, typename T2=void, typename T3=void>
class meta_container {
public:
    typedef T0 type;
    typedef meta_container<T1, T2, T3, void> next_node; // 参数移位了
    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<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    typedef typename get<C,i-1>::c_type::next_node c_type;
    typedef typename c_type::type ret_type;
};
template<typename C>
class get<C, 0> {
public:
    static_assert(0<C::size, "get<C,i>: index exceed num"); // C++11 引入静态断言
    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<T, T> { 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<get<C,i>::ret_type, T>::ret,
            number<i>, find_i<C,T,i-1> >::reType::ret;
    };
    template<typename C, typename T>
    class find_i<C, T, -1> {
    public:
        static const int ret = -1;
    };
public:
    static const int ret = find_i<C, T, C::size-1>::ret;
};

int main(){
    typedef meta_container<int, int&, const int> mc;
    int a = 9999;
    get<mc, 1>::ret_type aref = a;
    std::cout << mc::size << '\n';
    std::cout << aref << '\n';
    std::cout << find<mc, const int>::ret << '\n';
    std::cout << find<mc, float>::ret << '\n';
    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
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
399992-1
  • 1

上面例子已经实现了存储类型的元容器,和元容器上的查找算法,但还有一个小问题,就是它不能处理模板,编译器对模板的操纵能力远不如对类型的操纵能力强(提示:类模板实例是类型),我们可以一种间接方式实现存储“模板元素”,即用模板的一个代表实例(如全用 int 为参数的实例)来代表这个模板,这样对任意模板实例,只需判断其模板的代表实例是否在容器中即可,这需要进行类型过滤:对任意模板的实例将其替换为指定模板参数的代表实例,类型过滤实例代码如下(参考了文献[1]第 241 页):

// 类型过滤,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<tmp_1<T>, 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<tmp_2<T0, T1>, void, dummy_template_1, dummy_template_2> {
public:
    typedef tmp_2<int, int> ret_type;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

现在,只需将上面元容器和元容器查找函数修改为:对模板实例将其换为代表实例,即修改 meta_container<> 通例中“typedef T0 type;”语句为“typedef typename meta_filter::ret_type type;”,修改 find<> 的最后一行中“T”为“typename meta_filter::ret_type”。修改后,下面代码的执行结果是:

template<typename, typename> class my_tmp_2;

// 自动将 my_tmp_2<float, int> 过滤为 my_tmp_2<int, int>
typedef meta_container<int, float, my_tmp_2<float, int>> mc2;
// 自动将 my_tmp_2<char, double> 过滤为 my_tmp_2<int, int>
std::cout << find<mc2, my_tmp_2<char, double>>::ret << '\n'; // 输出 2
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
2
  • 1
10 总结

博文比较长,总结一下所涉及的东西:

  • C++ 模板包括函数模板和类模板,模板参数形式有:类型、模板型、非类型(整型、指针);
  • 模板的特例化分完全特例化和部分特例化,实例将匹配参数集合最小的特例;
  • 用实例参数替换模板形式参数称为实例化,实例化的结果是产生具体类型(类模板)或函数(函数模板),同一模板实参完全等价将产生等价的实例类型或函数;
  • 模板一般在头文件中定义,可能被包含多次,编译和链接时会消除等价模板实例;
  • template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果;
  • C++11 对模板引入了新特性:“>>”、函数模板也可以有默认参数、变长模板参数、外部模板实例(extern),并弃用 export template;
  • C++ 模板是图灵完备的,模板编程是函数编程风格,特点是:没有可变的存储、递归,以“<>”为输入,typedef 或静态常量为输出;
  • 编译期数值计算虽然实际意义不大,但可以很好证明 C++ 模板的能力,可以用模板实现类似普通程序中的 if 和 while 语句;
  • 一个实际应用是循环展开,虽然编译器可以自动循环展开,但我们可以让这一切更可控;
  • C++ 模板编程的两个问题是:难调试,会产生冗长且难以阅读的编译错误信息、代码膨胀(源代码膨胀、二进制对象文件膨胀),改进的方法是:增加一些检查代码,让编译器及时报错,使用特性、策略等让模板更通用,可能的话合并一些模板实例(如将代码提出去做成单独模板);
  • 表达式模板和向量计算是另一个可加速程序的例子,它们将计算表达式编码到类型,这是通过模板嵌套参数实现的;
  • 特性,策略,标签是模板编程常用技巧,它们可以是模板变得更加通用;
  • 模板甚至可以获得类型的内部信息(是否有某个 typedef),这是反射中的内省,C++ 在语言层面对反射支持很少(typeid),这不利于模板元编程;
  • 可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归;
  • 用实例参数替换模板形式参数称为实例化,实例化的结果是产生具体类型(类模板)或函数(函数模板),同一模板实参完全等价将产生等价的实例类型或函数;
  • 模板一般在头文件中定义,可能被包含多次,编译和链接时会消除等价模板实例;
  • template、typename、this 关键字用来消除歧义,避免编译错误或产生不符预期的结果;
  • C++11 对模板引入了新特性:“>>”、函数模板也可以有默认参数、变长模板参数、外部模板实例(extern),并弃用 export template;
  • C++ 模板是图灵完备的,模板编程是函数编程风格,特点是:没有可变的存储、递归,以“<>”为输入,typedef 或静态常量为输出;
  • 编译期数值计算虽然实际意义不大,但可以很好证明 C++ 模板的能力,可以用模板实现类似普通程序中的 if 和 while 语句;
  • 一个实际应用是循环展开,虽然编译器可以自动循环展开,但我们可以让这一切更可控;
  • C++ 模板编程的两个问题是:难调试,会产生冗长且难以阅读的编译错误信息、代码膨胀(源代码膨胀、二进制对象文件膨胀),改进的方法是:增加一些检查代码,让编译器及时报错,使用特性、策略等让模板更通用,可能的话合并一些模板实例(如将代码提出去做成单独模板);
  • 表达式模板和向量计算是另一个可加速程序的例子,它们将计算表达式编码到类型,这是通过模板嵌套参数实现的;
  • 特性,策略,标签是模板编程常用技巧,它们可以是模板变得更加通用;
  • 模板甚至可以获得类型的内部信息(是否有某个 typedef),这是反射中的内省,C++ 在语言层面对反射支持很少(typeid),这不利于模板元编程;
  • 可以用递归实现伪变长参数模板,C++11 变长参数模板背后的原理也是模板递归;
  • 元容器存储元信息(如类型)、类型过滤过滤某些类型,它们是元编程的高级特性。
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号