简介
之前对模版的进行了初步了解和使用,可查看博客:C++ 初始模板_c++模板初始化_chihiro1122的博客-CSDN博客
其实模版除了是一类算法,或者自定义类型的 套用,还有其他功能,和其他的更高阶的使用方法。
之前在实现 各种 C++ 当中的 STL 的容器的时候用就多次用到了类,比如:套用正向迭代器模版实现的 反向迭代器的适配器;还有 queue 和 stack 容器适配器;还有仿函数的实现,都是使用了 模版来实现的:
C++ - 优先级队列(priority_queue)的介绍和模拟实现 - 反向迭代器的适配器实现_chihiro1122的博客-CSDN博客
C++ - stack 和 queue 模拟实现 -认识 deque 容器 容器适配器_chihiro1122的博客-CSDN博客
模版进阶
typename 前缀修饰问题的解决
在简介当中都是类模版,我们在这里回顾一下函数模版,函数模版不想类模版一样需要 显示实例化,直接传入参数就可以按照模版来自动进行替换:
//template<typename Container>
template<class Container>
void Print(const Container& v)
{typename Container::const_iterator it = v.begin();while (it != v.end()){cout << *it << " ";++it; // 因为只重载了 前置的++}cout << endl;
}int main()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);for (auto ch : v){cout << ch << " ";}cout << endl;Print(v);return 0;
}
上述 Print()函数不仅仅可以打印 vector 的多种参数类型,其他容器也可以打印。
但是需要注意的是,上述在取出迭代器的时候,使用的方式有些奇怪,如下所示:
typename Container::const_iterator it = v.begin();
如果前面不加 typename 前缀的话,就会报错:
我们在定义模版参数的时候,参数之前的前缀可以是 class 可以是 typename,如下所示:
template<typename Container>
template<class Container>
但是,不管是哪一种定义方式,如果是 const 迭代器的话,在迭代器类型之前,必须用 typename前缀修饰。
原因很简单:
上述如果不加 typename 的代码:
Container::const_iterator it = v.begin();
程序运行,首先进行编译实例化。
如果要寻找 const_iterator ,编译器就会去 Container 当中去寻找,但是这里寻找就会有问题,如果我们不使用函数模版,那么就不会出现问题,因为这里的 Container 直接写成 vector<int> ,直接实例化了,编译器直接在这个实例化的对象当中寻找就行了。
但是,如果使用模版,这里就有三种情况,一种是在 Container 当中寻找 const_iterator 这个动态成员变量;另一种是寻找 const_iterator 这个内部类(对象);那么到底这个 const_iterator 是一个成员变量,还是一个对象,还是一个类型,编译器搞不明白。而此时的 Container 不知道是什么。
如果 Container::const_iterator 这个表示的是一个类型,那么此处的语法是正确的;如果表示的是一个 对象,就不符合语法了。
所以,在此处,加一个 typename 表示此处的 Container::const_iterator 就是一个 类型。等实例化再去实例化的对象当中去寻找。
其实这里还有更好的方式来解决,用一个 auto就可以自动推导类型 ,就不用再使用之前一大长串 的类型名了。这里我们就可以体会到了 auto 的强大。
因为 auto 一定是类型,所以编译器就不会再去往 对象那一方面去想了。
但是不是所有使用 typename 的地方都可以用 auto,有些地方还是需要用到 typename 的,比如在 优先级对象当中就是用了 typename。如下图所示:
问题:有些编译器会 按需实例化,比如:当类当中的 某一个成员函数当中有编译错误,如果这个函数没有调用,那么编译器会略过这个错误;但是 按需实例化也是看编译器的,不同的编译器实例化程度不同。
非类型模版参数
模版当中不仅仅有需要类型模版参数的情况,可能还需要传入一些数值,比如下面这个例子,定义一个静态栈:
#define N 10template<class T>
class stack
{
public:private:T _a[N];size_t _size;
};int main()
{stack<int> st1; // 10 个stack<int> st2; // 100 个
}
此时我想定义两个静态栈,但是因为是静态的,宏 N 的大小不能改变 ,那么上述代码我们只能满足 一种情况,这就和 C 当中的 宏 一样的。
所以,C++ 当中的模版参数,还可以传入值:
template<class T, size_t N>
class stack
{
public:private:T _a[N];size_t _size;
};int main()
{stack<int, 10> st1; // 10 个stack<int, 100> st2; // 100 个
}
template<class T, size_t N>
上述模版直接用 类型来当做是模版参数的类型,这个N 就是一个非类型的模版参数,解决了上述静态栈的问题。
关于非类型模版参数,需要注意的点:
- 非类型的模板参数相等于是一个常量,他不像函数参数一样,可以进行修改;非类型的模版参数是不能进行修改的。(也就是说;非类型的模板参数必须在编译期就能确认结果。)
- 非类型模板参数的类型必须是整形。
模版的特化
函数模版的特化
模版可以实现无关类型的代码,但是一些特殊类型的代码可能会出现问题:
template<class T>
bool Less(T left, T right)
{return left < right;
}
int main()
{cout << Less(1, 2) << endl; // 可以比较,结果正确int d1 = 1;int d2 = 2;cout << Less(d1, d2) << endl; // 可以比较,结果正确Date* p1 = &d1;Date* p2 = &d2;cout << Less(p1, p2) << endl; // 可以比较,结果错误return 0;
}
最后一组,传入的是指针类型,那么 模版类型 T 就是指针,指针的比较大小是比较地址的高低,但是肯定有上述的情况下,我们想要传入指针但是不想按照指针去比较,按照传入指针 解引用的值来进行比较,但是我们不可能直接修改模版函数,如果改成解引用的话,之前我们想要实现的功能就不能实现了。
所以,上述的例子就要用到模版的特化。
上述的less ()函数,特化之后如下所示:
// Less 函数的模版
template<class T>
bool Less(T left, T right)
{return left < right;
}// Less 模版函数的特化
template<>
bool Less<int*>(int* left, int* right)
{return *left < *right;
}
如上下面一个 Less<int*> 就是Less 模版函数的函数特化。
注意:
- 模版函数的特化一定要有模版为前提,然后再去对这个函数模版进行 模版特化。
- 函数的特化不是函数的重载。
上述的特化还可以写成下述函数重载的样式(和函数模版实例化出的函数进行 函数重载):
bool Less(int* left, int* right)
{return *left < *right;
}
上述的 模版特化 和 函数重载,两种方式虽然都可以达到我们想要目的,但是,上述两种情况都只是实现了 int 类型的指针问题,不能解决多种指针的问题。
所以,聪明的你一下发现了,那么我们在实现一个模版不就行了?是这样的,看如下代码:
template<class T>
bool Less(T* left, T* right)
{return *left < *right;
}
当,传入的T 是一个指针的时候,虽然 第一种形式的模版 和 上述这个模版都可以匹配,但是,上面这个模版更加的符合,所以,如果传入的是 某类型的指针的话,就会调用上面这个模版。
而且,如果你实现上述两个模版的同时,在想上述一样实现了某一个类型的重载函数,或者像第一次那样的 实现 模版函数的 特化,那么这两个都是现成的,编译器优先调用现成的函数。
类模板的特化
类模板的特化和函数模版的特化是类似的 ,函数模版的特化是相当于是重新写了一个函数,类模板的特化也相当于是多写了一个类:
// 类模板
template<class T1, class T2>
class Data
{
public:Data() { cout << "Data<T1, T2>" << endl; }
private:T1 _d1;T2 _d2;
};// 类模板的全特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;
};void TestVector()
{Data<int, int> d1;Data<int, char> d2;
}
输出:
Data<T1, T2>
Data<int, char>
当传入的模版参数是 int 和 char 的时候,调用的就是下面定义的 特化的 模版类,然后进行特殊处理,在这个当中的特殊处理,不影响 之前定义的类模板。
运用场景,在使用仿函数的时候就可以使用,当我们想对仿函数的 类模板 当中某一个类型的实例化进行特殊操作的时候,就可以使用上述类模板的特化,来对某一种类型进行特化。
像之前在 介绍 优先级队列,当中对 less 这个仿函数的介绍,当传入的是日期类指针(Date*)的结果就不对,这时候,就可以使用 特化,给 less 类模板 特化出一个 特殊处理的类。
全特化和偏特化
向上述的 :
template<>
class Data<int, char>和 template<>
bool Less<int*>(int* left, int* right)
都属于是全特化。全特化就是把所以的模版参数都特化。
而偏特化,也叫做半特化,就是没有把全部的模版参数都特化。
如下代码所示:
// 类模板的全特化
template<>
class Data<int, char>
{
public:Data() { cout << "Data<int, char>" << endl; }
private:int _d1;char _d2;
};// 类模板的偏特化(特化部分参数)
template<class T1>
class Data<T1, char>
{
public:Data() { cout << "Data<T1, char>" << endl; }
private:T1_d1;char _d2;
};
向上述的偏特化,是对某一些模版参数进行特化;其实偏特化有两种方式:
- 一种就是向上述一样的 特化部分参数。
- 另一种就是对某一些模版参数类型的进一步限制。
// 类模板的偏特化(对一些参数进行一些限制)
template<class T1, class T2>
class Data<T1*, T2* >
{
public:Data() { cout << "Data<T1, char>" << endl; }
private:
};
向上述不对某些参数进行特化,只是判断传入的两个模版参数是不是指针,是就调用这个 特化的类。
特化在库当中也是有运用的,库当中对为了在某一模版当中找到其中调用的模版的模版参数,即用了萃取,而萃取本质上其实就是特化实现的,只不过库当中的萃取实现很麻烦。
array 静态数组
array数组和 c语言 当中的 数组,在功能上和 效率上没有任何区别,就连不能用变量来初始化个数这个特性都是一样的。而且,如果使用 array 的无参数的构造函数,里面的元素也不会进行初始化,这里和 C 当中的 数组也是一样的;
array 相比于 C当中的数组,唯一的好处就是,array 可以检查越界,而且检查非常的快,他是用assert ()断言来实现的。如果是 C语言的 数组,只是读的访问,越界是检查不出来的,写可能会检查出来;而 array 容器无论是 写还是读,都会检查出来。利用的就是 operator[] 当中的 对 下标越界的检查。
除此之外,array 容器 ,相比于数组是没有任何优点的。