【C++】深入理解模板
2017-12-12 12:25
447 查看
1、简介
模板是一种代码复用方式,其它的代码复用方式还包括继承和组合。当我们使用模板时,参数由编译器来替换,这非常像原来的宏方法,但却更清晰、更容易使用。在C++中,模板实现了参数化类型的概念,放在一对尖括号中,通过template这个关键字,告诉编译器随后的定义将操作一个或更多未指明的类型,当由这个模板产生实际代码时,必须指定这些类型以使编译器能够替换它们。下面是一个简单的模板类。
template <class T> // 模板类 T为未知类型 其它的为模板相关的关键字和固定格式 class Object { public: T getValue() const { return mValue; } // 内联定义 void setValue(T value); private: T mValue; }; template <class T> // 注意添加template声明及参数列表Object<T> void Object<T>::setValue(T value) // 非内联定义 { mValue = value; } int main() { Object<int> a; // 编译器用int类型扩展模板类Object并进行实例化 a.setValue(100); a.getValue(); return 0; }
即使是在创建非内联函数定义时,我们还是通常把模板的所有声明和定义都放入一个头文件中,这似乎违背了通常的头文件规则,即不要放置分配存储空间的任何东西,这条规则是为了防止在链接期间的多重定义错误,但模板定义很特殊,在templage声明之后的任何东西都意味着在当时不为它分配存储空间,而是一直处于等待状态直到被一个模板示例告知。在编译器和链接器有机制能去掉同一模板的多重定义,所以为了使用方便,几乎总是在头文件中放置全部的模板声明和定义。
2、函数模板
除了上面提到的类模板,模板还可以应用于函数,见下面的例子。template <class T> void foo(T t) {} int main() { foo(100); return 0; }
后面还会对函数模板作个详细的介绍。
3、无类型模板参数
模板参数并不局限于类定义的类型,可以使用编译器内置类型,这些参数值在编译期间变成模板的特定示例的常量。在类模板中,可以为模板参数提供默认参数,但是在函数模板中却不行,见下面的例子。template <class T = int, int size = 100> class Array { T array[size]; }; int main() { Array<int, 10> a; Array<int> b; Array<> c; return 0; }
尽管不能在函数模板中使用默认的模板参数,却能够用模板参数作为普通函数的默认参数,见下面的例子。
template <class T> T sum(T *b, T *e, T init = T()) { while (b != e) init += *b++; return init; } int main() { int a[] = {1, 2, 3}; int total = sum(a, a + sizeof a / sizeof a[0]); // total为6 init默认值为0 return 0; }
4、模板类型的模板参数
模板可以接受另一个类模板作为模板参数类型,如果想在代码中将一个模板类参数用作另一个模板,编译器首先需要知道这个参数是一个模板,见下面的例子。template <class T> class Array {}; template <class T, template<class> class Seq> // template <class T, template<class U> class Seq> // U可省略 class Container { Seq<T> seq; }; int main() { Conatiner<int, Array> container; return 0; }
对于模板参数中的默认参数,必须在模板类作为模板参数类型时重复声明默认参数,见下面的例子,参数名同样不是必须的。
template <class T, int N = 100> class Array {}; template <class T, template<class, int = 100> class Seq> class Container { Seq<T> seq; };
5、typename与template
typename有两种作用,一是替代模板参数中的关键字class,二是声明模板参数类型中的嵌套类型,否则这个类型不能被识别,见下面的例子。template <typename T> class A { typename T::Type type_t; type_t t; // 等同于typename T::Type t; };
另外,在模板代码中,template关键字也能起到类似typename关键字的作用,用于提示编译器后面的符号为模板中的符号,例如尖括号解析为模板符号,而不是大于小于,见下面的例子。
template <class T, class traits, class Allocator> basic_string<T, traits, Allocator> to_string() const; template <class T, int size> basic_string<T> bitsetToString(const bitset<size> &bs) { return bs. template to_string<T, char_traits<T>, allocator<T> >(); }
6、成员模板
成员模板包括成员函数模板和成员类模板,在类中声明template,见下面的例子。template<typename T> class complext { public: template<class X> complex(const complex<X>&); }; template<typename T> template<class X> complex<T>::complex(const complex<X> &r) {} complex float x(1.2); complex double y(x); // y的T为double y的X为float template<class T> class Outer { public: template<class R> class Inner { public: void foo(); }; }; template<class T> template<class R> void Outer<T>::Inner<R>::void() {}
7、函数模板参数
函数模板可以像类模板一样,使用尖括号进行声明,见下面的例子,使用int类型进行特化。template<typename T> cont T& min(const T& a, const T& b) { return (a>b) ? b : a; } int x = min<int>(i, j);
不使用尖括号时,可以让编译器推断出参数类型,见下面的例子,如果两个参数的类型相同,自然没有问题,但是对于一个由模板参数来限定类型的函数参数,
C++系统不能提供标准转换,所以如果两个参数的类型不同时将出错,比如说一个int类型一个double类型,不能自动进行类型转换,一种解决方法是使用尖括号帮助类型转换,是int转换为double,另一种解决方法是干脆提供两个不同的模板参数,但此时的函数返回类型是个问题,不知道应该返回哪个类型是合适的。
int x = min(i, j); int x = min<double>(i, j); template<typename T, typename U> cont T& min(const T& a, const U& b) { return (a>b) ? b : a; }
若一个函数模板的返回类型是一个独立的模板参数,当调用它的时候就一定要明确指定它的类型,因为这时已经无法从函数参数中推断它的类型了,见下面的例子。
template<typename T> T fromString(const std::string& s) { std::istringstream is(s); T t; is >> t; return t; } int i = fromString<int>(std::string("1234"));
结合上面提到的对函数模板的说明,如果有一个函数模板,它的模板参数既作为参数类型又作为返回类型,那么一定要首先声明函数的返回类型参数,否则就不能省略掉函数参数表中的任何类型参数,见下面的例子。
template<typename R, typename P> R implicit_cast(cont R& p) { return p; } int i = 1; float f = implicit_cast<float>(i); int j = implicit_cast<int>(f); char *p = implicit_cast<char*>(i); // error 标准不支持这种转换
函数模板还可以取地址作为函数指针,传递给另一个函数作为参数,见下面的例子,g的参数为f时需至少指定它们中的一个的类型T,剩下的由编译器推断。
template<typename T> void f(T*) {} template<typename T> void g(void (*pf)(T*)) {} void h(void (*pf)(int*){) {} int main() { h(&f); // 编译器推断类型T为int h(&f<int>); // 明确指定T为int g<int>(&f<int>); 明确指定g和f的T为int g(&f<int>); 明确指定f的T为int 编译器推断g的T为int g<int>(&f); 明确指定g的T为int 编译器推断f的T为int return 0; }
8、函数模板重载
函数模板可以像普通函数一样,用相同的函数名进行重载,见下面的例子,编译器会选择一个最佳匹配函数,首先是强制调用的模板函数,然后是没有进行任何类型转换的准确匹配的普通函数,然后是模板,最后是需要进行类型转换的普通函数。template<typename T> const T& min(const T& a, cont T& b) { return a > b ? b : a; } template<typename T> const T& min(const T& a, cont T& b, const T& c); const char* min(const char* a, const char* b) { return (<strcmp(a, b) < 0) ? a : b; } double min(double a, double b) { return a < b ? a : b; } int main() { const char* s2 = "say", s1 = "knights"; min(1, 2); // template 没有template时将调用double版的min min(1,0, 2,0); // double 准确匹配 min(1, 2.0); // double 1转换为1.0 min(s1, s2); // const char* 准确匹配 min<>(s1, s2); // template 尖括号强制使用template return 0; }
对于重载的多个函数模板,当它们都满足调用请求时,编译器会选择一个模板特化度高的版本,见下面的例子,任何类型都可以匹配第一个模板,第二个模板比第一个模板的特化程度更高,因为只有指针类型才能够匹配它,第三个模板特化程度最高,仅仅能被指向const的指针匹配调用,如果特化程度不能进行区别对待,将带来二义性,编译器会报错,这种特征称为半有序,稍后还会详细介绍模板特化的内容。
template<class T> void f(T); template<class T> void f(T*); template<class T> void f(const T*); int main() { f(0); // T int i = 0; f(&i); // T* const int j = 0; f(&j); // const T* return 0; }
9、模板特化
模板特化指的是指定模板参数的类型,包括显式指定的类型和编译器推断出来的类型,显式特化见下面的例子,用指定的类型替换T,并且注意尖括号中的内容。template<class T> const T& min(const T& a, const T& b) {} template<> const char* const& min<const char*>(const char* const& a, const char* const& b) {}
在标准库中也有显式特化的例子,如下。
template<class T, class Allocator = allocator<T> > class vector {}; template<> class vector<bool, allocator<bool> > {};
类模板也可以半特化,这意味着在模板特化的某些的方法中至少还有一个方法,其模板参数是开放的,上面例子的模板特化vector限定了T为bool类型,但没有指定参数allocator的类型,如下的例子是个半特化。
template<class Allocator> class vector<bool, Allocator>;
上面提到了函数模板的半有序,同样,类模板也有半有序,见下面的例子,后面几个的用法中出现了二义性,因为找到了多个匹配的模板特化版本。
template<class T, class U> class C { public: void foo(); }; // 无特化 template<class U> class C<int , U> { public: void foo(); }; // T=int template<class T> class C<T, double> { public: void foo(); }; // U=double template<class T, class U> class C<T*, U> { public: void foo(); }; // T* template<class T, class U> class C<T, U*> { public: void foo(); }; // U* template<class T, class U> class C<T*, U*> { public: void foo(); }; // T* U* template<class T> class C<T, T> { public: void foo(); }; // U=T void test() { C<float, int>().foo(); // 无特化 C<int, float>().foo(); // T=int C<float, doble>().foo(); // U=double C<float, float>().foo(); // U=T C<float*, float>().foo(); // T* C<float, float*>().foo(); // U* C<float*, int*>().foo(); // T* U* C<int, int>().foo(); // error 二义性 C<double, double>().foo(); // error 二义性 C<float*, float*>().foo(); // error 二义性 C<int, int*>().foo(); // error 二义性 C<int*, int*>().foo(); // error 二义性 }
10、模板和友元
在类模板中声明一个友元函数模板,见下面的例子,使用了前置声明,friend声明的foo使用了尖括号,这告诉编译器foo是个函数模板,模板参数依赖于类模板的参数T,当然也可以使用一个独立的不依赖于类模板参数的模板参数。template<class T> class Friendly; template<class T> void foo(const Friendly<T>&); template<class T> class Friendly { friend void foo<>(const Friendly<T>&); }; template<class T> void foo(const Friendly<T> &r) {}
11、模板和继承
见下面的例子,类Base定义了一个静态成员变量count,用于记录实例化的个数,构造函数中加1,拷贝构造函数中加1,析构函数中减1,类Child和类Child2是类Base的两个子类,然后对Child和Child2分别实例化时,发现它们共享一个静态成员变量count,这是合理的,因为它们共用一个基类,那么,每个子类独自使用一个静态成员变量count可以吗?class Base { public: Base() { ++count; } Base(const Base&) { ++count; } ~Base() { -- count; } static int count; }; int Base::count = 0; class Child : public Base {}; class Child2: public Base {}; int main() { Child c; // count=1 Child c2; // count=2 Child2 c3; // count=3 return 0; }
接着上面的问题,每个子类独自使用一个静态成员变量count是可以的,方法是使用模板,见下面的例子,因为基类为模板,所以,基类对模板参数进行了扩展,所有的子类实际上都是派生于不同的基类。
template<class T> class Base { public: Base() { ++count; } Base(const Base<T>&) { ++count; } ~Base() { -- count; } static int count; }; template<class T> int Base<T>::count = 0; class Child : public Base<Child> {}; class Child2: public Base<Child2> {}; int main() { Child c; // count=1 Child c2; // count=2 Child2 c3; // count=1 return 0; }
12、模板元程序
模板元程序就是编译时编程,见下面的例子,用于求解斐波那契数列,是一个递归模板,模板特化提高了终止递归的条件,利用了模板的特性,所有结果都是在编译时完成的。template<int n> struct Fib { enum { val = Fib<n - 1>::val + Fib<n - 2>::val }; }; template<> struct Fib<1> { enum { val = 1 }; }; template<> struct Fib<0> { enum { val = 0 }; };
13、模板编译
一般将模板的完整定义放在一个独立的头文件中,而普通函数的定义与它们的声明一般是分离的,分别放于源文件和头文件中,这样做可能是基于下面的原因。1)头文件中的非内联函数体会导致函数多重定义,在链接时产生错误。
2)隐藏实现。
3)头文件越小,编译时间就越短。
对于模板来说,模板本质上不是代码,而是产生代码的指令,只有模板的实例化才是真正的代码。当一个编译器在编译期间已经看到了一个完整的模板定义,又在同一个翻译单元内碰到了这个模板实例化点的时候,它就必须涉及这样一个事实,一个相同的实例化点可能出现另一个翻译单元内,处理这种情况最普遍的方法,是在每一个翻译单元内都为这个实例化生成代码,让链接器清除这些副本,另一种特殊的方法也可以很好地处理这种情况,就是用不能被内联的内联函数和虚函数表,具体实现方式由编译器而定,这种模板编译方式称为模板的包含模型。包含模型的缺点是暴露了所有的代码实现,头文件比函数体分开编译时大多了,相比传统编译模型而言,大大增加了编译时间。
为了帮助减少包含模型所需要的大的头文件,
C++提供了两种代码组织机制,使得模板可以将声明与实现分离,一种是显式实例化,手工实例化每一个模板特化,一种是使用导出模板,export关键字,支持最大限度的独立的编译。显式实例化见下面的例子,不使用显示实例化即min_instance.cpp时,由于模板的声明与实现分离,链接器不能找到min的int和double特化版本,将出错,解决办法是使用min_instance.cpp中的显式实例化方式,为了手工实例化一个特定的模板特化,可以在该特化的声明前使用template关键字,min_instance.cpp包含了min.cpp而非min.h是因为编译器需要用模板定义来进行实例化。
// min.h #inndef MIN_H #define MIN_H template<typename T> const T& min(const T&, const T&); #endif // min.cpp #inndef MIN_CPP #define MIN_CPP #include "min.h" template<typename T> const T& min(const T& a, const T& b) { return (a > b) ? b : a; } #endif // min_instance.cpp #include "min.cpp" tempalte const int& min(const int&, const int&); tempalte const double& min(const double&, const double&); // min_int.cpp #include "min.h" void testmin_int() { min(1, 2); } // min_double.cpp #include "min.h" void testmin_double() { min(1.1, 2.2); } // main.cpp void testmin_int(); void testmin_double(); int main() { testmin_int(); testmin_double(); return 0; }
我们还可以手工实例化类和静态成员变量,当显式实例化一个类时,除了一些之前可能已经显式实例化了的成员外,特化所需要的所有成员函数都要进行实例化,与隐式实例化相比,隐式实例化只有被调用的成员函数才进行实例化。
前面介绍了显式实例化,下面介绍导出模板。导出模板并不常见,可能只有部分编译器支持,使用export关键字,见下面的例子。
// min.h #inndef MIN_H #define MIN_H export template<typename T> const T& min(const T&, const T&); #endif // min.cpp #include "min.h" export template<typename T> const T& min(const T& a, const T& b) { return (a > b) ? b : a; }
结束
相关文章推荐
- C++深入理解(4)------函数模板以及显式具体化(读书笔记)
- C++模板深入理解
- C++模板深入理解
- 【ThinkingInC++】73、深入理解模板
- 深入理解c++模板中的class与typename
- (C/C++学习笔记)函数模板的深入理解
- 深入理解C/C++数组和指针
- 深入理解C++中public、protected及private用法
- 深入理解C++异常
- 学习《深入理解C++对象模型》小结
- C++默认构造函数——深入理解
- [深入理解C++(二)]理解接口继承规则
- 深入理解C/C++ [Deep C (and C++)] (2)
- 深入理解C++对象模型之构造函数
- 深入理解C++中的vector类的用法及特性
- 理解C++模板
- 深入理解C++中的mutable关键字
- 深入理解C++的动态绑定和静态绑定
- 深入理解C++的动态绑定和静态绑定
- C++默认构造函数——深入理解