C++模板【下篇】— 详解模板进阶语法及模板细节
文章目录
- 前言
- 1. 模板的特化
- 1.1 模板的全特化
- 1.2 模板的偏特化
- 2. 模板参数缺省
- 3. 非类型模板参数
- 4. typename第二重含义
- 5. 为什么不能分文件编译
- 6. 总结模板的优缺点
- 7. 扩展
- 7.1 模板的按需实例化
- 7.2 关于类模板中的继承关系
- 7.3 实现模板类的非成员函数
- 7.4 了解
前言
在 C++模板上篇主要介绍了基础的语法特性。在下篇中,主要探讨一些进阶的语法特性和一些需要注意的细节和一些扩展内容。
-
模板的特化
-
模板参数的缺省
-
非类型模板参数
-
typename
的第二重含义 -
为什么不能分文件编译
-
总结:
- 模板的优缺点
- 了解显隐式接口/编译时期的多态
- 类模板的按需实例化
- 元编程
1. 模板的特化
实际上并不是有了模板就可以放之四海皆准的。例如下面的一个例子
例1.1,我需要一个函数,比较两个数的值:
template<class T>
bool Less(const T& x, const T& y)
{return x < y;
}
看起来似乎非常美好,但是有没有考虑到一点:如果T
类型是一个指针呢?或者具体点说,是一个自定义类型(pair
类,Date
类)的指针呢?有考虑到这一点吗?
那么接下来说探讨的模板的特化就可以解决诸如此类的问题!
特化特化,我们可以理解为模板特殊的实例化,意思是给你准备好一个可能出现的类型供你使用!
特化分为:
- 全特化:类模板、函数模板
- 偏特化:类模板(函数模板不支持)
1.1 模板的全特化
- 全特化:顾名思义就是完全特化出一个版本。
用法:
- 首先,必须要先有一个基础的模板
- 关键字
template
后面跟一对空内容的<>
- 名称后面跟一对
<>
,其中标注清楚所特化的类型例如<int>
- 对于函数而言,紧接着就是函数参数列表
例1.2:
// 类模板的全特化
template<class T> // 首先需要一个基础版本
class box
{};template<>
class box<int> //全特化一个版本
{};// 函数模板的全特化
template<class T>
bool Less(T& x, T& y)
{return x < y;
}template<>
bool Less<int*>(int*& x, int*& y)
{return *x < *y;
}
需要注意的是:
- 函数模板而言,参数类型一定要匹配。
<const int*>
<int*>
<int&>
<const int&>
的参数列表都是有差异的,了解一下,可以自己验证。否则会报一些奇奇怪怪的错误。
当然对于全特化而言,我们显然可想到以下方案
假如需要一个指针的内容进行比较,例1.3:
//方案一:全特化
template<class T>
bool Less(T x, T y)
{return x < y;
}template<>
bool Less<const int*>(const int* x, const int* y)
{return *x < *y;
}//方案二:模板指明指针
template<class T>
bool Less(const T* x, const T* y)
{return *x < *y;
}//方案三:不用模板,直接写
bool Less(const int* x, const int* y)
{return *x < *y;
}
在实际的运用中,根据自己的需要,可以创建不同的类模板。
1.2 模板的偏特化
函数模板不支持偏特化。就是对部分模板参数进行特化。
下面有一个Data
类:
template<class T1, class T2>
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
- 第一种偏特化 – 只特化部分参数
- 第二种偏特化 – 限制类型
例1.4:
template<class T1, class T2> //需要基础版本
class Data
{
public:
Data() {cout<<"Data<T1, T2>" <<endl;}
private:
T1 _d1;
T2 _d2;
};template<class T1> //第一种偏特化 -- 第二种参数类型给出了int
class Data<T1, int>
{
public:
Data() {cout<<"Data<T1, int>" <<endl;}
private:
T1 _d1;
T2 _d2;
};template<class T1, class T2> //第二种偏特化 -- 只能用指针实例化
class Data<T1*, T2*>
{
public:
Data() {cout<<"Data<T1*, T2*>" <<endl;}
private:
T1 _d1;
T2 _d2;
};
2. 模板参数缺省
- 注意模板参数是可以像函数一样给缺省值的!!
道理和函数参数的缺省几乎一模一样。
比如说:STL库,就是有模板参数缺省
这里就不再过多介绍了
3. 非类型模板参数
模板除了可以给类型的参数以外,还可以给非类型的参数。但是仅仅局限于整型。
例如3.1:
template<class T, size_t N = 100> //给缺省值
class Array //大写和库做区分
{
private:T _a[N];
};
主要特点如下:
- N是一个常量,不可修改。
- 只能是一个整型。
运用例子:在STL的一个容器array中:
建议
将与非类型模板参数的代码抽离模板
这是什么意思呢?考虑下面的场景
我现在要使用一个正方形:
template<class T, size_t N>
class square
{
public:square(T val = T()):_s(N, vector<T>(N, val)){}void Print() const{cout << N << endl;}
private:vector<vector<T>> _s;
};int main()
{square<int, 10> s1;s1.Print();square<int, 5> s2;s2.Print();return 0;
}
仅仅由于edge
的不同而生产了两份类,代码就变得很冗长了,所以我们需要尽可能的将类似于这样的代码冗长。我们可以将这个整型参数作为构造函数的参数传入,就不会发生类似的情况了。
这个问题涉及了模板导致代码膨胀的问题,详情请看Effective C++
4. typename第二重含义
typename
除了可以声明模板参数以外,还可以声明一个类中的类型。
考虑以下情景,我现在需要打印一个容器内的数据,采用迭代器。
例4.1:
template<class Container>
void Print(const Container& con)
{Container::const_itreator it = con.begin();// 打印内容// ....
}
对于例4.1来说,这个Container::const_itreator
是否是有点熟悉?好像是一个类中的静态成员变量也可以这么访问吧?Container
是一个类类型,那么这么访问好像就是可以把const_iterator
当作是一个静态成员。编译器就无法成功识别了,因为像上面那么写就是一个语法错误。所以我们应该这样写:
例4.2:
template<class Container>
void Print(const Container& con)
{typename Container::const_itreator it = con.begin();// 打印内容// ....
}
typename
就是告诉编译器,这是一个类型,不要把它当作是一个变量了!!
其实,这个用法还是很常见的,例如上面的priority_queue
:
5. 为什么不能分文件编译
在前篇我们提到了,编译器根据传入的参数类型,在编译阶段给我们实例化出一个符合类型的模板函数或者模板类。那么当我们分文件编译的时候,就要好好想想我们的编译链接过程了(建议看:简要的编译链接),结合下图来理解:
-
一句话概括:当分离过后,另一个实现的文件不能确定其类型,从而无法实例化
-
解决方案
- 将声明和定义放在一个文件(.hpp) – 推荐
- 在模板定义的位置显示实例化 – 不推荐
6. 总结模板的优缺点
-
优点
- 模板复用了代码,节省了资源,更快高效地完成开发。 – STL
- 增强了代码的灵活性。 – 适配器和仿函数
-
缺点
- 模板会导致代码膨胀问题,导致编译时间过长。 — 建议看《Effective C++》有一些解决方案!
- 出现模板编译报错,错误信息不方便排除。
7. 扩展
7.1 模板的按需实例化
对于类模板的实例化来说,它并不是你实例化就可以得到一个完整的类。来看下面一个场景。
例7.1:
#include<iostream>
using namespace std;
template<class T>
class A
{
public:void func1(){cout << "func1()" << endl;}void func2(){func1(10); //故意的错误调用}
};int main()
{A<int> v;v.func1();//v.func2();return 0;
}
运行上面代码可以发现两点:
- 如果不调用
v.func2();
那么上面可以通过编译,因为func2
没有被实例化出来。 - 如果调用
v.func2();
那么上面代码不可以通过编译,因为func1
没有支持int
类型的一个参数!
- 当我们去掉模板参数的时候,就会发现,编译器直接提示编译错误。这也就间接说明了:类模板是支持按需实例化的!!!
7.2 关于类模板中的继承关系
类模板中的类也是可以继承的。
例7.2:
template<class T>
class A
{
public:void Print(){T a;cout << "type:" << typeid(a).name()<< endl;// typeid(a)创建一个匿名对象,其中的name()函数获取a这个类型的字符串!}
};template<class T>
class B : public A<T>
{
public:void Func(){Print();}
};int main()
{BC<int> b;b.Func();
}
很可惜,这个代码是无法通过编译的……
这里小编直接告诉大家原因:
- 编译器在
B<int>
类中找不到名为Print
的函数……我的B
类继承了A
类,在继承的时候我们希望是A<int>
类型,这是很好的。不过,编译器并不能为A<int>
这个类实例化出Print
这个函数。也就是说类模板中,模板化的子类“不会进入模板化的基类观察”。
有哪些办法可以解决这个问题呢?
- 方法一:
this
指针显示使用Print
template<class T>
class B : public A<T>
{using A<int>::Print;
public:void Func(){this->Print();}
};
- 方法二:显示类域调用函数
template<class T>
class B : public A<T>
{using A<int>::Print;
public:void Func(){A<int>::Print();}
};
- 方式三:利用using展开类域
template<class T>
class B : public A<T>
{using A<int>::Print;
public:void Func(){Print();}
};
这些方法都是显示告诉编译器:“我现在要在模板化的父类中调用一个名为Print
的函数,你快快给我实例化一个来”。关于这点,在《Effective C++》这本书中有比较详细的介绍,在条款43
7.3 实现模板类的非成员函数
在《Effective C++》中的条款46谈到了这点。在C++模板上篇我们谈到了函数模板不能完成隐式类型转换,同时我们也谈到了模板不支持声明和定义分离!所以这里建议,当想要实现一个支持隐式类型转换并且支持混合运算的模板类的非成员函数的时候,建议友元+类中定义
例如7.3:
- 版本一:
template<class T>
class Rational //分式
{
public:Rational(const T& numerator = 0, const T& denominator = 1): _numerator(numerator), _denominator(denominator){}const T& getnumerator(){return _numerator;}const T& getdenominator(){return _denominator;}private:T _numerator; //分子T _denominator; //分母
};template<class T>
const Rational<T>& operator*(const Rational<T>& r1, const Rational<T>& r2)
{//…… 分式的计算
}int main()
{Rational<int> r(10, 11);//Rational<int> ret1 = r * 2;//Rational<int> ret2 = 2 * r;
}
编译错误 – 失败的原因:
模板函数被定义在全局域中,传参的时候,编译器无法根据类型实例化出一份对应的operator*
。函数模板不支持隐式类型转换,无法根据下面2
和r1
推导出对应的模板函数来。
- 版本二 – 单纯声明友元:
template<class T>
class Rational
{friend const Rational<T>& operator*(const Rational<T>& r1, const Rational<T>& r2); //声明友元
public:Rational(const T& numerator = 0, const T& denominator = 1): _numerator(numerator), _denominator(denominator){}const T& getnumerator(){}const T& getdenominator(){}private:T _numerator;T _denominator;
};template<class T>
const Rational<T>& operator*(const Rational<T>& r1, const Rational<T>& r2)
{//…… 分式的计算
}int main()
{Rational<int> r(10, 11);Rational<int> ret1 = r * 2;Rational<int> ret2 = 2 * r;
}
链接错误 – 失败原因:
模板并不支持声明与定义分开,即使编译器已经知道了你要使用operator*
这个函数了,但是编译器在编译阶段并不会真正实例化出来这个函数体。也就是这个函数仅仅有一个声明而没有定义,在链接阶段当然找不到对应的地址!
- 版本三 – 友元 + 实现:
template<class T>
class Rational
{friend const Rational<T>& operator*(const Rational<T>& r1, const Rational<T>& r2) //声明友元{//…… 分式的计算}
public:Rational(const T& numerator = 0, const T& denominator = 1): _numerator(numerator), _denominator(denominator){}const T& getnumerator(){}const T& getdenominator(){}private:T _numerator;T _denominator;
};
int main()
{Rational<int> r(10, 11);Rational<int> ret1 = r * 2;Rational<int> ret2 = 2 * r;
}
终于正确,但是上面代码并不完整,operator*
需要一个返回值。同时也记住,友元函数不受访问限定符的限制!
在这里感谢:Scott Meyers老师举出的例子!
7.4 了解
我觉得对于C++的模板来说,还可以了解的是:
-
隐式接口和显示接口。推荐书籍:Scott Meyers所著的《Effective C++》其中的条款41。
-
元编程(高阶)。推荐文章
完