C++模板工具书

1 引言

不会模板,还真不敢说自己懂C++,不过对于平时项目比较少的小伙伴而言,即便爆肝学习了整个模板教程,过不了多久也就忘了,因而想写一个工具书,用时翻翻即可。

2 基本概念

2.1 为什么要有模板

  • 模板的目的很明确:类型参数化。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数 T 。
  • 有什么好处呢?比如让你定义一个两数相加的函数,要求支持int、float、double、short、long、bool甚至字符串。函数的基本功能都相似,只是类型不一样,如果针对每一个类型都写一个函数,不仅冗余容易出错,扩展性还差。模板却可以很优雅的解决这一问题,实现了真正的代码重用。习惯于使用STL的小伙伴就知道,STL就是标准模板库的英文首字母缩写。
  • 模板在编译阶段处理,会进行类型检查,而不是像define做简单的替换。

2.2 模板关键字

  • template
  • typename/class

2.3 模板的声明

模板分为函数模板和类模板,声明语句分别如下:

template <typename T>  
T sum(T t1, T t2);
template <typename T> 
class mData;

其中typename在大多数情况下可替换为class,不过个人还是习惯于在特定的场合使用typename或class,这样代码的可读性会更好。

2.4 模板的定义

2.4.1 函数模板的定义

目的是将相同功能的函数模板化
书写方法:

template <class 或 typename T>
返回类型 函数名(形参表)
{
    函数体
}
template <typename T>
T sum(T & t1, T & t2)
{
    return t1+t2;
}
2.4.1.1 函数模板与模板函数一样吗?

非也!模板函数表示的是由一个函数模板生成而来的函数。

//模板函数
sum<int>(12,34);
sum<float>(1.3f,2.4f);
2.4.1.2 注意事项
  • 在函数模板定义中声明的对象或类型不能与模板参数同名
typedef string T;//这个地方无所谓
template <typename T>//这里的T是模板参数,而不是string的别名
T sum(T & t1, T & t2)
{
	//typedef double T;这是不被允许的,因为T与模板参数同名。
    T ans =  t1+t2;
    return ans;
}
  • 模板参数名在同一模板参数表中只能被使用一次。
template <typename T,typename T>//错误!模板参数名T的非法重复定义
T sum(T & t1, T & t2);
  • 如果一个函数模板有一个以上的模板类型参数,则每个模板类型参数前面都必须有关键字class或typename
template <typename T1,T2>//错误!模板参数名T2的非法定义
T sum(T1 & t1, T2 & t2);
2.4.1.3 二次编译

先举个例子吧!

class A{
public:
	void func(){
		cout<<"A:func()"<<endl;
	}
};

template<class T>
void foo(){
	A a;
	a.func();//已知类型调用
	T t;
	t.func();//未知类型调用
	t.what();//未知类型调用
}

void main(){
	foo<A>();
}

其实foo发生了两次编译:

  • 第一次编译发生在实例化函数模板之前,只检查函数模板本身内部代码,查看基本词法是否正确!
    • 函数模板内部出现的所有标识符是否均有声明
    • 对于已知类型的调用要查看调用是否有效
    • 对于未知类型的调用认为都合理 ,除非带有<>。
class A{
public:
	template<class T>
	void foo(){...}
}

template<class D>
void Func(){
	D d;
	d.foo<int>();//错误,对于未知类型的调用认为都合理 ,除非带有<>。
	//怎么解决呢,有套路,虽然很丑
	d.template foo<int>();//正确
}
  • 第二次编译发生在实例化函数模板之后(产生真正函数实体之后)。
    • 结合所使用的类型实参,再次检查模板代码,查看所有调用是否真的均有效。
2.4.1.4 函数模板的重载
  • 普通函数和可实例化出该函数的函数模板构成重载关系。
    • 在数据类型匹配度相同的情况下编译器优先选择普通函数
    • 除非函数模板可以产生具有更好的数据类型匹配度的实例。
  • 函数模板的实例化不支持隐式类型转换但普通函数支持。
    • 在传递参数时如果需要编译器做隐式类型转换,则编译器选择普通函数
  • 可以在实例化时用<>强制通知编译器选择函数模板。
template<class T>
T max(T x,T y){...}

int max(int x,int y){...}

void main(){
	int x = 10,y = 20;
	cout<<max(x,y)<<endl;//选择普通函数
	cout<<max<>(x,y)<<endl;//选择函数模板,<>强制选择
	double dx = 1.2,dy = 2.3;
	cout<<max(dx,dy)<<endl;//选择函数模板
	cout<<max(x,dy)<<endl;//选择普通函数,隐式类型转换
}

