C/C++编程:模板参数
现在存在3种模板参数:
- 类型参数
- 非类型参数
- 模板的模板参数
C++设计模板参数的用意在于:尽量将编译可知的因素提取处理,从而进一步抽象代码。无论时代码中的类型、变量地址还是函数地址,只要编译时可知,C++语言就为其一视同仁的提供模板参数支持,以最大限度的降低代码对编译器已知元素的依赖。当代码中有不确定的常数、类型、函数指针或者数据时,就可以用模板参数试着解决。
类型参数
类型参数是通过关键字typename
或者class
引入的:它们两者几乎是等同的。关键字后面必须是一个简单的标识符,后面用逗号来隔开下一个参数声明,等号=
代表接下来的缺省模板实参,一个封装的>
表示参数化子句的结束
在模板声明内部,类型参数的作用类似于typedef(类型定义)
名称。 比如,如果T是一个模板参数,就不能用class T等形式来修饰:
template<typename Allocator>
class List{
class Allocator * allocator; //错误
friend class Allocator; // 错误
};
非类型参数
非类型模板参数表示的是:在编译期或者链接期可以确定的常值。这种参数的类型(也就是这些常值的类型)必须是下面的一种:
- 整型或者枚举类型
- 指针类型(包含普通对象的指针类型、函数指针类型、指向成员的指针类型)
- 引用类型(指向对象或者指向函数的引用都是允许的)
所有其他的类型现今都不允许作为非类型参数使用(将来可能增加浮点型)
另外,在某些情况下,非模板参数的声明也可以使用关键字typename
:
template<typename T, //类型参数
typename T::Allocator*Allocator> // 非类型参数
- 类型参数:typename后面是一个简单的标识符T
- 非类型参数:typename后面是一个受限的名称
非类型模板参数的声明和变量的声明很像,但是它们不能有static、mutable等修饰符;只有具有const
和valatile
限定符。但如果这两个限定符限定的如果是最外层的参数类型,编译器将会忽略它们:
template<int const length> class Buffer; // 这里的const没用,会忽略
template<int length> class Buffer; // 和上面等同
最后,非类型模板参数只能是右值:它们不能被取值,也不能被赋值
整数模板参数
非类型模板参数的作用相当于为函数模板或者类模板预定义了一些常量,在生成模板实例时,也要求必须以常量即编译器已知的值为非可选模板参数赋值。
相对于常量,非类型模板参数的灵活之处在于:
- 模板中声明的常量,在模板的所有实例中都具有相同的值,
- 非类型模板参数则对于在不同的模板实例中可以拥有不同的值来满足不同的需求。
我们来看个例子:如果已知要用到一个长度为10的数组,此时可将数组长度定义为一个常量,代码如下:
template<typename T>
class array{
static const unsigned size = 10;
T elems[size];
public:
T& operator[](unsigned i){
throw (std::out_of_range){
// 访问元素时先进行越界检查
if(i >= size){
throw std::out_of_range("assess out of range.);
}else{
return elems[i];
}
}
}
};
但是当程序中需要多个带越界检查而且长度不同的数组时,当数组长度定义为常量便无法满足要求了。此时,一个解决方法是将长度定义为数组的成员变量,在生成数组类实例时给定并动态申请所需要的内存空间。但是如果数组的长度不变,则记录其长度的成员变量显得有些多余。而将数组长度声明为一个整数型的模板参数,则就能满足要求而且不会有额外的存储以及运行开销。如下:
#include <stdexcept>
//用整形模板参数定义数组长度
template<typename T, unsigned size>
class array{
T elems[size];
public:
T& operator[](unsigned i){
throw (std::out_of_range){
if(i >= size)
throw std::out_of_range("array assess out of range");
else
return elems[i];
}
}
};
int main(){
array<char, 20> array0;
array<char, 10> array1;
array0[10] = 'n';
try{
array1[10] = 'c';}
catch(std::out_of_range &e){
std::cerr << "access out of range.\n";
}
}
指针以及引用参数模板
- 以指针以及引用作为模板参数时,其作用与整数型模板参数类似,相当于为函数或类声明一个常量指针或引用。
- 注意,只有指向全局变量、外部变量(extern修饰)、类静态变量的指针及引用才可以作为模板参数。函数的局部变量、类成员变量等均不能作为模板参数。
这是因为模板参数值必须是编译时已知的。对于指针及引用,需要在编译时确定其所指向或者所引用的内存中的地址,而局部变量有可能被编译器分配到函数调用栈上,内存地址不固定,故而无法作为模板参数使用。
include
template
struct wrapper1{int get(){
return *p;}
void set(int v){
*p = v;}
};
template
struct wrapper2{int get(){
return p;}
void set(int v) {
p = v;}
};
int global_variable = 0;
int main() {
wrapper1<&global_variable> g1;
g1.set(1);
printf("%d\n", g1.get());
wrapper2<global_variable> g2;
g2.set(2);
printf("%d\n", g2.get());
// int local_variable; // 局部变量的指针和引用无法用于模板参数
return 0;
}
成员函数指针参数。
成员函数指针的声明方法:如果某个类some_class有多个函数都接受一个int值参数并返回一个int值结果的话,则指向这一系列成员函数的指针mfp可以定义为:
int (some_class::* mfp)(int)
上面,除了mfp是变量名外,其他部分都用于描述所指函数的各个细节。一般我们会给类型起个别名:
typedef int (some_value::* some_value_mfp)(int);
some_value_mfp mfp;
使用成员函数指针调用函数时,需要用到操作.*
和->*
,这两个操作符的优先级都很低,因此要用到()
以保证正确:
some_class a;
std::cout << (a.*mfp)(0) << "\n";
some_class* p(&a);
std::cout << (p->*mfp)(0) << "\n";
我们来看个完整的例子:
#include <iostream>
class some_value;
typedef int (some_value::* some_value_mfp)(int);
template <some_value_mfp func> //func是一个成员函数指针模板参数
int call(some_value &value, int op){
return (value.*func)(op);
}
class some_value{
int value;
public:
some_value(int _value) : value(_value) {
}
int add_by(int op){
return value += op;}
int sub_by(int op){
return value -= op;}
int mult_by(int op){
return value *= op;}
};
int main() {
some_value v0(0);
printf("%d\t", call<&some_value::add_by>(v0, 1));
printf("%d\t", call<&some_value::sub_by>(v0, 2));
printf("%d\t", call<&some_value::mult_by>(v0, 3));
return 0;
}
成员函数指针的本意在于提供一种在运行时类行为可变的多态机制。但当以成员函数指针为模板时,则将原本的动态绑定变为静态绑定,其作用相当于直接调用所绑定成员函数。如此,其目的时从调用不同成员函数的操作中提取出共性,提到代码重用率
函数指针模板参数
函数和数组类型可以被指定为非模板参数,但是要把它们先隐式的转换为指针类型,这种转型也称为decay:
template<int buf[5]> class Lexer; //buf实际上是一个int *类型
template<int* buf> class Lexer; // 正确:这是上面的重新声明
作用:相当于回调函数
#include <iostream>
template<typename T, void (*f)(T &v)>
void foreach(T array[], unsigned size){
for(unsigned int i = 0; i < size; i++){
f(array[i]);
}
}
template<typename T>
void inc(T &v){
++v;
}
template<typename T>
void dec(T &v){
--v;
}
template<typename T>
void print(T &v){
std::cout << '\t' << v;
}
int main() {
int array[] = {
1, 2, 3, 4, 5, 6, 7, 8};
using namespace std;
foreach<int, print<int>> (array, 8);
printf("\n");
foreach<int, inc<int>> (array, 8);
foreach<int, print<int>> (array, 8);
printf("\n");
foreach<int, dec<int>> (array, 8);
foreach<int, print<int>> (array, 8);
printf("\n");
return 0;
}
在同一对<>
内部,位于后面的模板参数声明可以引用前面的模板参数名称(但前面的不能引用后面的):
template<typename T,
T *root,
template<T*> class Buf>
class Structure;
模板型模板参数
引入
当函数指针作为模板时, 函数指针参数目标有两个目标参数,第一个T是数组元素类型,第二个函数指针参数标明针对每一个元素的操作。由于操作必然是针对类型为T的元素数组,所以指针所指函数必须接收一个T型引用作为参数。函数指针目标参数例子中将三种操作都包装为三个模板函数。因此,在生成模板实例时,需要先生成三个操作的函数实例,然后再作为foreach的模板参数传入,也就是说,需要这样写:
foreach<int, print<int>> (array, 8);
foreach<int, dec<int>> (array, 8);
foreach<int, inc<int>> (array, 8);
这样写比较麻烦,既然print、dec、inc都是函数模板,是否可以直接将模板传入,然后再foreach中自动生成实例呢?也就是引入了模板型模板参数(必须将三个模板函数改为函数类模板):
- —模板型模板参数是代表类模板的占位符
函数类(functor)是一种特殊的类,通过重载若干括号操作符函数使得函数类实例可以模板函数的调用
include
// Func是一个模板型函数模板,保证foreach要对每个元素进行的操作
template class Func, typename T>
void foreach(T array[], unsigned size){Func<T> func;
for(unsigned i = 0; i < size; i++){
func(array[i]);
}
}
// 三种操作都保证成函数类模板,可以通过拷贝操作符的调用
template
struct inc{void operator()(T &v) const {
v++;
}
};
template
struct dec{void operator()(T &v) const {
v--;
}
};
template
struct print{void operator()(T &v) const {
std::cout << ' ' << v;
}
};
int main(){using namespace std;
int array[] = {
1, 2, 3, 4, 5, 6, 7, 8};
foreach<print>(array, 7); // 不需要多写一遍int,在foreach自动完成
printf("\n");
foreach<inc>(array, 7);
printf("\n");
}
比较foreach<print>(array, 7);
和foreach<int, print<int>> (array, 8);
,是不是简单了很多。
- 这里是故意将foreach的参数都颠倒的,将类型模板参数T放在参数列表末尾以便自动推导。很多时候都会写成
template<typename T, template<typename TT> class Func>
- 另外,正如在声明函数时其参数名可以省略一样,作为参数的模板其参数名也可以省略,也就是说TT可以省略,即
template<typename T, template<typename > class Func>
定义
模板型模板参数:故名思意,就是模板的参数是另一个模板。其声明形式类似如下:
template<typename T, template<typename TT0, typename TT1> class A>
struct FOO(A<T, T> bar);
上面所声明的第二个模板参数A就是一个模板。
要注意参数声明中的关键字class是必须的,也就是说只有类模板可以作为模板参数。此处的class不能用struct和union代替,即:
template class C> //正确
void f(C*p); template struct C> //error
void f(C*p); template union C> //error
void f(C*p); 另外模板的模板参数的参数(比如下面的A)可以由缺省的模板实参。显然,只有在调用时没有指定该参数的情况下才会应用缺省模板实参:
template<template<typename T,
typename A = MyAllocator> class Container>
class Adaptation{
Container<int> storage; //隐式等同于Container<int, MyAllocator>
};
对于模板的模板参数而言,它的参数名称只能被自身其他参数的声明所使用。比如下面:
template class Buf>
class Lexer{static char storage[5];
Buf<char, &Lexer<Buf>::storage[0]>buf;
};
template class List>
class Node{static T* storage; // 错误
};
缺省的模板实参
目前,只有类模板声明才能有缺省模板实参(可能会变)。任何类型的模板参数都可以有一个缺省实参,只要该缺省实参能够匹配这个参数就可以。显然,缺省实参不能依赖自身的参数,但是可以依赖前面的参数
template<typename T, typename Allocator = allocator<T>> //依赖前面的参数T,不能依赖自己Allocator
class List;
与缺省的函数调用参数的约束一样:对于任一个模板参数,只有在之后的模板参数都提供了缺省实参的前提下,才能具有缺省模板实参。后面的缺省值通常是在同个模板声明中提供的,也可以在前面的模板声明中提供:
template<typename T1, typename T2, typename T3,
typename T4 = char, typename T5 = char>
class Quituple; // 正确
template<typename T1, typename T2, typename T3 = char,
typename T4 , typename T5>
class Quituple; // 正确,根据前面的模板声明,T4和T5都有缺省值了
template<typename T1 = char, typename T2, typename T3,
typename T4 , typename T5>
class Quituple; // error:T1不能具有缺省实参,因为T2还没有缺省实参
另外,缺省实参不能重复声明:
template<typename T = void>
class Value;
template<typename T = void>
class Value; // error:重复出现的缺省实参
模板声明要引入参数化子句,模板参数就是在该子句中声明的。这类声明可以把模板参数的名称省略不写(在后面不会引用该名称的前提下)
template<typename, int> // 省略不写
class X;
还没有评论,来说两句吧...