1、从 python 说起
def add(a, b): return a + b; print add(3, 5); #8 print add(3.1, 5.1); #8.2 print add("abc", "abd"); #abcabd
上面使用 python 定义了一个 add 函数,用于返回两个值相加的结果。由于 python 是弱类型语言,所以在调用该函数时,可以使用各种类型,实现了在 C 语言中必须通过重载来完成的功能。
在 C++ 语言中,不能像上面一样去定义一个函数,因为 C++ 也是一门强类型语言,定义函数时,必须严格声明参数类型和返回值类型。不过,C++语言可以通过模板来实现类似的方案:
template<typename TR, typename T1, typename T2> TR add(T1& t1, T2& t2) { return t1 + t2; } int main() { int i(3); double j(5.1); std::cout<<add<double, int, double>(i,j)<<std::endl; }
虽然实现了相同的功能,但 C++ 与 python 的实现原理却不一样。 C++ 把各种类型相似函数的声明定义的重复性工作,交给编译器去完成,(一组完全相同的模板类型只生成一个函数代码,可以通过 nm 命令来验证),所以与之相关的函数调用都是静态调用,而 python 是在运行时检查数据的类型,所以 C++ 模板的运行效率要高得多。
2、模板参数的类型的自动推导
自动推导的原则是:编译器可以根据函数调用时给出的实参列表来推导模板参数值,与函数参数类型无关的模板参数无法推导,包括返回值类型。
如上面的例子,还可以使用 add<double>(i,j) 来调用,但却不能使用 add(i,j) ,但是可以利用 C++11 标准中的 auto 和 deltype 两个关键字配合,让编译器也能自动推导返回值类型:
template<typename T1, typename T2> auto add(T1& t1, T2& t2) ->decltype(t1 + t2) { return t1 + t2; } int main() { int i(3); double j(5.1); std::cout<<add(i,j)<<std::endl; }
但是,自动推导的的原则并不变,如果函数返回值是模板类型,仍然是不可推导的,上面的代码只是在模板声明和定义时,使用 auto 和 decltype 的技巧替代了返回值的模板类型。
3、Name-Mangling
中文的意思,类似于 "命令重整",它是一种规范编译器和链接器之间用于通信的符号表表示方法的协议。在 C++ 语言中,如 函数重载, namespace, class, template 等都需要 Name-Mangling 的支援。
如以下代码有两个重载函数:
void print(int i) { } void print(double d) { } int main() { }
使用 g++ 编译器,使用 nm a.out | grep print ,则输出结果是:
C++语言中规定 :以下划线加大写字母开头 或 以两个下划线开头的标识符都是C++语言中保留的标示符。所以 _Z5printd 和 _Z5printi 是保留的标识符,g++编译的目标文件中的符号使用 _Z 开头(C99标准),后面的数字指定数字后面方法名字符串的长度(侧面也说明方法名不能是数字开头,否则这里就乱了),方法名后面依次是类型(i 是 int, d 是 double)。
由于 C 语言的 Name-Mangling 与 C++ 语言的不一样的,所以在C语言中调用C++中的函数,需要使用关键字extern "C",它的目的就是以 C 语言的标准来进行 Name-Mangling。
以下是不同命名空间的 print() 的 Name-Mangling:
其中,N 表示命名空间,后面的 2 表示命名空间字符串长度,即后面的 n1 和 n2,命令空间字符串后面的数字表示函数名字符串长度,即后面的 print,再后面是函数参数,E 暂时我也不知道是什么意思。。。
模板函数,只有在调用时,才会生成相应类型的函数声明定义代码,同样可以通过 nm 命令来查看 Name-Mangling 表。也证明了,模板不是多态,或者说是编译时多态。另外,如果不同 cpp 文件中都调用了同一组类型的模板函数,则会在各自的 .o 文件里生成相应的 函数定义,则在链接时,将根据函数名、参数类型及模板参数值是否相同,只选取链接命令排在前面位置的 .o 文件里的函数进行链接。(这样,如果两个 .o 文件里声明了两个完全一样的模板函数,但函数内的实现却不同,如 g++ test1.o test2.o main.o -o main ,则优先选取 test1.o 中的该函数模板实现。该问题应该通过命名空间等良好的代码风格来避免)。类似的,当模板类中有静态成员时,也使用相同的处理方式,以便在多个目标文件中选择一个目标中的空间作为最终存储空间。
4、export template
这是 C++98标准提出的,它的作用是可以把模板的声明和实现分别放到头文件和源文件中,就跟平时使用的 c++ 编写的风格一致。但是由于它需要在编译与链接之间加入一个预链接的操作,来补全模板实例,而不是像一般模板那样先生成重复的模板实例后再消除重复,所以它可以缩短编译时间。虽然 export template 拥有良好的代码组织风格,又具有更好的编译速度,可是却已经在 C++0x 标准中被逐出了。这是因为,对于编译器厂商来说,为了支持该特性,需要对编译软件做出很大的改动,实现成本太高。目前,貌似常见的编译器中, Borland C++ 编译器是支持该特性的,但我们常用的 GCC 并不支持。
5、在一个类模板内部出现的类名,等价于该模板被调用时所生成的实例。
6、友元模板
友元函数模板与友元类模板的任何实例都是该类的友元,都可以访问类中的私有成员。
class Data { int id; template<typename T> friend class User; template<typename T> friend T print(Data&); };
7、成员函数模板
普通类中的成员函数模板:
class Print { int id; template<typename T> void print(T const &t); }; template<typename T> void Print::print(T const &t) { }
类模板中的成员函数模板:
template<typename TC> class Print { int id; template<typename T> void print(T const &t); }; template<typename TC> template<typename T> void Print<TC>::print(T const &t) { }
成员函数模板,可以在类中实现,也可以在类外实现。
8、模板参数可以分为:类型模板参数、非类型模板参数和模板型模板参数
非类型模板参数包括 整数及枚举值、指针及引用模板参数、函数指针模板、成员函数指针模板。由于模板参数需要在编译期确定,所以只有指向全局变量及外部变量(extern修饰)及类的静态变量的指针及引用才可以作为模板参数。经测试,不同的非类型模板参数,也会产生多份实例,如此看来,为什么不用构造函数参数代替呢?唯一的原因,可能也就省一个该变量的存储空间吧?
#include <iostream> template <typename T, unsigned size> class array { T elems[size]; public: void print(){} }; int main() { array<char, 10> a1; array<char, 20> a2; //a1.print(); //a2.print(); }
如果注释掉最后两个语句,则通过 nm 命令查不到有 array 的模板实例生成,可能是编译器优化了。如果打开注释,会发现 array 的模板实例生成两次, print 函数虽然与模板类型无关,但该函数的模板实例也会生成两次。
9、模板的缺点:晦涩难懂的编译错误和膨胀的代码
晦涩的编译错误,可以通过之前 c++0x 提案的 concept(概念) 来解决,它有点类似于 JAVA 中的接口,定义一种规范,只不过这种规范是用来约束和检查模板参数类型的是否符合要求,如果不符合要求,可在编译时立即发现错误,并给出明确提示。但非常可惜,该提案最终没有纳入 c++0x 新标准。但仍可继续关注,因为它带来的是一种更良好的编程范式,或许下一次会被加入新标准。
由于不同源文件的调用,导致多个目标文件中相同的模板函数重复实现,在最终链接时去重复的机制,会导致编译时间过长,另外不同模板实现中,如果仅仅是因为类型不同,而有细小改变,却不得不特化相应类型的完整特化代码,而不能使用类似于 if(T == int) 的代码,也会导致代码量增加。另外,在模板类中,如果用多个不同类型实例化模板,会生成多个实例代码,当类模板生成新实例时,其成员函数都会相应生成新的模板实例,包括那些与模板类型无关的成员函数,如 stl 中的 size() 函数等,解决方案是把这些类型无关函数提取出来,放到基类中。(不过我表示这种方法挺让人蛋疼的。。。不知道有没有更好的方法)
10、C++的模板与JAVA的泛型
JAVA 泛型的原理,是JAVA所有类都直接或间接继承 Object 类,在容器里以存储 Object 对象,这样就可以向容器中经过向上转型加入任何类型的对象,取出元素时,要通过向下转型来使用。JAVA泛型的好处是对于所有类型的参数,只有一个版本的类或者函数生成。但缺点是运行时效率,如不断的向上和向下转型,尤其是当容器中存储的是基本类型时,泛型不能使用原始类型作为类型参数,如int、double等,因为他们和Object之间没有直接的继承关系,因此在需要时只能使用包装类,如 Integer、Double分别予以替换,这样就带来了更多效率上的折损,而C++中没有这样的限制,因此模板类的增多只会影响编译的效率,却不会影响运行时的效率。
9、模板特化
10、变长模板
11、标签与 traits
附:不用条件判断语句和循环语句,实现输出 1 + 2 + 3 + .. + 100 的值,两种方法:
利用模板特化技术:
template <int i> int add() { return add<i-1>() + i; } template<> int add<1>() { return 1; } int main() { int sum = 0; const int max = 100; sum += add<max>(); std::cout<<sum<<std::endl; }
利用&&短路技术:
bool add(int& i, int& sum) { sum += i++; (i <= 100) && add(i, sum); return false; } int main() { int i = 1; int sum = 0; add(i,sum); std::cout<<sum<<std::endl; }
使用变长模板封装 LINUX 下 SHELL 颜色控制函数:
#include <string> #include <sstream> #include <iostream> typedef enum shell_color { //使用时先输出 " 33[" ,然后输出下面的枚举值,多个枚举值用分号隔开,最后输 出"m" 即可 SC_DEFAULT = 0, //重新设置属性到缺省设置 FONT_B = 1, //粗体 FONT_HALF_LIGHT = 2, //一半亮度 FONT_U = 4, //斜体 FONT_FLICKER = 5, //闪烁 SC_REVERSE = 7, //将背景与字体颜色相换 FONT_BLACK = 30, //黑色字体 FONT_RED = 31, //红色字体 FONT_GREEN = 32, //绿色字体 FONT_BROWN = 33, //棕色字体 FONT_BLUE = 34, //蓝色字体 FONT_PURPLE = 35, //紫色字体 FONT_BLUEST = 36, //青色字体 FONT_WHITE = 37, //白色字体 BG_BLACK = 40, //黑色背景 BG_RED = 41, //红色背景 BG_GREEN = 42, //绿色背景 BG_BROWN = 44, //棕色背景 BG_BLUE = 44, //蓝色背景 BG_PURPLE = 45, //紫色背景 BG_BLUEST = 46, //青色背景 BG_WHITE = 47, //白色背景 } SHELL_COLOR; std::string itos(int i) { std::string temp; std::stringstream ss; ss<<i; ss>>temp; return temp; } bool b = true; std::string head = "