2.4.2 类模板的定义

template <classtypename T>
class 类名
{
	...
}

个人习惯:为了避免模板关键字class与类关键字class混淆,推荐使用typename,PS:当然实际上编译器是认识的不会混淆,不过用成typename更清晰不是!

template <typename T>
class mData
{
private:
    T _val;
public:
    explicit mData(T & val) : _val(val) { }
    explicit mData(T && val) : _val(val) { }
    void add(T & t)
    {
        _val += t;
        std::cout<<_val<<std::endl;
    }
    void sub(T& t);
};

//成员函数在外部实现
template<typename T>
void mData<T>::sub(T& t){
	_val -= t;
    std::cout<<_val<<std::endl;
}

需要注意的是在类定义体外定义成员函数时,还需在类体外进行模板声明,比如上面的sub函数。

2.4.2.1 类模板与模板类一样吗?

非也!模板类表示的是由一个类模板生成而来的类。正式一些:模板类是由类模板实例化而成的一个类,这也就是2.5节所要讨论的内容。

//模板类
mData<int>
mData<float>
mData<double>
2.4.2.2 注意事项
  1. 模板类的函数类的声明与实现必须放在同一个.h文件内!!!(类模板中成员函数创建时机时在调用阶段,编译器不知道实现模板类的.cpp文件的存在)
  2. 如果必须要分开写,有两种方法:
    • 方法1:直接包含 .cpp 文件(一般是包含.h文件)
    • 方法2:将声明和实现写在同一个文件中,并更改后缀名为 .hpp,hpp 是约定的名称,并不是强制
2.4.2.3 类模板的使用
  • 使用类模板必须对类模板进行实例化(产生真正的类)
    • 其实很好理解,模板就是个模子,就像做饼干,只有往模子里放上东西烤熟,才算是饼干
    • 类模板本身并不代表一个确定的类型,也就不能去定义对象。只有通过类型实参实例化成真正的类后才具备类的语义(即可以定义对象)。
  • 类模板被实例化时类模板中的成员函数并没有实例化,成员函数只有在被调用时才被实例化(注:成员虚函数除外)。
    • 类模板中的普通成员函数可以是虚函数
    • 类模板中的成员函数模板不可以是虚函数,原因是成员函数模板的延迟编译阻碍了虚函数表的静态构建。
  • 某些类型虽然并没有提供类模板所需要的全部功能但照样可以实例化类模板,只要不调用那些未提供功能的成员函数即可。

后面的这两点是相当重要的,下面以一个例子说明:

template<class T>
class CMath{
private:
	T x,y;
public:
	CMath(T x,T y):x(x),y(y){}
	void add(){
		T sum = x + y;
		cout<<sum<<endl;
	}
}

class CPoint{
private:
	int x,y;
public:
	CPoint(int x,int y):x(x),y(y){}	
}

void main(){
	CMath<double> obj(12.3,45.6);//到目前为止,成员函数add()并没有被实例化
	obj.add();//这个时候add()才被实例化

	//怎么样理解第三点呢
	CPoint p1(12,34),p2(45,67);
	CMath<CPoint> obj2(p1,p2);//CPoint并没有实现+这个功能,但是依然可以实例化CMath。有什么好处呢,那就多多了,假如CMath包含几百种数学操作,如果没有这一性质,想要实例化CMath,CPoint也必须支持这些功能,比如exp2,sin,cos,但明显不需要也不可能吧,CPoint只需要简单的加减操作就行,这样,只要CPoint支持加减操作,就可以实例化自己需要的CMath模板类。
}
2.4.2.4 模板递归

先看下面一段代码,看有什么问题:

template<typename T>
class A{
private:
	A<void> a;//递归,进入死循环
}

void main(){
	A<int> a;//报错!提示使用正在定义的A<void>
}

学过递归的肯定知道,递归需要一个出口,在这里采用特化来终结递归。

template<>
class A<void>{...}
  • 利用模板递归可以在编译期间实现求和
template<int N>
class Sum{
public:
	enum
	{
		//这里用枚举类型,因为用变量不会在编译期间推导值
		value = N + Sum<N-1>::value>;
	};
};

//特化,想停在哪里就特化在哪里
template<>
class Sum<0>{
public:
	enum
	{
		value = 0;
	};
};

对了,模板递归在处理可变参数模板是很有用的,见3.6.4节。

2.5 模板实例化

