【C++】模板使用总结
文章目录
- 模板函数
- 模板参数的声明
- 模板函数的定义和实例化
- 模板函数重载
- 类模板
- 类模板的特化
- 局部特化
- 缺省模板实参
- 注意
- 非类型模板参数
- 非类型的类模板参数
- 非类型的函数模板参数
- 非类型模板参数的限制
- 技巧性基础知识
- 关键字typename
- 使用this->
- 成员模板
- 模板作为模板参数
- 零初始化
- 使用字符串作用函数模板的实参
- 包含模式
- 静多态
- 动多态和静多态两者的优点
- 使用模板实现Bridge Pattern
模板函数
模板参数的声明
template<comma-separated-list-of-parameters> // template<用逗号隔开的参数列表>
模板函数的定义和实例化
模板函数的定义:
template<typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
**模板函数的实例化:**使用具体类型代替模板参数。只要使用函数模板,编译器会自动进行实例化。
int a = max(1, 2); // 使用int实例化
float b = max(4.0f, 3.0f); // 使用float实例化
// 实例化时,编译器会自动生成实例化的代码。比如使用int实例化会生成如下代码:
inline int const& max(int const& a, int const& b)
{
return a < b ? b : a; // int支持a < b的调用
}
注意:用于实例化模板的类型,必须支持模板函数的所有操作
class A{}
A a1, a2;
A a3 = max(a1, a2); // 编译报错:A不支持<运算符
模板的编译:
- 实例化之前,先检查模板代码本身,查看语法是否正确;
- 实例化期间,检查模板代码,查看实参的所有调用是否都有效。
实参的推导:
模板可以根据实参的类型来推导出实例化的类型,但所有的实参必须正确匹配类型,不支持自动转换类型。
max(4, 7); // OK:两个实参的类型都是int,使用int实例化
max(4, 7.0); // ERROR: 两个实参的类型不同,不支持自动转换类型
// 解决方法
max(static_cast<double>(4), 7.0); // 强制类型转换
max<double>(4, 4.2); // 显示指定T的类型
模板函数重载
可以使用模板函数重载非模板函数
inline int const& max(int const& a, int const& b)
{
return a < b ? b : a;
}
// 使用模板函数重载
template<typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
int main()
{
max(2, 3); // 当模板和非模板函数都匹配时,优先匹配非模板函数
max(2.0f, 3.0f); // 模板函数有更优的匹配,调用模板函数
max<>(2, 3); // 显示声明调用模板函数
}
类模板
// xx.h文件
#include <stdexcept>
#include <vector>
template<typename T>
class Stack
{
private:
std::vector<T> elems;
public:
void push(T const&);
void pop();
T top() const;
bool empty() const
{
return elems.empty();
}
}
template<typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem);
}
template<typename T>
void Stack<T>::pop()
{
if(elems.empty()){
throw std::out_of_range("Stack<>::pop():empty stack");
}
elems.pop_back();
}
template<typename T>
T Stack<T>::top() const
{
if(elems.empty()){
throw std::out_of_range("Stack<>::top():empty stack");
}
return elems.back();
}
//===================================================================
// main.cpp文件
#include <iostream>
#include "xx.h"
int main()
{
try{
Stack<int> intStack;
Stack<std::string> stringStack;
intStack.push(7);
std::cout << intStack.top() << std::endl;
stringStack.push("hello");
std::cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch(std::exception const& ex){
std::cerr << "Exception:" << ex.what() << std::endl;
}
}
注意:基于不同类型的实例化,会生成不同的类型,比如:Stack<int>和Stack<float>是不同的类型。
类模板的特化
可以用实参来特化类模板,和函数模板的重载类似,可以指定特定类型的实现,来克服实例化模板的不足。
比如,实例化的实参必须支持模板参数的所有调用。对于不支持的类型,可以重载实现:
struct Number
{
int value;
}
template<typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
// 使用Number类型对模板函数进行特化,Number类型不支持max()的模板参数的'<'的调用,
// 特化对其进行特殊处理。
inline Number const& max(Number const& a, Number const& b)
{
return a.value < b.value ? b : a;
}
类模板的特化也是相同的道理,相当于对类重新实现。
// 对上面的Stack类模板进行特化,模板参数T替换成string
#include <stdexcept>
#include <deque>
using namespace std;
template<> // 在class前面用template<>来表示特化
class Stack<string>
{
private:
// 使用deque来管理内部元素,而不是vector
std::deque<string> elems;
public:
void push(string const&);
void pop();
string top() const;
bool empty() const
{
return elems.empty();
}
// 添加full()成员函数,特化相当于是重新实现stack类,不需要和模板保存相同
bool full() const{
return elems.full();
}
}
// 特化后成员函数不是模板,不用加template
void Stack<string>::push(string const& elem)
{
// 这里和模板的实现相同,但特化的实现可以模板的实现完全不同
elems.push_back(elem);
}
...略
局部特化
当有多个模板参数时,可以对部分模板参数进行局部特化
template<typename T1, typename T2>
class MyClass
{
...
}
// 两个模板参数相同
template<typename T>
class MyClass<T, T>
{
...
}
// 对第二个模板参数进行局部特化
template<typename T>
class MyClass<T, int>
{
...
}
// 对两个模板参数进行特化
template<typename T1, typename T2>
class MyClass<T1*, T2*>
{
...
}
// 局部特化可能产生二义性,某个实例化可以匹配多个局部特化时就会产生二义性而报错
Myclass<int, int> m; // 可以匹配MyClass<T, T>和MyClass<T, int>,报错
缺省模板实参
可以为类模板的模板参数定义缺省值,这个缺省值称为缺省模板实参,并可以引用它前面的模板参数。
// xx.h
template<typename T, typename CONT = std::vector<T>> // std::vector<T>的T引用了前面的T
class Stack
{
private:
// 内部容器的类型可以使用模板参数指定
CONT elems;
public:
void push(T const&);
void pop();
T top() const;
bool empty() const{
return elems.empty();
}
}
template<typename T, typename CONT>
void Stack<T, CONT>::push(T const& elem)
{
elems.push_back(elem);
}
...略
// Stack模板的使用
Stack<int> intStack; // CONT = 缺省值(std::vector<int>)
Stack<double, std::deque<double>> dblStack; // CONT = std::deque<double>
注意
类模板的实例化有个特殊的地方是:只有被调用的成员函数才会被实例化。对于开头的Stack<T>类模板,比如:
Stack<int> intStack;
intStack.push(1);
// 如果整个程序中Stack<int>只调用了push()函数,那么它的实例化代码如下:
class Stack<int>
{
private:
std::vector<int> elems;
public:
// 只有push()被调用,因此只有push()被实例化
void push(int const&);
}
void Stack<int>::push(int const& elem)
{
elems.push_back(elem);
}
非类型模板参数
非类型的类模板参数
template<typename T, int MAX_SIZE> // MAX_SIZE不是类型,也可以作为模板参数
class Stack
{
private:
T elems[MAX_SIZE];
int numElems;
public:
Stack();
void push(T const&);
void pop();
T top() const;
bool empty() const{
return numElems == 0;
}
bool full() const{
return numElems == MAX_SIZE;
}
}
template<typename T, int MAX_SIZE>
Stack<T, MAX_SIZE>::Stack(): numElems(0)
{
}
template<typename T, int MAX_SIZE>
void Stack<T, MAX_SIZE>::push(T const& elem)
{
elems[numElems] = elem;
numElems++;
}
....略
// 使用
Stack<int, 20> int20Stack;
Stack<int, 40> int40Stack;
// 也可以使用缺省值
template<T, int MAX_SIZE = 20>
class Stack
{
}
Stack<int> intStack; // Stack<int, 20>
注意:非类型的模板参数不同也是不同的类型,Stack<int, 20>和Stack<int, 40>是不同的类型
非类型的函数模板参数
template<typename T, int VAL>
T addValue(T const& x)
{
return x + VAL;
}
// 也可以使用缺省值
template<typename T, int Val = 5>
T addValue(T const& x)
{
return x + VAL;
}
非类型模板参数的限制
合法:常整数(包括枚举值),指向外部链接对象的指针
非法:浮点数(历史遗留),类对象,字符串,指向内部链接对象的指针
template<char const* name>
class MyClass
{
...
}
char const* s = "hello";
MyClass<s> x; // ERROR:s为内部链接对象
extern char const* s = "hello";
MyClass<s> x; // OK:s为外部链接对象
(不确定,根据相关只是猜测) 如果是内部对象,则只被翻译单元所拥有,而编译发生在编译单元内,不同的编译单元内会生成相同的类型,而链接的时候,因为是内部链接而无法识别,导致不能优化重复类型。而使用外部链接对象编译时,链接的时候可以识别为相同的类型而进行优化。
技巧性基础知识
关键字typename
如果要访问依赖模板参数的类型名称,要在类型名称前添加关键字typename
class Type
{
class SubType
{
}
}
template<typename T>
class MyClass
{
typename T::SubType* ptr; // typename声明SubType是定义于类T内部的一种类型
T::SubType* ptr; // 不使用typename,T::SubType表示T中的静态成员,表示乘法操作
...
}
使用this->
如果基类是类模版,不能直接调用基类的成员,必须用this->去调用
template<typename T>
class Base
{
public:
void exit1()
{
std::cout << "call Base::exit1()." << std::endl;
}
};
void exit1()
{
std::cout << "call exit1()." << std::endl;
}
template<typename T>
class Derived : public Base<T>
{
public:
void foo()
{
exit1(); // call exit1().
this->exit1(); // call Base::exit1().
Base<T>::exit1(); // call Base::exit1().
}
};
成员模板
嵌套类和成员函数也可以是模板
template<typename T>
class Stack
{
private:
std::deque<T> elemes;
public:
// 成员函数模板
template<typename T2>
Stack<T>& operator=(Stack<T2> const&);
}
template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(Stack<T2> const& op2)
{
if((void*)this == (void*)&op2){
return *this;
}
Stack<T2> temp(op2);
elems.clear();
while(!temp.empty()){
elems.push_front(temp.top());
temp.pop();
}
return *this;
}
模板作为模板参数
// typename T1 = allocator<T>> class CONT是模板参数,但它也是一个模板,缺省值是std::deque模板,CONT和缺省值std::deque两个模板的模板参数必须完全匹配。
template<typename T, template<typename ELEM, typename T1 = allocator<ELEM>> class CONT = std::deque>
class Stack
{
CONT<T> elems;
}
零初始化
任何未被初始化的局部变量都具有一个不确定的(undefined)值。
模板参数的初始化方式:
template<typename T>
class MyClass
{
T x;
MyClass(): x(){} // 初始化方法1
void foo()
{
x = T(); // 初始化方法2
}
}
对于内建类型,比如int,bool等,也可以采用这种方法初始化。
这两种初始化方式是通用的,指针也可以这样初始化,初始化为0。
使用字符串作用函数模板的实参
字符常量的类型为const char[]。比如“name”的类型为const char[5],5 = 4个字符 + 一个’\0’。
const char[4]和const char[5]是不同的类型,它门占用的内存大小不相同。
// 使用引用时,字符串常量模板实参为其本身,即字符常量数组const char[]
template<typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
string s;
::max("apple", "peach"); // OK:相同类型的实参,都为const char[6]
::max("apple", "tomato"); // ERROR:不同类型的实参,const char[6], const char[7]
::max("apple", s); // ERROR:不同类型的实参,const char[6], string
========================================================================================
// 不使用引用时,字符串常量作为参数会退化成const char*指针
template<typename T>
inline T max(T a, T b)
{
return a < b ? b : a;
}
string s;
::max("apple", "peach"); // OK:相同类型的实参,都为const char*
::max("apple", "tomato"); // OK:相同类型的实参,都为const char*
::max("apple", s); // ERROR:不同类型的实参,const char*, string
包含模式
当我们组织模板代码时,对于模板函数,把模板函数的声明放在.h文件,模板函数的定义放在.cpp文件;对于模板类,将模板类的定义放在.h文件,成员函数的定义放在.cpp文件,编译器编译时会报错。
// xx.h
template<typename T>
inline T const& max(T const& a, T const& b); // 模板函数的声明
template<typename T>
class MyClass // 模板类的定义
{
public:
void Foo(); // 成员函数的声明
}
// xx.cpp
template<typename T>
inline T const& max(T const& a, T const& b) // 模板函数的定义
{
return a < b ? b : a;
}
template<typename T>
void MyClass<T>::Foo() // 成员函数的定义
{
}
// main.cpp
#include<xx.h>
int main()
{
max(2, 3); // ERROR:无法解析外部符号max<int>()
MyClass<int> myClass;
myClass.Foo(); // ERROR:无法解析外部符号MyClass<int>::Foo()
return 0;
}
首先,这个报错发生在链接阶段。在编译阶段,调用max()和MyClass::Foo()时,编译器只看到了它们的声明,但编译依然会通过。编译器会生成一个指向定义的引用,让链接器在链接的时候将引用指向定义。但是cpp文件作为一个编译单元是互相独立的,就是xx.cpp文件编译时,编译器不知道main.cpp文件中对max()和MyClass::Foo()的调用,也就不会实例化Foo()和MyClass,也就不会生成max()和MyClass::Foo()的定义,这样链接器链接的时候就找不到定义。
包含模型是对上面问题的一个解决方案,将模板的声明和定义都放在.h文件。c++自带的头文件和STL就是采用的这种方式。
// xx.h
template<typename T>
inline T const& max(T const& a, T const& b); // 模板函数的声明
template<typename T>
void MyClass<T>::Foo() // 成员函数的定义
{
}
template<typename T>
class MyClass; // 模板类的声明
template<typename T>
class MyClass // 模板类的定义
{
public:
void Foo(){} // 成员函数的声明和定义
void Print(); // 成员函数的声明
}
template<typename T> // 成员函数的定义
void Print()
{
}
// 类的成员函数默认都是内联的,如果将成员函数的定义放到.cpp文件就不是内联的了。
如果有多个cpp文件包含xx.h,如果每个cpp文件都会对xx.h中的模板进行max()的实例化,这样max()不是有多个定义了吗?不会产生多个定义,c++链接时会进行优化。但是特别是当包含c++的头文件时(比如等),由于模板,编译器会产生大量的模板实例代码,很大的增加了编译的复杂程度,使编译的时间也大量增加(每个文件都会编译大量相同的模板实例)。对于这个问题也有解决方案:
- 分离模型,并不是所有的编译器都支持
- 预编译头文件
静多态
一般,使用虚函数和继承实现的是动多态,即在运行期间确定调用者的类型。使用模板,可以实现静多态,在编译期间确定调用者的类型。
例如我们要对某一类对象进行统一处理,使用虚函数可以这样实现:
class BaseType
{
public:
virtual void action1(){...}
virtual void action2(){...}
}
class SubType1 : BaseType
{
public:
virtual void action1(){...}
virtual void action2(){...}
}
class SubType2 : BaseType
{
public:
virtual void action1(){...}
virtual void action2(){...}
}
// 统一处理
void DoAction1(BaseType const& type)
{
type.action1();
}
void DoAction2(BaseType* type)
{
type->action2();
}
// 可以处理异类集合
void DoActions(std::vector<BaseType*> list)
{
...
}
如果使用模板来实现,就会变成这样
class SubType1
{
public:
void action1(){...}
void action2(){...}
}
class SubType2
{
public:
void action1(){...}
void action2(){...}
}
// 统一处理
template<typename BaseType>
void DoAction1(BaseType const& type)
{
type.action1();
}
template<typename BaseType>
void DoAction2(BaseType* type)
{
type->action2();
}
// 不可以处理异类集合,因为要在编译期间确定,vector只能为一个类型的集合
template<typename BaseType>
void DoActions(std::vector<BaseType*> list) // 处理异类集合会报错
{
...
}
动多态和静多态两者的优点
动多态:
- 可以处理异类集合
- 生成的代码比较小,只需要一个多态函数,而静多态会生成多个实例化函数
静多态:
- 不需要和公共基类绑定,可以自由实现处理类型
- 代码运行效率更高,直接调用函数,比虚函数间接调用拥有更多内联的机会
使用模板实现Bridge Pattern
用一个指针引用具体的实现,然后把所有的调用都委托给这个(包含这个指针)的类。
使用多态实现:
// 实现的接口基类
class Implementation
{
virtual operationA() = 0;
virtual operationB() = 0;
virtual operationC() = 0;
}
// 实现A
class ImplementationA : Implementation
{
virtual operationA(){...}
virtual operationB(){...}
virtual operationC(){...}
}
// 实现B
class ImplementationB : Implementation
{
virtual operationA(){...}
virtual operationB(){...}
virtual operationC(){...}
}
class Bridge
{
public:
// 通过将body指向不同的实现类来调用不同的实现
Implementation* body;
void operationA()
{
body->operationA();
}
void operationB()
{
body->operationB();
}
}
使用模板实现,即使用静多态:
// 实现A
class ImplementationA : Implementation
{
virtual operationA(){...}
virtual operationB(){...}
virtual operationC(){...}
}
// 实现B
class ImplementationB : Implementation
{
virtual operationA(){...}
virtual operationB(){...}
virtual operationC(){...}
}
template<typename Implementation>
class Bridge
{
public:
// 使用不同的类型来生成不同实现的实例化类
Implementation* body;
void operationA()
{
body->operationA();
}
void operationB()
{
body->operationB();
}
}
使用模板实现桥接模式的一个例子就是C++STL的迭代器,通过迭代器间接执行元素的操作。在容器扩容的时候,有时会重新申请一块内存,并将原来的数据转移过去,如果外面有指针指向容器内的元素,那么这个指针就会失效,如果是指向迭代器,那么就不会有问题。
template<typename T>
class Iterator
{
private:
T value;
public:
T operator+(T const& right){...}
...
}