C++ Primer 第十六章 模板与范型编程
16.1 模板定义
模板和c#范型一样,建立一个通用的类或函数,其参数类型和返回类型不具体指定,用一个虚拟的类型来代表,通过模板化函数或类实现代码在的重用。
定义语法是:
template<typename 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
或 :
template<class 类型参数>
返回类型 函数名(模板形参表)
{
函数体
}
template是一个声明模板的关键字,类型参数一般用T这样的标识符来代表一个虚拟的类型,当使用函数模板时,会将类型参数具体化。typename和class关键字作用都是用来表示它们之后的参数是一个类型的参数。只不过class是早期C++版本中所使用的,后来为了不与类产生混淆,所以增加个关键字typename。
函数模板:
T Add(T x,T y)
{
return x+y;
};
int main()
{
int x=10,y=10;
std::cout<<Add(x,y)<<std::endl;//相当于调用函数int Add(int,int)
double x1=10.10,y1=10.10;
std::cout<<Add(x1,y1)<<std::endl;//相当于调用函数double Add(double,double)
long x2=9999,y2=9999;
std::cout<<Add(x2,y2)<<std::endl;//相当于调用函数long Add(long,long)
}
template内可以定义多个类型形参,每个形参用,分割并且所有类型前面都要用typename修饰。
template <typename T,typename Y> T Add(T x,Y y) ; // ok
template <typename T,Y> T Add(T x,Y y) ; // 错误,Y之前缺少修饰符
函数模板也可以声明inline 语法是 template <typename T,typename Y> inline T Add(T x,Y y) ;
类模板:
class base
{
public:
base(T a);
Y Get();
private:
T s1
T s2
};
int main()
{
base<int,string> it(1,"name"); // 类后面的类型参数不能缺省
}
和函数模板不一样,类模板无法使用类型推断,所以定义对象时一定要显示传递类型参数。
类型形参名称有自己的作用域:
template <typename T>
T Add(T x,T y)
{
typedef stirng T; // 错误,内部定义会产生名字冲突
//...
};
可以像申明一般函数或类一样声明(而不定义)。但类型形参不能省略 template <typename T,typename Y> class base ; 声明了一个类模板。
模板类型参数可以用typename 或者class 来修饰,大部分情况下二者可以互换。但有一种特殊用方法时需要typename
{
public:
class inbase{}; // 内部类
};
template <typename T>
void test()
{
typename T::inbase p; // 这时候必须要在前面加上typename,表示要定义一个类型为T类(T是类型参数)内部定义的inbase类对象
T::inbase p; // 如果不加编译会报错,因为编译器认为T::inbase表示T类的静态成员inbase,所以这样书写语法是错误的
}
要注意,这种用法需要满足条件:类型形参T必须要定义内部类inbase 否则会编译错误。
模板编程中还可以在类型形参列表中定义非类型形参,这时非类型形参会被当成常量
T Add(T x) // Add(T x,int i) 这样定义编译错误,i 和非形参i名称冲突
{
return x + i;
};
int main()
{
Add<int,10>(5);
}
范型编程有两个重要原则:形参尽量使用const引用(防止拷贝),形参本身操作尽量少(传递一个不支持函数形参体操作的类型会报错)
16.2 实例化
函数模板可以定义函数指针并予以赋值
int(*pr) (int,string) = Get ; // 定义函数指针并赋值
pr(5,"str") ; // 用函数指针调用函数无需解引,或者(*pr)(5,"str") ;
函数模板指针作为形参时需注意重载情况。对二义性的调用要指定类型来消除
template <typename T> T Get(T x) ; // 声明函数
void fun(int (*) (int));
void fun(string (*) (string));
fun(Get); // 错误,有二义性,类型推断后重载的两个fun函数都能通过。
fun(Get<int>); // 指定类型,消除了二义性
16.3 模板编译模型
[1] 当编译器看到模板定义的时候,它不立即产生代码。 只有在看到用到模板时 ,如调用了函数模板或定义了类模板的对象的时候,编译器才产生特定类型的模板实例 。
[2] 一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
[3] 模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。 当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要哪些通常放在源文件中的代码。
[4] 标准C++为编译模板代码定义了两种模型。 所有编译器都支持第一种模型,称为“包含”模型( inclusion compilation model) ;只有一些编译器支持第二种模型,“分别编译”模型( separate compilation model) 。
[5] 在两种模型中,构造程序的方式很大程度上是相同的:类定义和函数声明放在头文件中,而函数定义和成员定义放在源文件中。两种模型的不同在于,编译器怎样使用来自源文件的定义 。
[6] 在包含编译模型,编译器必须看到用到的所有模板的定义。一般而言,可以通过在声明函数模板或类模板的头文件中添加一条#include指示使定义可用,该#include引入了包含相关定义的源文件 。
[7] 在分别编译模型中,编译器会为我们跟踪相关的模板定义。但是,我们必须让编译器知道要记住给定的模板定义,可以使用export关键字来做这件事 。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化 。
[8] 在一个程序中,一个模板只能定义为导出一次。 一般我们在函数模板的定义中指明函数模板为导出的 ,这是通过在关键字template之前包含export关键字而实现的。对类模板使用export更复杂一些 ,记得应该在类的实现文件中使用export,否者如果在头文件中使用了export,则该头文件只能被程序中的一个源文件使用。
[9] 导出类的成员将自动声明为导出的。也可以将类模板的个别成员声明为导出的,在这种情况下,关键字export不在类模板本身指定,而是只在被导出的特定成员定义上指定。任意非导出成员的定义必须像在包含模型中一样对待:定义应放在定义类模板的头文件中。
16.4 类模板成员
普通类不但定义非模板函数成员,也能定义模板函数成员:
{
public:
template<typename T> T Get(T a); // 模板函数成员申明
};
template<typename T> T base::Get(T a) //成员函数类外部定义
{
return a;
}
可这样调用:
base obj ;
obj.Get<int>(20) ;
obj.Get("str") ; // 类型推断,等价于obj.Get<string>("str") ;
如果是模板类
class base
{
public:
template<typename Y> Y Get(Y a); // 模板函数成员申明
};
template<typename T> // 这一步不可少,确定T也是个模板类型参数
template<typename Y> Y base<T>::Get(Y a)
{
return a;
}
可这样调用:
base<string> obj ;
obj.Get<int>(20) ;
obj.Get("str") ; // 类型推断,等价于obj.Get<string>("str") ;
类模板或函数模板可以作为其他类的友元,不过由于其特殊性可以做一些限制。
class he
{
// ...
}
template<typename T>
class base
{
template<typename Y> friend class he; // 表示所有类型的模板类对象都是友元
friend class he<int>; // 表示只有int类型形参的模板类对象才是友元
friend class he<T>; // 表示只有类型形参和base类型参数一致的模板类对象才是友元
}
友元函数和模板类情况相似。 第一种友元可以看做是完全申明,第二种和第三种友元则需要至少在base定以前有完全申明,否则会编译错误。
16.5 一个范型句柄类
如果对上一章句柄类有充分理解范型句柄类应该非常容易掌握。
16.6 模板特化
模板的特化(template specialization)分为两类:函数模板的特化和类模板的特化。
函数模板的特化:当函数模板需要对某些类型进行特别处理,称为函数模板的特化。例如:
{
return t1 == t2;
};
int main()
{
char str1[] = "Hello";
char str2[] = "Hello";
cout << IsEqual(1, 1) << endl;
cout << IsEqual(str1, str2) << endl; //输出0
return 0;
}
最后一行比较字符串是否相等。由于对于传入的参数是char *类型的,IsEqual函数模板只是简单的比较了传入参数的值,即两个指针是否相等,因此这里打印0。显然,这与我们的初衷不符。因此,sEqual函数模板需要对char *类型进行特别处理,即特化:
{
return strcmp(t1, t2) == 0;
}
这样,当IsEqual函数的参数类型为char* 时,就会调用IsEqual特化的版本,而不会再由函数模板实例化。
类模板的特化:与函数模板类似,当类模板内需要对某些类型进行特别处理时,使用类模板的特化。例如:
class compare
{
public:
bool IsEqual(T t1, T t2)
{
return t1 == t2;
}
};
int main()
{
char str1[] = "Hello";
char str2[] = "Hello";
compare<int> c1;
compare<char *> c2;
cout << c1.IsEqual(1, 1) << endl; //比较两个int类型的参数
cout << c2.IsEqual(str1, str2) << endl; //比较两个char *类型的参数
return 0;
}
这里最后一行也是调用模板类compare<char*>的IsEqual进行两个字符串比较,显然这里存在的问题和上面函数模板中的一样,我们需要比较两个字符串的内容,而
不是仅仅比较两个字符指针。因此,需要使用类模板的特化:
{
public:
bool IsEqual(char* t1, char* t2)
{
return strcmp(t1, t2) == 0; //使用strcmp比较字符串
}
};
注意:进行类模板的特化时,需要特化所有的成员变量及成员函数。