问题:什么是泛型编程?
泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。STL以迭代器 (Iterators)和容器(Containers)为基础,是一种泛型算法(Generic Algorithms)库,容器的存在使这些算法有东西可以操作。STL包含各种泛型算法(algorithms)、泛型迭代器(iterators)、泛型容器(containers)以及函数对象(function objects)。
STL并非只是一些有用组件的集合,它是描述软件组件抽象需求条件的一个正规而有条理的架构。
模板的精神其实很简单:参数化类型。换句话说,把一个原本特定于某个类型的算法或类当中的类型信息抽掉,抽出来做成模板参数T。
事实上,noexcept被广泛地、系统地应用于C++的标准库中,用于提供标准库的性能,以及满足一些阻止异常扩散的需求。
当然,noexcept更大的作用是保证应用程序的安全。比如一个类析构函数不应该抛出异常,那么对于常被析构函数调用的delete函数来说,C++11默认将delete函数设置成noexcept,就可以提高应用程序的安全性。
void operator delete(void*) noexcept;
void operator delete[](void*) noexcept;
同样处于安全考虑,C++11标准中让类的析构函数默认也是noexcept(true)的。当然,如果程序员显式地为析构函数指定了noexcept,或者类得基类或成员有noexcept(false)的析构函数,析构函数就不会再保持默认值:
#include<iostream>
using namespace std;
struct A{
~A(){
throw 1;
}
};
struct B{
~B() noexcept(false){
throw 2;
}
};
struct C{
B b;
};
int funA(){
A a;
}
int funB(){
B b;
}
int funC(){
C c;
}
int main(){
try{
funB();
}
catch(...){
cout<<"caught funB."<<endl; //caught funB
}
try{
funC();
}
catch(...){
cout<<"caught funC."<<endl;//caught funC.
}
try{
funA();//terminate called after throwing an instance of 'int'
}
catch(...){
cout<<"caught funA."<<endl;
}
}
无论是析构函数声明为noexcept(false)的类B,还是包含了B类型成员的类C,其析构函数都是可以抛出异常的。只有什么都没有声明的类A,其析构函数被默认为noexcept(true), 从而阻止了异常的扩散。
问题:为什么一个类析构函数不应该抛出异常?(effective C 有详细解释)
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
快速初始化成员变量
C++98 对类中就地声明的要求非常高。如果静态成员不满足常量性,则不可以就地声明,而且即使常量的静态成员也只能是整型或者枚举型才能就地初始化。而非静态成员变量的初始化则必须在构造函数中进行。
class Init{
public:
Init(): a(0){}
Init(int d): a(d){}
private:
int a;
const static int b=0;
int c=1;//无法通过编译,因为非静态成员的初始化必须在构造函数中进行。
static int d=0;//无法通过编译,因为静态成员如果不满足常量性,则不可以就地声明。
static const double e=1.3;//非整数或者枚举,无法通过编译
static const char*const f="e";//非整型或者枚举,无法通过编译。
};
在C++11中,标准还允许使用等号=或者花括号{} 进行就地的非静态成员变量初始化:
struct init{
int a=1;
double b{1.2};//花括号集合
};
花括号式的集合(列表)初始化以及成为C++11中初始化声明的一种通用形式,效果类似于C++98中使用圆括号() 对自定义变量的表达式列表初始化。不过在C++中,对于非静态成员进行就地初始化,两者却并非等价的。
#include<iostream>
#include<string>
using namespace std;
struct C{
C(int i): c(i){
};
int c;
};
struct init{
int a=1;
string b("hello");
C c(1);
};
C++11标准支持了就地初始化非静态成员的同时,初始化列表这个手段也被保留下来了。如下代码同时使用了两者:
#include<iostream>
#include<string>
using namespace std;
struct Mem{
Mem(){
cout<<"Mem default, num:"<<num<<endl;
}
Mem(int i): num(i){
cout<<"Mem,num:"<<num<<endl;
}
int num=2;//使用=初始化非静态成员
};
class Group{
public:
Group(){
cout<<"Group default.val:"<<val<<endl;
}
Group(int i) : val('G'), a(i){
cout<<"Group.val:"<<val<<endl;
}
void NumofA(){
cout<<"number of A:"<<a.num<<endl;
}
void NumofB(){
cout<<"number of B:"<<b.num<<endl;
}
private:
char val{'g'}; //使用{} 初始化非静态成员
Mem a;
Mem b{19}; //使用{} 初始化非静态成员
};
int main() {
Mem member; //Mem default, num:2
Group group; //Mem default, num:2
//Mem,num:19
//Group default.val:g
group.NumofA(); //number of A:2
group.NumofB(); //number of B:19
Group group2(7); //Mem,num:7
//Mem, num:19
//Group.val:G
group2.NumofA();//number of A:7
group2.NumofB();//number of B:19
}
我的问题是:当一个类去实例化一个对象的时候,是先实例化private里面的属性还是先调用构造函数?
答:先分配内存再初始化,分配内存的时候的发现有Mem类型的成员,就先给Mem类型成员分配内存
我们定义了有两个初始化函数的类Mem, 此外还定义了包含两个Mem对象的Group类。类Mem中的成员变量num, 以及class Group中的成员变量a、b、val。就地初始化和初始化列表并不冲突。程序员可以为统一成员变量即声明就地的列表初始化,又在初始化列表中进行初始化,只不过初始化列表总是看起来"后作用于" 非静态成员。也就是说,初始化列表的效果总是优先于就地初始化。(就比如,Group group2(7);时,num 的值变成了7,而不是就地初始化的2)
相对于传统的初始化列表,在类声明中对非静态成员变量进行就地初始化可以降低程序员的工作量。我们只有多个构造函数,且有多个成员变量的时候可以看到新方式带来的便利。
#include<iostream>
#include<string>
using namespace std;
class Mem{
public: Mem(int i): m(i){}
private: int m;
};
class Group{
public:
Group(){} //这里就不需要初始化data、mem、name成员了
Group() {int a}: data(a){} //这里就不需要初始化mem、name成员了
Group() {Mem m}: mem(m){} //这里就不需要初始化data、name成员了
Group(int a, Mem m, string n): data(a), mem(m), name(n){}
private:
int data=1;
Mem mem{0};
string name{"Group"};
};
在代码中,Group 有4个构造函数。如果我们使用的是C++98的编译器,我们不得不在Group()、Group(int a), 以及Group(Mem m)这3个构造函数中将data、mem、name这3个成员都写进初始化列表。(因为这三个变量都是非静态变量,如果需要初始化,在编译器为C++98时,就必须通过构造函数进行初始化)。但是如果使用的是C++11的编译器,那么通过对非静态成员变量的就地初始化,我们就可以避免重复地在初始化列表中写上每个非静态成员了(C++98中,我们还可以通过调用公共的初始化函数来达到类似的目的,不过目前在书写的复杂性及效率性上远低于C++11改进后的做法)
对于非常量的静态成员,C++11则与C++98保持了一致。**程序员还是需要到头文件以外去定义它,这会保证编译时,类静态成员的定义最后只存在于一个目标文件中。不过对于静态常量成员,除了const关键词外,还可以使用constexpr来对静态常量成员进行声明。
非静态成员的sizeof
从 C语言开始,sizeof就是一个运算符,也是C语言中除了加减乘除以外为数不多的特殊运算符之一。而在C++引入类(class)类型之后,sizeof的定义也随之进行了拓展。不过在C++98标准中,对非静态成员变量使用sizeof是不能够通过编译的。
#include<iostream>
#include<string>
using namespace std;
struct People{
int hand;
static People*all;
};
int main(){
People p;
cout<<sizeof(p.hand)<<endl;//C++98中通过,C++11中通过 :4
cout<<sizeof(People:: all) <<endl;//C++98中通过,C++11中通过 :8
cout<<sizeof(People::hand) <<endl;//C++98中错误,C++中通过 :4
}
在最后一个sizeof操作。在C++11中,对非静态成员变量使用sizeof操作符是合法的。而在C++98中,只有静态成员,或者对象的实例才能对其成员进行sizeof操作。因此如果读者只有一个支持C++98标准的编译器,在没有定义类实例的时候,要获得类成员的大小,我们会采用如下代码:
sizeof(((People*)0)->hand);//这里我们强制转换0为一个People类的指针,继而通过指针的解引用获得其成员变量,并用sizeof求得该成员变量的大小。而在C++11中,我们无需这样的技巧,因为sizeof可以作用的表达式包括了类成员表达式。【所以,无论从代码的可读性还是编写的便利性,C++11的规则都比强制指针转换的方案更胜一筹】
扩展的friend语法
friend关键字在C++中是是一个比较特别的存在。因为我们常常会发现,一些面向对象程序语言,比如Java,就没有定义friend关键字。friend关键字用于声明类的友元,友元可以无视类中成员的属性。无论成员是public、protected或是private的,友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念。(通常情况下,面向对象程序开发的专家会建议程序员使用Get/Set接口来访问类的成员,但有的时候,friend关键字确实会让程序员少写很多代码。)C++11对friend关键字进行了一些改进,以保证其更加好用。
#include<iostream>
#include<string>
using namespace std;
class Poly;
typedef Poly P;
class LiLei{
friend class Poly; //C++98通过,C++11通过
};
class Jim{
friend Poly; //C++98 失败,C++11通过
};
class HMM{
friend P; //C++98 失败,C++11通过
};
我们声明了3个类型:LiLei、Jim和HanMeiMei,它们都有一个友元类型Poly。从编译通过与否的状况中我们可以看出,在C++11中,声明一个类为另一个类的友元时,不再需要使用class关键字。甚至使用Poly的别名P,也同样是可行的。所以,程序员可以为类模板声明友元了。
#include<iostream>
#include<string>
using namespace std;
class P;
template<typename T>class People{
friend T;
};
People<P>PP; //类型P在这里是People类型的友元
People<int> Pi //对于int类型模板参数,友元声明被忽略
对于People 这个目标类,在使用类P为模板参数时,P是People<P>的一个friend类。而在使用内置类型int作为模板参数的时候,People<int>会被实例化为一个普通的没有友元定义的类型。这样,我们可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。
#include<iostream>
#include<string>
using namespace std;
#ifdef UNIT_TEST //这里的意思是,如果定义了单元测试的宏的话,就可以在这里直接找到了
#define private public
#endif
class Defender{
public:
void Defence(int x, int y){}
void Tackle(int x,int y){}
private:
int pos_x=15;
int pos_y=0;
int speed=2;
int stamina=120;
};
class Attacker{
public:
void Move(int x, int y){}
void SpeedUp(float ratio){}
private:
int pos_x=0;
int pos_y=-30;
int speed=3;
int stamina=100;
};
#ifdef UNIT_TEST
class Validator{
public:
void Validate(int x,int y,Defender&d){}
void Validate(int x,int y,Attacker&a){}
};
int main(){
Defender d;
Attacker a;
a.Move(15,30);
d.Defence(15,30);
a.SpeedUp(1.5f);
d.Defence(15,30);
Validator v;
v.Validate(7,0,d);
v.Validate(1,-10,a);
return 0;
}
#endif
(问题:单元测试的时候,是怎么去定义宏的?)
这个例子,实际是,测试人员在一系列函数调用后,检测Attacker类变量a和Defender类变量d中的成员变量的值是否符合预期。(我想知道,为什么这里能测试到成员变量)。测试人员为了能快速写出测试程序,采用了比较危险的做法,即使用宏将private关键词统一替换为public关键词。这样一来,类中的private成员就都成了public的。
问题有:
- 如果侥幸程序中没有变量包含private字符串,该方法可以正常工作,但相反,则有可能导致严重的编译时错误;
- 降低可读性,因为改变了一个常见关键字的意义,没有注意到这个宏的程序员可能会非常迷惑程序的行为;
- 如果在类的成员定义时不指定关键字,而使用默认的private成员访问限制,那么该方法也不能达到目的;
下面用扩展的friend声明,在C++11中,我们可以将Defender和Attacker类改良一下。
#include<iostream>
#include<string>
using namespace std;
template<typename T> class DefenderT{
public:
friend T;
void Defence(int x, int y){}
void Tackle(int x,int y){}
private:
int pos_x=15;
int pos_y=0;
int speed=2;
int stamina=120;
};
template<typename T> class AttackerT{
public:
friend T;
void Move(int x, int y){}
void SpeedUp(float ratio){}
private:
int pos_x=0;
int pos_y=-30;
int speed=3;
int stamina=100;
};
using Defender=DefenderT<int>;//普通的类定义,使用int做参数
using Attacker=AttackerT<int>;
#ifdef UNIT_TEST
class Validator{
public:
void Validate(int x,int y,Defender&d){}
void Validate(int x,int y,Attacker&a){}
};
using DefenderTest=DefenderT<Validator>;//测试专用的定义,Validator类成为友元
using AttackerTest=AttackerT<Validator>;//(使用了using来定义类型的别名,这跟使用typedef的定义类型的别名是完全一样的。使用using定义类型别名是C++11中的一个新特性。
int main(){
DefenderTest d;
AttackerTest a;
a.Move(15,30);
d.Defence(15,30);
a.SpeedUp(1.5f);
d.Defence(15,30);
Validator v;
v.Validate(7,0,d);
v.Validate(1,-10,a);
return 0;
}
#endif
我们把原有的Defender和Attacker类定义为模板类DefenderT和AttackerT。而在需要进行测试的时候,我们使用Validator为模板参数,实例化出DefenderTest及AttackerTest版本的类,特点是,Validator是它们的友元,可以任意访问任何成员函数。而另外一个版本则是使用int类型进行实例化的Defender和Attacker,按照C++11的定义,它们不会有友元。(这个版本保持了良好的封装性,可以用于提供接口用于常规使用)
final/override控制
我们先回顾一下C++关于重载的概念。简单来说,一个类A中声明的虚函数fun在其派生类B中再次被定义,且B中的函数fun跟A中fun的原型一样(函数名、参数列表等一样),那么我们就称B重载(overload)了A的fun函数。对于任何B类型的变量,调用成员函数fun都是调用了B重载的版本。而如果同时有A的派生类C,却并没有重载A的fun函数,那么调用成员函数fun则会调用A中的版本。这在C++中就实现多态。
通常情况下,一旦在基类A中的成员函数fun被声明为virtual的,那么对于其派生类B而言,fun总是能够被重载的(除非被重写了)。有的时候,我们并不想fun在B类型派生类中被重载,那么,C++98没有方法对此进行限制。
#include<iostream>
#include<string>
using namespace std;
class MathObject{
public:
virtual double Arith() =0;
virtual void Print() =0;
};
class Printable:public MathObject{
public:
double Arith() =0;
void Print(){ //在C++98中我们无法阻止该结构被重写
cout<<"Output is:" <<Arith() <<endl;
}
};
class Add2:public Printable{
public:
Add2(double a,double b):x(a),y(b){
}
double Arith(){
return x+y;
}
private:
double x,y;
};
class Mul3:public Printable{
public:
Mul3(double a,double b,double c):x(a),y(b),z(c){
}
double Arith(){
return x*y*z;
}
private:
double x,y,z;
};
对于Java这种所有类型派生于单一元类型(Object)的语言来说,这种问题早就出现了。因此JAVA语言使用了final关键字来组织函数继续重写。final关键字的作用是使得派生类不可覆盖它所修饰的虚函数。C++11也采用了类似的做法。
#include<iostream>
#include<string>
using namespace std;
struct Object{
virtual void fun() =0;
};
struct Base:public Object{
void fun final;//声明为final
};
struct Derived:public Base{
void fun();//无法通过编译
};
(问:final关键字都是用于描述一个派生类的。那么基类中的虚函数是否可以使用final关键字呢?)
答案是肯定的,不过这样将使用该虚函数无法被重载,也就失去了虚函数的意义。如果不想成员函数被重载,程序员可以直接将该成员函数定义为非虚的。而final通常只在继承关系的"中途" 终止派生类的重载中有意义。从接口派生的角度而言,final可以在派生过程中任意地阻止一个接口的可重载性,这就给面向对象的程序员带来了更大的控制力。
C++中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符override,如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码无法通过编译。
#include<iostream>
#include<string>
using namespace std;
struct Base{
virtual void Turing () =0;
virtual void Dijkstra() =0;
virtual void VNeumann(int g)=0;
virtual void Dknuth() const;
void Print();
};
struct DerivedMid:public Base{
//void VNeumann(double g);
//接口被隔离了,曾想多一个版本的VNeumann函数
};
struct DerivedTop:public DerivedMid{
void Turing() override;
void Dikjstra() override;//无法通过编译,拼写错误,并非重载
void VNeumann(double g) override;//无法通过编译,参数不一致,并非重载
void DKnuth() override;//无法通过编译,常量性不一致,并非重载
void Print() override;//无法通过编译,非虚函数重载
};
因为编译器对以上三种错误都不会有任何的警示。这里override修饰符则可以保证编译器辅助地做一些检查。
模板函数的默认模板参数
在C++11中模板和函数一样,可以有默认的参数。这带来了一定的复杂性。通过简单的模板函数的例子来回顾一下函数模板的定义:
#include<iostream>
#include<string>
using namespace std;
//定义一个函数模板
template<typename T> void TempFun(T a){
cout<<a<<endl;
}
int main(){
TempFun(1); //1,(实例化为TempFun<const int>(1))
Tempfun("1"); //1,(实例化为TempFun<const char*>("1"))
}
当编译器解析到函数调用fun(1)的时候,发现fun是一个函数模板。这时候编译器就会根据实参1的类型const int推导实例化出模板函数void TempFun<const int>(int), 再进行调用。相应的,对于fun("1")来说也是类似的,不过编译器实例化出的模板函数的参数的类型将是const char*。
函数模板在C++98中与类模板一起被引入,不过在模板类声明的时候,标准允许其有默认模板参数。默认的模板参数的作用好比函数的默认形参。然而由于种种原因,C++98标准并不支持函数模板的默认模板参数。但在C++11中,这一限制已经被解除了。
#include<iostream>
#include<string>
using namespace std;
//定义一个函数模板
void DefParm(int m=3){ //c++98编译通过,c++11编译通过
}
template<typename T=int>
class DefClass{//c++98编译通过,c++11编译通过
};
template<typename T=int>
void DefTempParm(){
};//c++98编译失败,c++11编译通过
不过在语法上,与类模板有些不同的是,在为多个默认模板参数声明指定默认值的时候,程序员必须遵照"从右往左"的规则进行指定。而这个条件对于函数模板来说并不是必须的。
#include<iostream>
#include<string>
using namespace std;
//定义一个函数模板
template<typename T1, typename T2=int>class DefClass1;
template<typename T1=int, typename T2>class DefClass2;//无法通过编译
template<typename T, int i=0>class DefClass3;
template<int i=0, typename T=int>class DefClass4;//无法通过编译,但是c++11可以编译通过
template<typename T1=int, typename T2>void DefFunc1(T1 a,T2 b);
template<int i=0, typename T>void DefFunc2(T a);
可以看出,不按照从右往左定义默认类模板参数的模板类DefClass2和DefClass4都无法通过编译。而对于函数模板来说,默认模板参数的位置比较随意。可以看到DefFunc1和DefFunc2都为第一个模板参数定义了默认参数,而第二个模板参数的默认值并没有定义,C++编译器却认为没有问题。
函数模板的参数推导规则也并不复杂。简单来说,如果能够从函数实参中推导出类型的话,那么默认模板参数就不会被使用,反之,默认模板参数则可能会被使用。下面这个来自于C++11标准草案的例子:
#include<iostream>
using namespace std;
//定义一个函数模板
template<class T, class U=double> //T 和 U 为默认的模板参数
void f(T t=0, U u=0){}; // t=0 和 u=0 为默认的函数参数
void g(){
f(1,'c');//f<int,char>(1,'c')
f(1); //f<int,double>(1,0), 使用了默认参数模板double
f();//错误:T无法被推导出来
f<int>();//f<int,double>(0,0),使用了默认模板参数double
f<int,char> (); //f<int,char> (0,0)
}
int main(){
return 0;
}
我们定义了一个函数模板f, f同时使用了默认模板参数和默认函数参数。通过这个例子,可以看出,默认模板参数通常是需要跟默认函数参数一起使用的。(另外,模板函数的默认形参不是模板桉树推导的依据。函数模板参数的选择,总是由函数的实参推导而来的。
外部模板
"外部模板"是C++11中一个关于模板性能的改进。实际上,"外部"(extern)这个概念早在C的时候就有了。通常情况下,我们在一个文件中a.c中定义了一个变量 int i, 而在另外一个文件b.c中想使用它,这个时候我们就会在没有定义变量i的b.c文件中做一个外部变量的声明。比如:
extern int i;
这样的好处是,在分别编译了a.c和b.c之后,其生成的目标文件a.o和b.o中只有i这个符号的一份定义。具体的,a.o中的i是实在存在于a.o目标文件的数据区中的文件,而在b.o中,只是记录了i符号会引用其他目标文件中数据区中的名为i的数据。这样一来,在链接器(通常由编译器代为调用)将a.o和b.o链接成单个可执行文件(或者库文件)c的时候,c文件的数据区也只会有一个i的数据(供a.c和b.c的代码共享)。
(问题:为什么,如果b.c中我们声明int i的时候不加上extern的话,那么i就会实实在在地既存在于a.o的数据区中,也存在于b.o的数据区中。那么链接器在链接a.o和b.o的时候,就会报告错误,因为无法决定相同的符合是否需要合并。=》a.c和b.c是什么东西?)
而对于函数模板来说,现在我们遇到的几乎是一模一样的问题。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。
比如,我们以函数模板为例,在一个test.h的文件中声明了如下一个模板函数:
template<typename T>void fun(T){}
在第一个test1.cpp文件中,我们定义了如下代码:
#include "test.h"
void test1(){
fun(3);
}
而在另一个test2.cpp文件中,我们定义了如下代码:
#include"test.h"
void test2(){
fun(4);
}
两个源代码使用的模板函数的参数类型一致,所以在编译test1.cpp的时候,编译器实例化出了函数fun<int>(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun<int>(int).那么可以想象,在test1.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun<int>(int)代码。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun<int>(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。
在这个过程中,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。很显然,这样的工作太过冗余,而在广泛使用模板的项目中,由于编译器会产生大量冗余代码,会极大地增加编译器的编译时间和链接时间。解决这个问题的方法基本跟变量共享的思路是一样的,就是使用"外部的"模板。
显式的实例化与外部模板的声明
外部模板的使用实际依赖于C++98中一个已有的特性,即显式实例化。
template<typename T>void fun(T){}
我们只需要声明:
template void fun<int>(int);
而在C++11标准中,又加入了外部模板(Extern Template)的声明。语法上,外部模板的什么跟显式的实例化差不多,只是多了一个关键字extern。我们可以通过:
extern template void fun<int>(int);
回到原来的例子:
#include "test.h"
template void fun<int>(int); //显示地实例化
void test1(){fun(3);}
接下来,在test2.cpp中做外部模板的声明:
#include "test.h"
extern template void fun<int>(int);
void test1(){fun(3);}
这样一来,在test2.o中不会再生成fun<int>(int)的实例代码。因此链接器的工作很轻松,基本跟外部变量的做法是一样的,即只需要保证让test1.cpp和test2.cpp共享一份代码位置即可。而同时,编译器也不用每次都产生一份fun<int>(int)的代码,所以可以减少编译时间。也可以把外部模板声明放在头文件中,这样所有包含test.h的头文件就可以共享这个外部模板声明了。这一点跟使用外部变量声明是完全一致的。
需要注意的是,如果外部模板声明出现在某个编译单元中,那么与之对应的显示实例化必须出现于另一个编译单元中或者同一个编译单元的后续代码中;外部模板声明不能用于一个静态函数(即文件域函数),但可以用于类静态成员函数(这一点是显而易见的,因为静态函数没有外部链接属性,不可能在本编译单元之外出现)。
在实际上,C++11中"模板的显示实例化定义、外部模板声明和使用" 好比 "全局变量的定义、外部声明和使用"方式的再次应用。不过相比于外部变量声明,不使用外部模板声明并不会导致任何问题。外部模板定义更应该算作一种针对编译器的编译时间及空间的优化手段。
关于C++ extern 关键字
(一)extern关键字的作用
文件中定义的全局变量的可见性扩展到整个程序是在链接完成之后,而在编译阶段,他们的可见性仍局限于各自的文件。编译器的目光不够长远,编译器没有能够意识到,某个变量符号虽然不是本文件定义的,但是它可能是在其它的文件中定义的。extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件或本文件当前位置中定义,但是它是在别的文件中或本文件其它位置定义的全局变量,你要放行!”
如果一个工程现编译cpp文件,在把多个目标文件链接成为可执行文件,而两个或多个文件中,定义了相同的全局变量,那么,程序编译的时候不会报错,因为编译器单独编译每个文件,在链接可执行文件的时候,由于多个目标文件中含有相同的全局变量,而生成可执行文件的时候,任何文件中定义的全局变量对其它目标文件都是可见的,此时由于变量定义冲突而发生错误。
定义在.h文件中的函数和变量不能使用extern变量声明,原因是#include <filename>在预编译的时候将.h文件中的内容插入了cpp文件中,因此编译器找得到在其它.h文件中定义的变量或者函数。编译的时候,只编译cpp文件的内容,.h文件时不参与编译,如果使用extern声明在.h文件中定义的变量或者函数,那么声明为extern的变量和函数在其它.cpp文件中找不到,因此程序编译的时候就发生了错误。
局部和匿名类型作为模板参数
在C++98中,标准对模板实参的类型还有一些限制。具体地讲,局部的类型和匿名的类型在C++98中都不能做模板类的实参。
#include<iostream>
#include<string>
using namespace std;
//定义一个函数模板
template<typename T> class X{};
template<typename T>void TempFun(T t){
};
struct A{} a;
struct{int i;} b; //b是匿名类型变量
typedef struct{int i;} B;//B是匿名类型
void Fun(){
struct C{} c;//C是局部类型
X<A>x1;//C++98通过,C++11通过
X<B>x2;//C++98错误,C++11通过
X<C>x3;//C++98错误,C++11通过
TempFun(a); //C++98通过,C++11通过
TempFun(b); //C++98错误,C++11通过
TempFun(c); //C++98错误,C++11通过
}
我们定义了一个模板类X和一个模板函数TempFun,然后分别用普通的全局结构体、匿名的全局结构体,以及局部的结构体作为参数传给模板。可以看到,使用了局部的结构体C及变量c,以及匿名的结构体B及变量b的模板类和模板函数,在C++98标准下都无法通过编译。除此之外,匿名的联合体以及枚举类型,在C++98标准下也都是无法做模板的实参的。但是,在C++11中标准允许了以上类型做模板参数的做法,故而用支持C++11标准的编译器编译,可以通过。但是匿名类型的声明不能再模板实参的位置
template<typename T>struct MyTemplate{};
int main(){
MyTemplate<struct{int a;} >t;//无法编译通过,匿名类型的声明不能在模板实参位置
return 0;
}
可见,即使是匿名类型的声明,也需要独立的表达式语句。要使用匿名结构体作为模板参数。