宏直接就可以产生代码,而编译器遇到模板定义时,并不产生代码,只有当模板实例化后才会产生代码。
C++中模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。模板的实例化可以是隐式的(编译器生成的)或显式的(用户提供的)。

2.5.1 隐式实例化

隐式实例化应该是我们的默认选择。隐式实例化意味着编译器会自动使用提供的模板实参生成具体的函数或类。一般来说,编译器也会从函数的实参中推导出模板实参。在 C++17 中,编译器也可以推导出类模板的模板实参。部分内容与3.7节类似,可移步阅读。

2.5.2 类模板的递归实例化

我们已经知道可以使用任何类型来实例化类模板,那么由类模板实例化产生的类也可以用来实例化类模板本身,这种做法称之为类模板的递归实例化

通过这种方法可以构建空间上具有递归特性的数据结构(比如:多维数组)。

template<int N,class T>
class Array{
public:
	T& operator [](size_t i){
		return *(m_arr+i);
	}
private:
	T m_arr[N];
}

void main(){
	Array<20,int> a;
	a[6] = 7;
}

熟悉STL的小伙伴就知道了,这我们经常用啊!

vector<vector<int>> vc(20,vector<int>(10,0));

3 具体特性

3.1 typename关键字

typename关键字有两种用法:

  1. 用在模板定义中,标明其后的模板参数是类型参数,此时可与class互换。
  2. typename指出后面紧跟着的名称是一个类型,而不是成员变量

下面是STL中basic_string定义的一小段代码,正是typename的第二种用法:

class basic_string
{
public:
  typedef typename allocator_type::value_type      value_type;
  typedef typename allocator_type::pointer         pointer;
  typedef typename allocator_type::const_pointer   const_pointer;
  typedef typename allocator_type::reference       reference;
  typedef typename allocator_type::const_reference const_reference;
  typedef typename allocator_type::size_type       size_type;
  typedef typename allocator_type::difference_type difference_type;
  ...
  }

template <class T>
class allocator
{
public:
  //内置数据类型
  typedef T            value_type;
  typedef T*           pointer;
  typedef const T*     const_pointer;
  typedef T&           reference;
  typedef const T&     const_reference;
  typedef size_t       size_type;
  typedef ptrdiff_t    difference_type;
}

问题是假如不这样写会发生什么情况呢?就是下面的

  • 嵌套依赖错误
class A{
public:
	class B{
	public:
		void foo(){...}
	}
};

template<class T>
void Func(){
	T::B b;//错误,编译器并不会认为B是一个类,而是把它当成T的成员变量
	//正确的应该是:typename T::B b;
	b.foo();
}

3.2 模板的全特化与偏特化

特化是与泛化相对应的,泛化是一般化,而特化就是特殊化。对谁特殊化呢?当然是类型参数。

3.2.1 全特化

全特化:将类模板/函数模板的模板参数列表中的所有模板参数做特殊化!(说人话:将模板参数列表中所有的模板参数全部指定为确定的类型)。

//下面是全特化版本的模板类 以及 模板函数的定义
template<>//全特化标识
//全特化版本的模板类的definition
class 类名<具体参数>{
public:
    //...
}
//全特化版本的模板函数的definition
template<>//全特化标识
返回类型 函数名(具体参数){
    //...
}

同样是给出basic_string实现的例子:

template <class CharType>
class char_traits
{
  typedef CharType char_type;
  
  static size_t length(const char_type* str)
  {
    size_t len = 0;
    for (; *str != char_type(0); ++str)
      ++len;
    return len;
  }
  ...
}

//全特化版本
// Partialized. char_traits<char>
template <> 
class char_traits<char>
{
  typedef char char_type;

  static size_t length(const char_type* str) noexcept
  { return std::strlen(str); }
}  

对于编译器而言,在模板实例化过程中,由于特化版本提供了更精准的实现,因而优先考虑特化版本。

tips 特化模板类的成员函数
template<typename T, typename U>
class A {//类模板
public:
	A() {
		cout << "这是泛化版本的A类构造函数!" << endl;
	}
	void testfunc() {
		cout << "泛化版本!" << endl;
	}
	~A() {
		cout << "泛化版本的A类的析构函数!" << endl;
	}
};
//特化<char, char>版本的成员函数testfunc()
template<>
void A<char, char>::testfunc() {
		cout << "泛化版本的A类模板的特化成员函数void A<char, char>::testfunc()!" << endl;
}
int main(void) {
	A<char, char> t;//创建泛化版本的对象
	t.testfunc();
    //因为我定义了特化<char, char>版本的void A<char, char>::testfunc() 
	//所以这里编译器会优先调用特化版本的成员函数testfunc() !!!
	return 0;
}

