一、成员函数模板的基本含义
不管是普通的类,还是类模板,都可以为其定义成员函数模板,以下的情况就是类模板和成员函数模板都有各自独立的一套参数:
template<typename T1>
class A {
public:
T1 m_ic;
static constexpr int m_stacvalue = 200;
public:
//构造函数模板
template<typename T2>
A(T2 v1, T2 v2); //这里的T2和T1没有关系
//普通成员函数模板
template<typename T3>
void myft(T3 tmpt) {
std::cout << tmpt << "\n";
}
//拷贝构造函数模板
template<typename U>
A(const A<U>& other) {
std::cout << "A::A(const A<U>& other)调用了!\n";
}
//拷贝赋值运算符模板
template<typename U>
A<T1>& operator=(const A<U>& other) {
std::cout << "operator=(const A<U>& other)调用了!\n";
return *this;
}
};
这里我们为以下的成员提供了模板:
1.赋值构造函数
2.拷贝构造函数
3.赋值运算符
4.普通成员函数
二、构造函数模板与赋值运算符模板
1.构造函数模板
1.1 赋值构造函数模板
先观察下面三个赋值构造函数(模板):
//构造函数模板
template<typename T2>
A(T2 v1, T2 v2); //这里的T2和T1没有关系
//添加一个double类型的构造函数
A(double v1, double v2) {
std::cout << "A::A(double,double)运行了!\n";
}
//添加一个T1的类型的构造函数
A(T1 v1, T1 v2) {
std::cout << "A::A(T1,T1)运行了!\n";
}
其中第一个函数我们放在类外实现,注意参数列表的声明顺序要与类内的顺序保持一致:
//类外实现类模板的构造函数模板
template<typename T1>
template<typename T2>
A<T1>::A(T2 v1, T2 v2) {
std::cout << "A::A(T2,T2)运行了!\n";
}
当我们运行:
void Test1() {
A<int> a1(1, 1); //T1为int,T2为int
a1.myft(1.0f); //T3为float
A <int> a2(1.0, 1.0); //T1为 int,T2为double
a2.myft('a'); //T3为char类型
A<float>a3(1,1); //T1为float类型,T2为int类型
a3.myft(1.0f); //T3为float;
}
可以发现调用的优先级:
一定是:
A
(
d
o
u
b
l
e
,
d
o
u
b
l
e
)
>
A
(
T
1
,
T
1
)
>
A
(
T
2
,
T
2
)
A(double,double)>A(T1,T1)>A(T2,T2)
A(double,double)>A(T1,T1)>A(T2,T2)
只有前者不存在才会是有后者的模板来实例化。
运行后得到:
结果和预期一样。
1.2拷贝构造函数模板
观察下面的拷贝构造函数模板:
//拷贝构造运算符模板
template<typename U>
A(const A<U>& other) {
std::cout << "A::A(const A<U>& other)调用了!\n";
}
注意,我们的类内没有定义拷贝构造函数,只有拷贝构造函数模板,那么当我们执行下面这行代码时,是否能成功调用拷贝构造函数模板呢?
A<int>a1(0, 0);
a1.m_ic = 1;
A<int>a2(a1); //实际上不会执行拷贝构造函数模板
std::cout << a2.m_ic << "\n"; //确实发生拷贝了
运行后发现,只调用了一开始的赋值构造函数模板,并没有调用拷贝构造函数模板。
但却神奇的发现,确实发生了拷贝,即使不存在拷贝构造函数!
实际上这里,可以理解为编译器隐式地生成一个拷贝构造函数帮助我们拷贝(更多细节请自行查找资料)。
我们补充上拷贝构造函数看看:
A(const A<T1>& other) {
std::cout << "A::A(const A<T1>& other)调用了!\n";
}
成功调用了拷贝构造运算符,因为我们没有赋值,所以得到了一个随机值:
那什么时候会调用拷贝构造函数模板呢?
当拷贝类型不一致的时候!
如下,我们运行后得到:
A<int>a1(0, 0);
a1.m_ic = 1;
A<int>a2(a1); //实际上不会执行拷贝构造函数模板
std::cout << a2.m_ic << "\n"; //确实发生拷贝了
A<float>a3(a1); //执行拷贝构造函数模板
因为没有赋值,所以还是随机值,但是确实调用了拷贝构造函数模板:
如果多试试,我们发现去除掉拷贝函数模板中的 c o n s t const const修饰符,就不存在这个限制了:
template<typename U>
A(A<U>& other) {
std::cout << "A::A(const A<U>& other)调用了!\n";
}
但是,为了编写的完整性,以及
c
o
n
s
t
const
const修饰符对编译器的影响,还是不建议去除掉这个
c
o
n
s
t
const
const修饰符,而是手动补充一个拷贝构造函数。
记住:拷贝构造函数模板永远不可能是拷贝构造函数,即使拷贝构造函数不存在!
2.赋值运算符模板
对于赋值运算符模板和拷贝构造函数模板基本一致,对于相同的类型,调用的永远是赋值运算符,如果不存在,可以看做系统会默认生成一个。
而对于不同的类型,会调用赋值运算符模板。
同样的,去除掉 c o n s t const const关键字能起到同样的作用,但不建议使用!
下面是具体的代码和运行结果:
//拷贝赋值运算符模板
template<typename U>
A<T1>& operator=(const A<U>& other) {
std::cout << "operator=(const A<U>& other)调用了!\n";
return *this;
}
#if 0
//去掉const限定
template<typename U>
A<T1>& operator=(A<U>& other) {
std::cout << "operator=( A<U>& other)调用了!\n";
return *this;
}
#endif
//拷贝运算符
A<T1>& operator=(const A<T1>& other) {
std::cout << "operator=(const A<T1>& other)调用了!\n";
return *this;
}
测试代码:
//与拷贝构造函数模板类似
a2 = a1;
std::cout << a2.m_ic << "\n"; //确实发生拷贝了
a3 = a1;
运行结果:
三、成员函数模板
首先,我们在类内实现了三个成员函数模板,分别是泛化、偏特化和全特化
//泛化版本
template<typename T3,typename T4>
void myft2(T3 tmpt1,T4 tmpt2) {
std::cout << "调用了myft2()的泛化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
//偏特化
template<typename T4>
void myft2(int tmpt1, T4 tmpt2) {
std::cout << "调用了myft2(int,T4)偏特化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
//全特化
template<>
void myft2(double tmpt1,double tmpt2) {
std::cout << "调用了myft2(double,double)全特化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
调用优先级仍然是:
普通函数
>
全特化
>
偏特化
>
泛化
普通函数>全特化>偏特化>泛化
普通函数>全特化>偏特化>泛化
值得注意的是,如果我们在类内声明,而在类外实现成员函数的时候,全特化是无法正常实现的,如下:
//类外实现类模板的构造函数模板
template<typename T1>
template<typename T2>
A<T1>::A(T2 v1, T2 v2) {
std::cout << "A::A(T2,T2)运行了!\n";
}
//泛化
template<typename T1>
template<typename T2,typename T3>
void A<T1>::myft3(T2 tmpt1, T3 tmpt2) {
std::cout << "调用了myft3()的泛化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
//偏特化
template<typename T1>
template<typename T3>
void A<T1>::myft3(int tmpt1, T3 tmpt2) {
std::cout << "调用了myft3()的偏特化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
//全特化
template<typename T1>
template<> //注意这里会失败
void A<T1>::myft3(double tmpt1, double tmpt2) {
std::cout << "调用了myft3()的全特化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
注意,全特化版本这样会编译失败:
一种解决方法是在类内写一个普通的函数作为特化版本,在类外补充的时候可以去除 t e m p l a t e < > template<> template<>,这样就可以成功编译了。
另一种解决方法是对整个类进行全特化,然后只修改成员函数部分的内容,其他内容同泛化版本,如下:
template<>
class A<double> {
public:
template<typename T4>
A(T4 v1,T4 v2) {
std::cout << "调用了A<double>::A(double,double)的构造函数\n";
}
template<typename T2,typename T3>
void myft3(T2 tmpt1, T3 tmpt2) {
std::cout << "调用了myft3()的泛化版本\n";
std::cout << tmpt1 << "\n";
std::cout << tmpt2 << "\n";
}
//剩下的和之前的函数一致,省略
//...
//...
};
调用以下代码:
void Test3() {
A<int>a(1, 1);
a.myft3(1.2, 1.2);
std::cout << std::endl;
A<double>a2(1.0, 1.0);
a2.myft3(1.0,1.0);
}
可以发现成功运行起来了:
四、虚函数
虚函数是不存在成员函数模板的,原因有很多,其中有一条重要的原因是因为虚函数在编译期间就已经被实例化出来了,并创建了虚函数表
v
t
b
l
vtbl
vtbl,因此如果要求虚函数可以模板化,那么只能有连接器来实现这个功能,显然这个要求目前是不容易实现的。
更多细节如下: