C++程序员应了解的那些事(95)C++模板使用集锦
【1】C++函数模板的隐式实例化、显式实例化与显式具体化:统称具体化
一、什么是实例化和具体化?
为进一步了解模板,必须理解术语实例化和具体化。
(1)实例化:在程序中的函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。这即是函数模板的实例化!
而函数模板实例化又分为两种类型:隐式实例化和显式实例化。
<示例>
template < typename T >
void Swap( T &a, T &b )
{
T temp;
temp = a;
a = b;
b = temp;
}
int main(void)
{
int a= 1, b = 2;
Swap(a, b);
Swap<int>(a, b);
return 0;
}
可以发现,在主函数中有两种Swap函数调用:
第一个Swap(a, b)导致编译器自动识别参数类型生成一个实例,该实例使用int类型,此为隐式实例化。
而第二个Swap<int>(a, b),直接命令编译器创建特定的int类型的函数实例,用<>符号指示类型,此为显式实例化。
(2)具体化:即显式具体化,与实例化不同的是,它也是一个模板定义,但它是对特定类型的模板定义。显式具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int &, int &);
template <> void Swap(int &, int &);//Swap<job>中的<job>是可选的,因为函数的参数类型表明,这是job的一个具体化。
可以发现,显式具体化声明在关键字template后包含<>。上面声明的意思是"不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数的定义"。这些原型必须有自己的函数定义。
在这里,有人可能要说了:”明明可以通过隐式实例化自动生成int类型的函数定义,为何还要弄出一个显式具体化来弄出另外一个模板呢?这不是多此一举吗?”我要解释一下,显式具体化的主要用途!而在介绍用途之前,我们先来了解一下普通函数模板的局限性。
二、模板的局限性
假设有如下模板函数:
template <typename T>
void fun(T a, T b)
{ ... }
通常代码假定可执行哪些操作。例如下面的代码假定定义了赋值,但是如果T为数组,这种假设将不成立!
a = b;
同样,下面的语句假设定义了<,但如果T为结构,则该假设便不成立!
if ( a > b )
另外,为数组名定义了运算符 > ,但由于数组名是常量地址,因此它比较的是数组的地址,而这并不是我们所期望的操作。下面的语句假定为类型T定义了乘法运算符,但如果T为数组、指针或结构,这种假设便不成立:
T c = a * b;
总之,编写的模板函数很可能无法处理某些类型。通常在C++中有一种解决方案是:运算符重载,以便能将其用于特定的结构或类。就是说一个类重载了运算符+之后,使用运算符+的模板便可以处理重载了运算符+的结构。
但是,还有另外一种解决方案:为特定类型提供具体化的模板定义(这就是显式具体化的主要用途)。
三、显式具体化
假定定义了如下结构:
struct job
{
char name[40];
double salary;
int floor;
}
另外,假设希望能够交换两个这种结构的内容。原来的模板使用下面的代码来完成交换:
temp = a;
a = b;
b = temp;
由于C++允许将一个结构赋给另一个结构,因此即使T是一个job结构,上述代码也可适用。然而,如果只想交换salary和floor成员,而不交换name成员,则需要使用不同的处理代码。但Swap() 的参数将保持不变(两个job结构的引用),因此无法使用模板的重载来提供其他代码(模板重载,模板的参数列表必须不同)。这时可以提供一个具体化函数定义——称为显式具体化(explicit specialization),来实现这个需求。当编译器找到与函数调用匹配的具体化定义时,将使用该定义而不再寻找模板。
具体化机制随着C++的演变而不断变化。下面介绍C++标准定义的形式。
1.第三代具体化(ISO/ANSI C++标准)
试验其他具体化方法后,C++98标准选择了下面的方法。
①对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
②显式具体化的原型和定义应以template<>开头,并通过名称来指出类型。
③具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
上面已经介绍过显式具体化的声明方式,我们直接通过代码实例来看一下:
#include <iostream>
using namespace std;
//job结构
struct job
{
char name[40];
double salary;
int floor;
};
//普通交换模板
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
//job类型的显式具体化模板
template <> void Swap<job>(job &j1, job &j2)
{
double t1;
int t2;
//交换salary
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
//交换floor
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
int main(void)
{
int inta = 1, intb = 2;
job zhangSan = {"张三", 80000, 6};
job liSi = {"李四", 60000, 4};
cout << "inta = " << inta << " inta = " << intb << endl;
cout << zhangSan.name << " , " << zhangSan.salary << " , " << zhangSan.floor <<endl;
cout << liSi.name << " , " << liSi.salary << " , " << liSi.floor <<endl;
Swap(inta, intb); //编译器将实例化普通模板的int类型函数
Swap(zhangSan, liSi); //编译器将实例化显式具体化模板job类型函数
cout << "\n交换后:\n" << endl;
cout << "inta = " << inta << " inta = " << intb << endl;
cout << zhangSan.name << " , " << zhangSan.salary << " , " << zhangSan.floor <<endl;
cout << liSi.name << " , " << liSi.salary << " , " << liSi.floor <<endl;
return 0;
}
在程序运行时匹配模板时,遵循的优先级是:具体化模板优先于常规模板,而非模板函数优先于具体化和常规模板!
实例化和具体化~小结:
为进一步了解模板,必须理解术语实例化和具体化。记住,在代码中函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。例如,上面代码中,函数调用Swap(inta, intb))导致编译器生成Swap()的一个实例,该实例使用int类型。模板并非函数定义,但使用int的模板实例是函数定义。这种实例化方式被称为隐式实例化(implict instantiation),因为编译器之所以知道需要进行定义,是由于程序调用Swap()函数时提供了int参数。
最初,编译器只能通过隐式实例化来使用模板生成函数定义。但现在C++还允许显式实例化(explici instantiation)。这意味着可以直接命令编译器创建特定的实例,如Swap<int>()。其语法是:声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:
template void Swap<int>(int , int);//explicit instantiation
实现了这种特性的编译器看到上述声明后,将使用Swap()模板生成一个使用int类型的实例。也就是说,该声明的意思是使用Swap()模板生成int类型的函数定义。
与显示实例化不同的是,显式具体化使用下面两个等价的声明之一:
template <> void Swap<int>(int &, int &);//explicit specialization
template <> void Swap(int &, int &);//explicit specialization
区别在于这些声明的意思是”不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式地定义的函数定义。“这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有!
警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。
还可通过在程序中使用函数来创建显式实例化。例如, 请看下面代码:
#include<bits/stdc++.h>
using namespace std;
template <class T>
T Add(T a, T b)
{
return a+b;
}
int main()
{
int m = 6;
double x = 10.2;
cout << Add<double>(x, m) << endl;//显式实例化
return 0;
}
☆这里的模板与函数调用Add(x, m)不匹配,因为该模板要求两个函数参数的类型相同。但通过使用Add<double>(x, m),可强制为double类型实例化,并将参数m强制转换为double类型,以便与函数Add<double>(double, double)的第二个参数匹配。
如果对Swap()做类似的处理,结果将如何呢?
int m = 5;
double x = 14.3;
Swap<double>(m, x);
这将为double生成一个显式实例化。不幸的是,这些代码不管用,因为第一个形参的类型为double &,不能指向int变量m。
【2】C++ 模板何时被实例化
A:请问c++模板被继承时会发生实例化吗
答:只谈继承肯定不会实例化,实例化发生在调用点,不掉用不会实例化,如果你定义了一个派生类对象,那被继承的模板基类也就要实例化一份了
A:这种被继承的父类已经指名具体类型的,父类也不会发生实例化吗?
template <typename T>
struct E
{
}
template DDD : E<int>
{
}
答:新城 不会的,类型又不占空间,实例化它干啥,除非被调用,如一个函数模板,或者要分配内存空间,比如定义一个占内存的对象
有类型, 没变量! 一不涉及调用,二不涉及分配内存,编译器不会主动去实例化
推荐阅读
-
程序员应了解的那些事(16)C语言中利用setjmp和longjmp做异常处理 / 不要在C++中使用setjmp和longjmp
-
C++程序员应了解的那些事(64)~ 指向 Data Member 的指针 <成员指针>
-
C++程序员应了解的那些事(68)非类型模板参数
-
C++程序员应了解的那些事(47)函数之 传入传出参数 / 默认参数
-
C++程序员应了解的那些事(36)Effective STL第6条:当心C++编译器中最烦人的分析机制 --- 调用构造函数被误认为是函数声明的问题
-
C++程序员应了解的那些事(63)STL内建函数对象、仿函数
-
C++程序员应了解的那些事(62)~ list::splice()函数详解
-
C++程序员应了解的那些事(94)之STL容器内存释放问题
-
C++程序员应了解的那些事(99)之 C++中的ODR法则
-
C++程序员应了解的那些事(18)C++11 通过key访问map容器:下标访问、at()、find、lower_bound&upper_bound、equal_range