3.2.2 偏特化

将模板中的模板参数部分指定为确定的类型。

template<typename T, typename U>
class A {//类模板
public:
	A() {
		cout << "这是泛化版本的A类构造函数!" << endl;
	}
	void testfunc() {
		cout << "泛化版本!" << endl;
	}
	~A() {
		cout << "泛化版本的A类的析构函数!" << endl;
	}
};

template<typename T>
class A<T,float> {//类模板
public:
	A() {
		cout << "这是偏特化版本的A类构造函数!" << endl;
	}
	void testfunc() {
		cout << "偏特化版本A<T,float>!" << endl;
	}
	~A() {
		cout << "偏特版本的A类的析构函数!" << endl;
	}
};
tips 更详细的介绍

这里面提到了一点,可以从模板参数范围这个角度来进行偏特化。

举例:
本来参数类型是int:
参数范围缩小:
int->const int
int->int*
int->int&
int->int&&
int->const int*
参数范围增大:
const int->int
int*->int
int&->int
int&&->int

3.2.3 注意事项

  • 函数没有偏特化,函数支持重载,没必要。
    编译器调用函数的优先级顺序是:该函数的同名重载版本>函数的模板特化版本>函数的模板泛化版本。
  • 特化的模板类的标志就是类名后加了<>。

3.3 类模板静态数据成员

类模板的静态数据成员既不是每个对象拥有一份,也不是类模板拥有一份,而是由类模板实例化出的每一个真正的类各有一份,且为该实例化类定义的所有对象共享。

类模板静态数据成员分为以下两种:

  • 依赖于模板类型参数
template<typename T>
class A {
	static T s;//依赖于模板类型参数的静态成员
}

//只能进行特化
template<>
float A<float>::s = 13.14f;

//只能进行特化
template<>
char A<char>::s = '0';

//只能进行特化
template<>
double A<double>::s = 1314520;

//只能进行特化
template<>
int A<int>::s = 1314;

void main(){
	cout<<A<float>::s<<endl;
	cout<<A<char>::s<<endl;
	cout<<A<double>::s<<endl;
	cout<<A<int>::s<<endl;
}
  • 不依赖于模板类型参数
template<typename T>
class A {
	static int s;//不依赖于模板类型参数的静态成员
}

template<typename T>
int A<T>::s = 520;//模板类的静态成员变量初始化,对任意类型的T都有一份通用的值

//对s进行特化
template<>
int A<int>::s = 1314;

void main(){
	cout<<A<float>::s<<endl;
	cout<<A<char>::s<<endl;
	cout<<A<double>::s<<endl;
	//以上输出均为520
	cout<<A<int>::s<<endl;//输出为1314
}

3.4 内部类模板

也就是在模板类中再嵌入一个模板类,比如下面的:

template<typename T1>
class Outer {
private:
	T1 t1;
public:
	Outer(T1& t1):t1(t1){}
	void print();
	template<typename T2>
	class Inner{
	private:
		T2 t2;
	public:
		Inner(T2& t2):t2(t2){}
		void print();
	}
}

template<typename T1>
void Outer<T1>::print(){
	cout<<"Outer Print:"<<t1<<endl;
}

template<typename T1>
template<typename T2>
void Outer<T1>::Inner<T2>::print(){
	cout<<"InnerPrint:"<<t2<<endl;
}

void main(){
	//外部类
	Outer<int> O(23);
	O.print();
	//内部类
	Outer<int>::Inner<float> I(6.5f);
	I.print();
}
  • 内部类和外部类之间没有任何关系,不可访问对方的类成员。
  • 在类外定义成员函数时,作用域限定符::从外到里写完整。

3.5 类模板与继承

类模板用到继承时需要注意:

  • 当普通子类继承的父类是一个类模板时,子类在声明时,要指定父类中 T 的类型,如果不指定,编译器无法给子类分配内存。
  • 如果想灵活指定父类中 T 的类型,子类也需要变成模板,但是这种情况下和普通的继承是有很大区别的,如下代码。
// 类模板与继承
template<class T>
class A{
public:
    T m;
    void funA(){...}
};

