C++模板进阶
C++教学总目录
C++模板进阶
- 1、模板初阶的补充
- 2、非类型模板参数
- 3、模板的特化
- 3.1、函数模板特化
- 3.2、类模板特化
- 3.2.1、全特化
- 3.2.2、偏特化
- 3.2.3、类模板特化的应用
- 4、模板的分离编译
1、模板初阶的补充
现在假设我们有一个vector对象,我们要遍历输出vector对象中的所有数据,代码如下:
void Print(const vector<int>& v)
{vector<int>::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}
但是接下来我还有一个list容器,我也要遍历输出list容器中的所有数据。并且之后可能还会有其他容器要遍历输出容器内的所有数据。基于这样的原因:我们可以直接把Print函数写成模板函数。
template<class Container>
void Print(const Container& v)
{Container::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}
但是编译之后发现报错了:
这是因为Container::const_iterator可能是类型,也可能是一个对象,如果是类型就没有问题,但是如果是一个对象就存在语法错误。
说白了这里就是因为类中可能有静态成员变量,如果是静态成员变量就是一个对象,那么就不符合语法,如果是类型就没有问题。
基于上面的原因,我们需要在Container前面加上typename告诉编译器这是一个类型,等到对象实例化的时候到类中去找const_iterator这个类型。
如下:对于vector对象和list对象都可以调用
template<class Container>
void Print(const Container& v)
{typename Container::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it;}cout << endl;
}
在优先级队列这里less的数据类型传的是Container::value_type,由于无法判断是对象还是类型,所以也需要在前面加上typename, 明确告诉编译器这里是类型。
2、非类型模板参数
在过去,我们实现一个栈的方式如下:
#define N 100
// 静态栈
template<class T>
class Stack
{
private:T _a[N];int _top;
};
有了非类型模板参数,我们可以这么写:
template<class T, size_t N>
class Stack
{
private:T _a[N];int _top;
};
有了非类型模板参数N,需要多少容量我们可以自己传递。
但是需要注意两个点:
1. 浮点数、类对象以及字符串是不允许作为非类型模板参数的。
2. 非类型的模板参数必须在编译期就能确认结果。
库里实现的有bitset(位图)后面我们会学习。这里讲一个array:
array是C++11的产物,包含于头文件<array>,类似数组,是一个固定长度的容器。需要穿类型和容器的容量N。
实际上array和数组差不多,只不过对于越界的访问更加严格。
array重载operator[]中对pos位置进行断言检查,而普通数组越界编译器可能检查不出来。
用法如下:
array<int, 10> a;
a[0] = 0;
for (auto e : a)
{cout << e << " ";
}
cout << endl;
不过基本上没啥用,很鸡肋。
3、模板的特化
3.1、函数模板特化
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,需要特殊处理。
实现一个模板函数,如果x1 < x2返回true。
template<class T>
bool Less(T x, T y)
{return x < y;
}int main()
{int a = 2, b = 1;int* p1 = &a;int* p2 = &b;cout << Less(a, b) << endl;cout << Less(p1, p2) << endl;return 0;
}
上面比较两个整形是没有问题的。但是如果比较的是两个指针呢?这样就变成了比较两个指针地址的大小了,是不符合我们需求的。这时候我们就需要使用模板的特化。
函数模板的特化步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
template<class T>
bool Less(T x, T y)
{return x < y;
}template<>
bool Less<int*>(int* x, int* y)
{return *x < *y;
}
我们自己写了一个int*的特化Less函数,对于指针的比较就会走这个函数,不会再去走模板生成了。
但是写模板的特化,我不如直接实现函数重载,如下:
bool Less(int* x, int* y)
{return *x < *y;
}
或者直接实现指针模板的Less函数:
template<class T>
bool Less(T* x, T* y)
{return *x < *y;
}
3.2、类模板特化
3.2.1、全特化
全特化就是将类模板中的所有参数都确定化,如下:
template<class T1, class T2>
class Test
{
public:Test() { cout << "Test<T1, T2>" << endl; }
private:T1 _a1;T2 _a2;
};template<>
class Test<int, double>
{
public:Test() { cout << "Test<int, double>" << endl; }
};int main()
{Test<int, char> t1;Test<int, double> t2;return 0;
}
可以看到,t2走的是我们全特化的Test类。
3.2.2、偏特化
偏特化就是特化一部分参数,如下:
template<class T1>
class Test<T1, int>
{
public:Test() { cout << "Test<T1, int>" << endl; }
};
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
例如限制Test类的两个参数为指针和引用类型:
template<class T1, class T2>
class Test<T1*, T2*>
{
public:Test() { cout << "Test<T1*, T2*>" << endl; }
};template<class T1, class T2>
class Test<T1&, T2&>
{
public:Test() { cout << "Test<T1&, T2&>" << endl; }
};
可以看下面main函数实例化出不同类型调用上面所写特化的不同类的构造函数。
3.2.3、类模板特化的应用
4、模板的分离编译
之前我们实现vector和list的时候,都是声明和定义写在一起。因为如果声明和定义分离在两个不同的文件中会出问题。
下面先给出代码:
// stack.h
#pragma once
#include <queue>namespace zzy
{template<class T, class Container = std::deque<T>>class stack{public:void push(const T& x);void pop();T& top(){return _con.back();}size_t size(){return _con.size();}bool empty(){return _con.empty();}private:Container _con;};}
// stack.cpp
#include "stack.h"namespace zzy
{template<class T, class Container>void stack<T, Container>::push(const T& x){_con.push_back(x);}template<class T, class Container>void stack<T, Container>::pop(){_con.pop_back();}
}
在main函数中调用push/pop函数,就会出现如下错误:
这是因为,找不到push/pop函数的地址。因为stack.h和stack.cpp经过预处理、编译、汇编、链接然后形成可执行程序文件,但是在链接的时候,在符号表中找不到push/pop函数的地址,因为这里是模板函数,没有实例化出具体的类,所以也就没有具体函数代码 ,所以没有地址。
解决方案有两个:
1、显示实例化模板类
但是这种方式只能针对某种类型,当我换成stack<double>还是会报错,这时候就需要继续添加stack<double>的显示实例化。因此不推荐这种方法。
2、将声明和定义写在一起或写在同一个文件中
可以像size和empty一样将声明和定义直接写在一起。
也可以声明定义分离,但是写在同一个文件中。
推荐使用第二种方式,可以发现前面的vector、list声明和定义都是写在一个文件中的。