Step By Step(C++模板参数)
一、模板类静态数据成员的定义:
在下面的代码中,我们给我一个基于模板的单实例类SingletonClass,同时在该类中给出获取单实例和释放单实例的两个静态方法。这样,对于其他需要具有单实例功能的其他类直接继承该类便可同样具有了单实例的功能,该技巧可同样应用于引用计数功能。在下面的例子中,我们在模板类中声明了一个静态成员来表示单实例对象,和普通类的静态成员一样,该静态成员同样需要在外部被定义,但是其定义的规则在语法上普通类稍有不同,这一点可以在下面的示例中体现出来。
1 #include <stdio.h> 2 3 template<typename T> 4 class SingletonClass { 5 public: 6 static T* GetInstance() { 7 if (NULL == _instance) 8 _instance = new T(); 9 return _instance; 10 } 11 static void ReleaseInstance() { 12 if (NULL != _instance) { 13 delete _instance; 14 _instance = NULL; 15 } 16 } 17 private: 18 static T* _instance; 19 }; 20 21 template<typename T> 22 T* SingletonClass<T>::_instance = NULL; 23 24 class MySingletonClass : public SingletonClass<MySingletonClass> { 25 public: 26 MySingletonClass() {} 27 ~MySingletonClass() {} 28 29 public: 30 void DoPrint() { 31 printf("This is MySingletonClass.\n"); 32 } 33 }; 34 35 int main() 36 { 37 MySingletonClass* myClass = MySingletonClass::GetInstance(); 38 myClass->DoPrint(); 39 MySingletonClass::ReleaseInstance(); 40 return 0; 41 }
二、虚成员函数:
模板类的成员模板函数不能是虚函数,因为虚函数调用机制的普遍实现都使用了一个大小固定的表,每个虚函数都对应表的一个入口。然而,成员模板函数的实例化个数则需要等到整个程序都编译完毕后才能最终确定,即如果某个成员模板函数没有被任何代码调用过,那么该函数将不参与编译,因此也就不会有对应的函数入口。鉴于此,这就和虚函数表需要固定大小的限制发生了冲突。然而需要明确指出的是,模板类中的普通成员函数是可以成为虚函数的,因为普通成员函数是随同模板类一同被编译的。见如下代码示例:
1 #include <stdio.h> 2 3 template<typename T> 4 class TestClass { 5 public: 6 virtual ~TestClass() {} //析构函数是普通的成员函数,因此可以是虚函数。 7 8 public: 9 template<typename T2> 10 virtual void DoTest(T2 const& ) {} //vs2010给出的编译错误提示是: 'member function templates cannot be virtual' 11 }; 12 13 int main() 14 { 15 TestClass<int> c; 16 return 0; 17 }
三、模板参数:
C++的模板参数可以为普通类型参数、非类型参数和模板类型的模板参数。对于普通类型参数而言,我们可以将其视为typedef定义的类型名称。因此,该类型参数将不能用于某个类型的声明,如:
template<typename T>
class TestClass {
... ...
friend class T; //这样的声明是错误的。
}
对于非类型参数而言,目前C++的标准仅仅支持整型、枚举、指针类型和引用类型,如:
template<typename T, //该模板参数是普通类型参数。
typename T::InternalType* t> //非类型参数
class TestClass {
... ...
}
对于上例中的typename T::InternalType,即便也有typename关键字进行修饰,但是这里typename的函数和上一个是不同的,他用于标识InternalType是模板参数内部定义的类型,与此同时,他本是也是一个指针,因此我们需要将其视为非类型参数。对于非类型参数还需要明确说明的是,非类型参数只能是右值,即不能被取址,也不能被赋值。
对于模板的模板参数,其大多数声明和使用方式都是和普通类型参数相同的,一个明显的差别是,模板的模板参数的类型参数只能为其自身使用,而不能被其外部的模板类使用,见如下代码和说明:
template<template<typename T> class Test>
class MyTest {
static T* t; //在这里,T是模板类MyTest类型参数Test的类型参数,因此MyTest中不能直接使用。
}
最后一点是关于缺省模板参数的。一个重要的约束是,缺省参数只能依赖于template-id列表中之前声明的类型参数,而不能直接依赖当前类型参数,如:
template<typename T1, typename T2, typename T3 = DefaultT3<T1> >
class TestClass {
... ...
}
对于上述代码中的缺省模板参数DefaultT3<T1>,其类型参数只能依赖于此之间声明的T1和T2,而不能直接依赖其表示的T3。
四、模板实参:
C++编译器实例化模板参数的方式主要有3种:显示模板实参。缺省模板参数和动态推演实参。见如下示例代码:
1 template<typename T> 2 T const& max(T const& a, T const& b) { 3 return a < b ? b : a; 4 } 5 int main() { 6 max<double>(1.0,2.0); //显示指定模板参数。 7 max(1.0,2.0); //模板实参被隐式推演成double 8 max<int>(1.0,2.0); //显示指定的模板参数,会将函数函数直接转换为int。 9 }
对于动态推演方式,如果某个类型参数永远不会被推演,那么我们可以考虑将其放在模板参数列表的前面,至于可以被推演出来的类型参数,我们应该尽可能的将其放在后面,从而我们在使用时只是显示指定必须要指定的类型参数,而其余参数则可以通过推演的方式被实例化。如:
template<typename ReturnType, typename ParamType>
inline ReturnType Test(ParamType x) {
return x;
}
下面将再给出一个有关模板参数推演的实用技巧,即如果模板函数存在函数重载,那么该如何判别编译器选择的是哪个重载函数呢?
1 #include <stdio.h> 2 3 template<typename T> 4 int Test(T v) { 5 return v; 6 } 7 8 template<typename T> 9 double Test(T v, T v2) { 10 return v; 11 } 12 13 int main() { 14 //通过判断返回值的长度来确定调用的是哪个重载函数。 15 printf("This size of return value is %d.\n",sizeof(Test(0,0))); 16 printf("This size of return value is %d.\n",sizeof(Test(0))); 17 return 0; 18 }
模板的参数在实例化的过程中并不会考虑从派生类到基类的转化,见如下代码:
1 #include <stdio.h> 2 3 class Base { 4 public: 5 virtual void DoTest() { 6 printf("This is Base::DoTest()\n"); 7 } 8 }; 9 10 class Derive : public Base { 11 public: 12 void DoTest() { 13 printf("This is Derive::DoTest()\n"); 14 } 15 }; 16 17 template<typename T, T* param> 18 class TestClass { 19 public: 20 void DoTest() { 21 param->DoTest(); 22 } 23 }; 24 25 Derive d; 26 int main() 27 { 28 TestClass<Base,&d> test; //这里T的类型为Base,而第二个非类型参数的类型为Derive了。 29 test.DoTest(); 30 return 0; 31 }
在编译以上代码时,vs2010将给出的编译错误为:'specialization' : cannot convert from 'Derive *' to 'Base *'
五、友元:
关于友元本身的概念这里就不再做过多的赘述了,而只是介绍一下在模板类中的友元和普通类中友元在语法规则上的差异。
1. 如果要把模板声明的实例声明为其他类的友元时,该模板类必须已经被声明了,而普通类没有这样的限制。如:
1 template<typename T> 2 class TemplateFriendClass { 3 ... ... 4 } 5 6 template<typename T> 7 class TestTest { 8 friend class NormalFriendClass; //在此之前,普通类NormalFriendClass并没有被提前声明。 9 friend class TemplateFriendClass<T>; //模板类TemplateFriendClass在此之前必须已经被声明过了。 10 ... ... 11 }
2. 该差异是由上一条引申而来,即如果是友元模板函数,即便该模板函数已经被提前声明了,那么在定义友元时,实例化的友元函数一旦不能被推演并匹配之前声明的模板函数,编译器仍然会报错。因为友元模板函数不能在此处被定义。见如下代码:
1 template<typename T1,typename T2> 2 void TestFunc(T1,T2); 3 4 class TestClass { 5 friend void TestFunc<>(int&,int&); //OK. 该函数可以推演为与上面匹配的TestFunc。 6 friend void TestFunc<int,double>(int,double); //OK. 同样匹配上面声明的模板函数。 7 friend void TestFunc<char>(char&,int); //错误,该函数不能匹配上面声明的模板函数,这里就将表示新函数的声明了。 8 friend void TestFunc(int,int) {} //错误,此处不能进行函数的定义。 9 }
3. 模板类中声明友元函数时,应注意以下场景:
1 template<typename T> 2 class TestClass { 3 friend void TestFunc() { 4 ... ... 5 } 6 }; 7 int main() { 8 TestClass<void> t1; //此处TestClass模板类第一次被实例化,也同样产生了TestFunc函数的定义。 9 TestClass<double> t2; //基于double类型再次实例化TestClass,而也将再次实例化TestFunc函数。 10 return 0; 11 }
从上例中可以看出,在两次定义模板类变量时,TestFunc函数会跟随模板类TestClass基于不同类型的实例化而被定义两次。这将导致编译错误。下面将给出一种正确的方式。
template<typename T>
class TestClass {
friend void TestFunct(T*) {
... ...
}
};
修订后的代码中,TestFunc函数仍为普通函数,而非模板函数,只是他的函数参数类型依赖了外部类的模板参数,因此该函数可以在此处被定义。又由于函数参数的类型不同,其定义的符号也是不同的,因此即便多次实例化模板类TestFunc,友元函数TestFunc也会基于不同的类型参数来定义该函数的。
4. 如果声明友元模板,那么该模板将只是基本模板,然而在声明之后,与该基本模板想对应的特化模板和部分特化模板也将自动的被看成友元了。见如下代码示例和关键性注释:
1 #include <stdio.h> 2 3 class TestClass { 4 private: 5 static void DoPrint() { 6 printf("This is a private function of class TestClass.\n"); 7 } 8 //这里声明的友元类FriendClass是一个基本模板类。 9 template<typename T> friend class FriendClass; 10 }; 11 12 //很显然,基本模板类FriendClass中DoTest方法是可以访问TestClass中的私有方法的。 13 template<typename T> 14 class FriendClass { 15 public: 16 static void DoTest() { 17 TestClass::DoPrint(); 18 } 19 }; 20 //这里FriendClass是基本模板类FriendClass的一个特化类,因此他的DoTest函数也可以 21 //TestClass中的私有方法DoPrint()。 22 template<> 23 class FriendClass<double> { 24 public: 25 static void DoTest() { 26 TestClass::DoPrint(); 27 } 28 }; 29 30 int main() 31 { 32 FriendClass<int>::DoTest(); 33 FriendClass<double>::DoTest(); 34 return 0; 35 }