//普通子类继承类模板
class B:public A<int> {//这里要明确指定T的类型
	public:
	...
}
// 灵活指定父类中 T 类型,子类也需要变成类模板
template<class T1, class T2>
class C:public A<T2>{
public:
    C() {
        cout << typeid(T1).name() << endl;
        cout << typeid(T2).name() << endl;
    }
    T1 obj;
    //访问基类中的成员或成员函数
    funA();//错误!!!第一次编译的时候public A<T2>不是真正意义上的类,而继承以及访问基类的成员是C++类的特性。
    //编译器不会去基类中找funA的声明,类模板没有继承这个说法
    //怎么解决呢?
    A<T>::funA();//加入作用域限定符
    //或者
    this->funA();//其实这两种都是利用了编译器处理未知类型调用,第一次编译先容忍的机制。
};
 
void test(){
    C<int, char>s2;
}
 
int main(){
    test();
    system("pause");
    return 0;
}

3.6 模板形参

3.6.1 类型模板参数

顾名思义就是表示一个类型的参数,将类型参数化。

  • 类型形参由关键字classtypename后接说明符构成
template<class T> 
void h(T a){};//其中T就是一个类型形参,类型形参的名字由用户自已确定。
  • 模板形参表示的是一个未知的类型。模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型说明符或类类型说明符的使用方式完全相同,即可以用于指定返回类型,变量声明等。
  • 不能为同一个模板类型形参指定两种不同的类型。
template<class T>
void h(T a, T b){}

void main(){
	h(2, 3.2)//错误!!!因为该语句给同一模板形参T指定了两种类型
}

3.6.2 非类型模板参数

顾名思义,就是表示一个固定类型的常量而不是一个类型

  • 模板的非类型形参也就是内置类型形参,如:
template<class T, int a> 
class B{};//其中int a就是非类型的模板形参。
  • 模板的非类型形参只能是整型,指针和左值引用
//整形包括int,char,long,unsigned,bool,short等可转化为整形的类型
//像double,String, String **这样的类型是不允许的。
//但是double &,double *,对象的引用或指针是正确的。
  • 调用非类型模板形参的实参必须是一个常量表达式,即他必须能在编译时计算出结果。
  • 注意:任何局部对象,局部变量,局部对象的地址,局部变量的地址都不是一个常量表达式,都不能用作非类型模板形参的实参。全局指针类型,全局变量,全局对象也不是一个常量表达式,不能用作非类型模板形参的实参。
  • 全局变量的地址或引用,全局对象的地址或引用const类型变量是常量表达式,可以用作非类型模板形参的实参。
  • sizeof表达式的结果是一个常量表达式,也能用作非类型模板形参的实参。
  • 当模板的形参是整型时调用该模板时的实参必须是整型的,且在编译期间是常量,比如template <class T, int a> class A{};如果有int b,这时A<int, b> m;将出错,因为b不是常量,如果const int b,这时A<int, b> m;就是正确的,因为这时b是常量。
  • 非外部实体、非静态存储的变量,不能用作非类型模板形参的实参。
tips 为什么要使用非类型模板参数

1、比如说想要定义一个动态数组,即由用户指定长度的数组类,因为数组的长度需要在编译的时候就确定,这个时候就需要非类型模板参数,来指定数组的长度,在实例化这个模板数组的时候把数组的长度传进去!!!

//由用户自己亲自指定栈的大小,并实现栈的相关操作
//TemplateDemo.h

#ifndef TEMPLATE_DEMO_HXX
#define TEMPLATE_DEMO_HXX

template<class T,int MAXSIZE> 
class Stack{//MAXSIZE由用户创建对象时自行设置
    private:
        T elems[MAXSIZE];    // 包含元素的数组
        int numElems;    // 元素的当前总个数
    public:
        Stack();    //构造函数
        void push(T const&);    //压入元素
        void pop();        //弹出元素
        T top() const;    //返回栈顶元素
        bool empty() const{     // 返回栈是否为空
            return numElems == 0;
        }
        bool full() const{    // 返回栈是否已满
            return numElems == MAXSIZE;
        }
};

template <class T,int MAXSIZE> 
Stack<T,MAXSIZE>::Stack():numElems(0){     // 初始时栈不含元素
    // 不做任何事情
}

template <class T,int MAXSIZE>
void Stack<T, MAXSIZE>::push(T const& elem){
    if(numElems == MAXSIZE){
        throw std::out_of_range("Stack<>::push(): stack is full");
    }
    elems[numElems] = elem;   // 附加元素
    ++numElems;               // 增加元素的个数
}

template<class T,int MAXSIZE>
void Stack<T,MAXSIZE>::pop(){
    if (numElems <= 0) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    --numElems;               // 减少元素的个数
}

template <class T,int MAXSIZE>
T Stack<T,MAXSIZE>::top()const{
    if (numElems <= 0) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems[numElems-1];  // 返回最后一个元素
}

#endif

3.6.3 模板模板参数

模板参数的类型本身也是一个模板,就是模板模板参数

// 模板类 queue
// 参数一代表数据类型,参数二代表底层容器类型,缺省使用 mystl::deque 作为底层容器
template <class T, template <typename> class Container = mystl::deque>
class queue
{
public:
  ...
}
  • 如何标识参数是模板呢?
template <typename> class
//或者
template <class> class

另外我也看到有这样写模板模板参数的,先贴出来,表述不准确,下面的不是模板模板参数,但是实现的目的却很类似:

// 模板类 stack
// 参数一代表数据类型,参数二代表底层容器类型,缺省使用 mystl::deque 作为底层容器
template <class T, class Container = mystl::deque<T>>
class stack
{
// 使用底层容器的型别
  typedef typename Container::value_type      value_type;
  typedef typename Container::size_type       size_type;
  typedef typename Container::reference       reference;
  typedef typename Container::const_reference const_reference;

  static_assert(std::is_same<T, value_type>::value,
                "the value_type of Container should be same with T");
}

大家看,是否与下面的功能类似:

template <class T, template <typename> class Container = mystl::deque>
class stack
{...}

都给了缺省值deque,区别就是前者加了< T >,而后者没有;前者deque< T >是模板类,是一个类型,而后者是一个模板。不过还是推荐后者,前者类中需要大量的约束和判断来使得Container有效,毕竟就外人看来,它接受的就是个普通的类型参数;而后者指明了是模板模板参数。

3.6.4 可变参数模板

template<typename T>
void func(T arg){
	cout<<arg<<endl;
}

template<typename T,typename ... Types.
void func(T firstArg,Types ... args){
	cout<<sizeof...(args)<<endl;//通过sizeof...(args)可获取到变参个数
	func(firstArg);
	func(args...);
}

3.7 模板类型推导

调用C++的模板函数时,由编译器根据上下文来推断所调用的模板函数的模板参数。假设我们有下面这个模板函数:

tempalte<typename T>
void f(T& param){ // param is a reference
	cout<<typeid(T).name()<<endl;//打印类型
}
int x = 27;				// x is an int
const int cx = x;		// cx is a const int
const int& rx = x;		// rx is a reference to x as a const int

//使用类型推导,也就省的用<>了
f(x)		// T is int, param's type is int&
f(cx)		// T is const int, param's type is const int&
f(rx)		// T is const int, param's type is const int&

从这儿也就引出了下面的内容:

3.7.1 隐式推断类型实参

  • 如果函数模板的调用实参和类型形参相关。例如:
template<class T>
T max(T x,T y){...}
  • 那么在实例化函数模板时即使不显式指明函数模板的类型实参,编译器也有能力根据调用实参的类型隐式推断出正确的类型实参的类型。
max(123,456);   ->   max<int>(123,456);
  • 获得和普通调用函数一致的语法表现形式。

  • 类型推导的规律:

    • 一般情况下,param的类型是最完整的类型,继承了形参中声明的cr(const和reference)和实参总带过来的cr。但有两个特例:
      • 特例一:当形参时通用引用(T&&作为模板参数时称为通用引用)时,param根据具体的实参类型,推导为左值引用或者右值引用;
      • 特例二:当形参不是引用时,实参到形参为值传递,去除所有cr修饰符。
    • T中是否包含cr修饰符,取决于param的修饰符是否已在形参中声明过。也就是说,T中修饰符不会与形参中已声明的修饰符重复

大名鼎鼎的remove_cv用的就是上述性质:

#include <type_traits>
#include <iostream>

int main()
{
	int *p = (std::remove_cv_t<const volatile int> *)0;
	p = p;  // to quiet "unused" warning
	std::cout << "remove_cv_t<const volatile int> == "<< typeid(*p).name() << std::endl;
	//输出为:remove_cv_t<const volatile int> == int
	return (0);
}

在rb_tree的实现过程中,MyTinySTL也用了remove_cv:

 typedef typename std::remove_cv<typename T::first_type>::type key_type;

关于类型推导,下面给出几个推荐用法:

  • 想要按值传递,将模板函数参数声明为T param;
  • 想要按引用传递,但不考虑右值时,将模板函数参数声明为const T& param;
  • 想要按引用传递,但要区分左值和右值时,将模板函数声明为T&& param;

3.7.2 显式推断类型实参

额,这也是为了完整性,没什么好说的,显式推断也就是直接给出。

max<int,int>(123,456);
tips 应用场合(PS:当然是无法隐式推导的情形)

以下三种情况不能做隐式推断

// 1、调用参数和类型参数不完全相关,例如
template<class T,class D>
T max(T x,T y){...}
// 2、隐式推导不支持隐式类型转换,例如
template<class T>
T max(T x,T y){...}
max(123,45.6f)//出错
// 3、返回值类型不支持隐式推断

4、模板进阶

4.1 小技巧

4.1.1零值初始化

问题:
1、基本类型不存在缺省构造函数,未被初始化的局部变量都具有一个不确定的值。
2、类类型由于存在缺省构造函数,在未被初始化的情况下可以有一个确定的缺省初始化状态。

解决方法:
如果希望模板中,所有类型参数的变量,无论是类类型还是基本类型都以缺省方式获得初始化,就必须对其进行缺省的显示构造。

T t = T();//推荐
T t;//容易出现没有初值的未知错误

这个技巧在std::vector的实现里面也用到了,下面贴一小段代码:

template <class T>
class vector
{
public:
  // vector 的嵌套型别定义
  typedef mystl::allocator<T>                      allocator_type;
  typedef typename allocator_type::value_type      value_type;
  ...
public:
  explicit vector(size_type n)
  { fill_init(n, value_type()); }//此处用到了零值初始化技巧
}

还可以用在默认值的设定

// resize / reverse
void     resize(size_type new_size) { return resize(new_size, value_type()); }
void     resize(size_type new_size, const value_type& value);

4.1.2 initializer_list

std::initializer_list< T> 类型对象是一个访问 const T 类型对象数组的轻量代理对象。用花括号初始化列表器构造的均可视作initializer_list,可用来初始化vector、list、deque、queue、stack等。

 vector(std::initializer_list<value_type> ilist)
 {
    range_init(ilist.begin(), ilist.end());
 }
 
list(std::initializer_list<T> ilist)
{ copy_init(ilist.begin(), ilist.end()); }

deque(std::initializer_list<value_type> ilist)
{
    copy_init(ilist.begin(), ilist.end(), mystl::forward_iterator_tag());
}

vector& operator=(std::initializer_list<value_type> ilist)
{
  vector tmp(ilist.begin(), ilist.end());
  swap(tmp);//这样效率要比赋值快得多
  return *this;
}

// 与另一个 vector 交换
template <class T>
void vector<T>::swap(vector<T>& rhs) noexcept
{
  if (this != &rhs)
  {
    mystl::swap(begin_, rhs.begin_);
    mystl::swap(end_, rhs.end_);
    mystl::swap(cap_, rhs.cap_);
  }
}

4.1.3 移动构造与右值引用

先不解释,直接贴一段代码。

vector(vector&& rhs) noexcept
    :begin_(rhs.begin_),
    end_(rhs.end_),
    cap_(rhs.cap_)
{
  rhs.begin_ = nullptr;
  rhs.end_ = nullptr;
  rhs.cap_ = nullptr;
}

// 移动赋值操作符
template <class T>
vector<T>& vector<T>::operator=(vector&& rhs) noexcept
{
  destroy_and_recover(begin_, end_, cap_ - begin_);
  begin_ = rhs.begin_;
  end_ = rhs.end_;
  cap_ = rhs.cap_;
  rhs.begin_ = nullptr;
  rhs.end_ = nullptr;
  rhs.cap_ = nullptr;
  return *this;
}

4.1.4 无符号整数-1的妙用

size_type max_size() const noexcept
{ return static_cast<size_type>(-1) / sizeof(T); }

4.1.5 ::new以及void*的妙用

template <class Ty>
void construct(Ty* ptr)
{
  ::new ((void*)ptr) Ty();
}

template <class Ty1, class Ty2>
void construct(Ty1* ptr, const Ty2& value)
{
  ::new ((void*)ptr) Ty1(value);
}
  • 一度有疑问这里为什么要用::new,咱现在假设,如果你自己实现了重载了new运算符,不加::系统调用的是你自己的,而加了::系统调用最外层的,这样的代码更加稳健。
  • 再说说void*,当void*作为函数的输入和输出的时候,表示可以接受任意类型的输入指针和输出任意类型的指针。这样保证了可以灵活使用任意类型的指针,避免只能使用固定类型的指针。
  • 代码中对new的用法称为placement new,又称为定位new运算符,new后面紧跟的是地址,定位new运算符直接使用传递给它的地址,它不负责判断哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。
    再贴一段对上述construct的调用代码,以便更好的了解:
vector<T>::emplace(const_iterator pos, Args&& ...args)
{
  ...
  if (end_ != cap_ && xpos == end_)
  {
    data_allocator::construct(mystl::address_of(*end_), mystl::forward<Args>(args)...);
    ++end_;
  }
}

4.1.6 std::memmove真妙

若目标在源之前开始,则从缓冲区开始正向复制,否则从末尾反向复制,完全无重叠时回落到更高效的 std::memcpy 。
从末尾反向复制,这一点在插入元素时简直妙极!

vector<T>::emplace(const_iterator pos, Args&& ...args)
{
  ...
  else if (end_ != cap_)
  {
    auto new_end = end_;
    data_allocator::construct(mystl::address_of(*end_), *(end_ - 1));
    ++new_end;
    mystl::copy_backward(xpos, end_ - 1, end_);//内部调用的正是std::memmove
    *xpos = value_type(mystl::forward<Args>(args)...);
    end_ = new_end;
  }
  ...
}

4.1.7 运算符重载

template <class T>
bool operator==(const vector<T>& lhs, const vector<T>& rhs)
{
  return lhs.size() == rhs.size() &&
    mystl::equal(lhs.begin(), lhs.end(), rhs.begin());
}

template <class T>
bool operator<(const vector<T>& lhs, const vector<T>& rhs)
{
  return mystl::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), lhs.end());
}

/*****************************************************************************************/
// lexicographical_compare
// 以字典序排列对两个序列进行比较,当在某个位置发现第一组不相等元素时,有下列几种情况:
// (1)如果第一序列的元素较小,返回 true ,否则返回 false
// (2)如果到达 last1 而尚未到达 last2 返回 true
// (3)如果到达 last2 而尚未到达 last1 返回 false
// (4)如果同时到达 last1 和 last2 返回 false
/*****************************************************************************************/
template <class InputIter1, class InputIter2>
bool lexicographical_compare(InputIter1 first1, InputIter1 last1,
                             InputIter2 first2, InputIter2 last2)
{
  for (; first1 != last1 && first2 != last2; ++first1, ++first2)
  {
    if (*first1 < *first2)
      return true;
    if (*first2 < *first1)
      return false;
  }
  return first1 == last1 && first2 != last2;
}

// 重载版本使用函数对象 comp 代替比较操作
template <class InputIter1, class InputIter2, class Compred>
bool lexicographical_compare(InputIter1 first1, InputIter1 last1,
                             InputIter2 first2, InputIter2 last2, Compred comp)
{
  for (; first1 != last1 && first2 != last2; ++first1, ++first2)
  {
    if (comp(*first1, *first2))
      return true;
    if (comp(*first2, *first1))
      return false;
  }
  return first1 == last1 && first2 != last2;
}

// 针对 const unsigned char* 的特化版本
bool lexicographical_compare(const unsigned char* first1,
                             const unsigned char* last1,
                             const unsigned char* first2,
                             const unsigned char* last2)
{
  const auto len1 = last1 - first1;
  const auto len2 = last2 - first2;
  // 先比较相同长度的部分
  const auto result = std::memcmp(first1, first2, mystl::min(len1, len2));
  // 若相等,长度较长的比较大
  return result != 0 ? result < 0 : len1 < len2;
}

4.2 模板元编程

4.2.1 enable_if用法

enable_if可能的实现为:

template<bool B, class T = void>
struct enable_if {};
//偏特化版本 
template<class T>
struct enable_if<true, T> { typedef T type; }; 

若 B 为 true ,则 std::enable_if 拥有等同于 T 的公开成员 typedef type ;否则,无该成员 typedef 。下面给出一小段代码阐释enable_if的用法:

template <class Iter, typename std::enable_if<
    mystl::is_input_iterator<Iter>::value, int>::type = 0>
  vector(Iter first, Iter last)
  {
    MYSTL_DEBUG(!(last < first));
    range_init(first, last);
  }

用在上面的代码中,如果mystl::is_input_iterator< Iter>::value为假,则编译不通过。注意模板声明中的::type=0不能省略。

参考资料

1、C++之:模板元编程(二) 模板形参
2、Th4.6:模板全特化、偏特化(局部特化)详述
3、C++类模板
4、模板类型推导

结束语

持续更新中,水平有限,欢迎大家批评指正

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TsubasaAngel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值