5. 模板
简单的String模板:
template<class C> class String{
struct Srep;
Srep *rep;
public:
String();
String( const char C*);
String(const String&);
C read( int i) const;
// ...
};
template<class C>前缀说明了当前正在声明的是一个模板,它有一个将在声明中使用的类型参数 C。
引入之后,C的使用方式与其他的类型名相同,它的作用域一直延伸到由这个template<class C>作为前缀声明的结束处。
注:template<class C>只是说C是一个类型名,它不必一定是某个类的名字,也可以是基本类型,如char等。
模板是一种从相对较短的定义生成出代码的强有力方法。
模板参数:
模板可以有类型参数,也可以有常规类型的参数如int,还可以有模板参数,一个模板可以有多个参数。
如:
template<class T, T def_val> class Cont{ /*...*/};
一个模板参数可以用于定义跟随其后的模板参数。整数参数常常用于提供大小或者界限。
模板参数可以是常量表达式,具有外部链接的对象或者函数的地址,或者非重载的指向成员的指针。用作模板参数的指针必须具有&of的形式
其中of是对象或者函数的名字;或者具有f的形式,f必须是一个函数名。成员的指针必须具有&X::of的形式,of是一个成员名。
整数模板参数必须是常量:
void f(int i)
{
Buffer<int, i> bx; // 错误,需要一个常量表达式
}
函数模板:
在调用模板函数时,函数参数的类型决定到底应使用模板的那个版本,也就是说,模板的参数是由函数参数推断出来的。
模板函数必须在一些地方进行定义。
template<class T> void sort(vector<T>& v) // Shell Sort
{
const size_t n = v.size();
for( int gap = n / 2; 0 < gap; gap /= 2)
{
for( int i = gap; 0 < n; i++)
{
for( int j = i - gap; 0 <= j; j -= gap)
{
if(v[j+gap] < v[j])
{
T temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
}
}
}
函数模板的参数:
模板函数对于写出通用型算法是至关重要的,算法可以用于广泛且多样化的容器类型。对于一个调用
能够从函数的参数推断出模板参数的类型的能力是最关键的东西。
编译器能够从一个调用推断出类型参数和非类型参数,条件是由这个调用的函数参数表能够唯一标识出模板参数的一个集合。
template<class T, int i> T& lookup(Buffer<T, i>& b, const char* p);
class Record{
const char v[12];
//...
};
Record& f(Buffer<Record, 128>& buf, const char * p)
{
return lookup(buf, p);
}
从buf的类型可以推断,T为Record,i为128
注:绝对不会对类模板的参数做出任何推断。究其原因,对一个类可以提供多个构造函数,这种灵活性将会使有关的推断
在许多情况下无法完成,更多情况下是不清楚。
函数模板的重载:
可以声明多个具有同样名字的函数模板,甚至可以声明具有同一个名字的多个函数模板和常规函数的组合,当一个重载了的函数
被调用时,需要通过重载解析去找出应该调用的正确函数或模板函数。
template<class T> T sqrt(T);
template<class T> complex<T> sqrt(complex<T>);
double sqrt(double);
void f( complex<double> z)
{
sqrt(2); // sqrt<int>(int);
sqrt(2.0); // sqrt(double);
sqrt(z); // sqrt<double>(complex<double>);
}
出现函数模板时的解析规则也是对重载函数解析规则的推广。
1. 如果一个函数和一个实例化的模板函数具有同样好的匹配,那么就选用函数。
2. 找出一组能够参与这个重载解析的一组函数模板的实例化例子。
3. 如果两个模板函数都可以调用,那么选择其中更加专门的一个,在随后的解析中就只考虑这个最专门的模板函数。
4. 在这组函数上做重载解析
6. 异常处理:
错误处理:
检查到一个局部无法处理的问题时:
[1] 终止程序
对于大部分错误,我们都能够而且应该做得更好一些。特别地一个库并不知道它所嵌入其中的程序的用途和一般策略。不应该在库里面
简单地调用exit()或abort()结束掉程序。认识异常的方法是将它看作在无法局部地执行有意义的动作时,把控制权交给调用者。
[2] 返回一个表示"错误"的值
[3] 返回一个合法值,让程序员处于某种非法的状态
对于并行程序会有问题,并行下很难判断是何处出现问题。退一步很多C库都有全局错误标识,但是很难经常进行错误的检测。
[4] 调用一个预先中被好在出现"错误"的情况下用的函数
异常的结组:
一个异常也就是某个用于表示异常发生的类的一个对象。检查到一个错误的代码段throw一个对象。一个代码段用catch子句表明要处理某个异常。
一个throw的作用就是导致堆栈的一系列回退,知道找到某个适当的catch子句。
异常经常可以自然地形成一些族,这就意味着可以借助于继承来表示异常的结构,以帮助异常处理。例如:
class MathErr{};
class Overflow: public MathErr{};
class Underflow: public MathErr{};
class Zerodivide: public MathErr{};
这个时候就可以使得我们处理所有的MathErr,而不必去考虑它究竟属于哪一类。
void f()
{
try{
// ...
}
catch(Overflow){
// ...
}
catch(MathErr){
// ...
}
}
那么Overflow异常将作出特殊的处理,其他的MathErr类错误都将在MathErr子句中进行处理。
此时存在一个问题,由于捕捉到某个异常的经常是针对其基类的处理器,而不是针对这个异常本身的类的处理器。捕捉和命名异常的语义等同
于函数接受参数的语义。也就是 用实际参数的值对形式参数做初始化。也即隐含着抛出的异常可能因为捕捉而被"切割".
void f()
{
try{
g();
}
catch(MathErr m){
// ...
}
}
由于在catch时,使用抛出对象复制构造了m对象,此时,在catch中调用m的函数都是MathErr的成员,而非真正抛出对象对应的类的成员函数。
因此在这里可以使用指针或引用来避免对象的复制,从而可以避免对象被"切断"。
catch(MathErr &m)
也可以使用多继承,来使得一个异常可以被多个不同的种类的处理器处理。
重新抛出:
void f()
{
try{
g();
}
catch(MathErr m){
if(can_handle_it_completely){
return ; // 处理后直接返回
}
else
{
// 完成可以做的处理
throw; // 重新抛出异常
}
}
}
重新抛出还是将原来的异常进行抛出,而非构建新的对象。
捕获所有异常:
"捕获并重新抛出"技术的某种退化版本也非常重要,对于函数,省略号 ... 表示 "任何参数",同样catch(...)表示要 捕捉所有异常。
void m()
{
try{
//
}
catch(...)
{
// 清理工作
throw;
}
}
也即在代码中出现任何异常,处理器中的清理工作就会执行,一旦局部清理结束,这个异常被重新抛出。去激发进一步的错误处理工作。
注:在安排catch语句,也即处理器的顺序时一定注意,对于 基类异常,所有异常(...),以及派生类异常的顺序应该考虑清楚。
首先是派生类的对象,再是基类的对象,最后才是所有异常的catch语句,否则会中断异常捕捉。无法找到最匹配的处理器。
资源管理:
void use_file( const char* fn)
{
FILE * f = fopen(fn, 'r');
// ....
fclose(f);
}
一旦在fclose之前发生了异常,那么fclose就不会被调用。那么资源无法释放。
第一个修改版本
void use_file( const char* fn)
{
FILE * f = fopen(fn, 'r');
try{
}
catch(...){
fclose(f);
throw;
}
fclose(f);
}
让这个块捕获所有的异常,关闭文件并重新抛出异常。缺点是太罗嗦,而且代价高昂。
对于如下的形式,将使得代码很繁琐:
void acquire()
{
// 申请资源1
...
// 申请资源n
.
.
.
// 释放资源n
...
// 释放资源1
}
因此可以做如下的处理,使用局部对象的构造函数和析构函数实现。
class File_ptr{
FILE * p;
public:
File_ptr(const char* n, const char* a){ p = fopen(n, a)};
File_ptr(FILE *pp){p = pp;};
~File_ptr() {if(p) fclose(p);}
operator FILE*() {return p;};
};
void use_file( const char* fn)
{
File_ptr f(fn, "r");
//
}
这样的话,就不用在去考虑文件资源的释放问题。在抛出异常,"向上穿过堆栈"去为某个异常查询相应处理器的过程,也即"堆栈回退"。
在堆栈回退过程中,将会对所有构造起来的局部对象调用析构函数。
auto_ptr标准库提供了模板类auto_ptr,支持"资源申请即初始化"的技术,简言之,auto_ptr可以用指针去初始化,且能以指针同样的方式
间接访问,在auto_ptr退出作用域时,被它所指的对象将被隐式地自动删除。
为了获取这种所有权语义,auto_ptr具有与常规指针很不一样的复制语义,将一个auto_ptr复制给另外一个之后,原来的auto_ptr将不再
指向任何东西。那么const auto_ptr 就不能被复制。
异常的描述:
抛出或捕捉异常也对一个函数域其他函数的关系产生了影响,因此将爱那个可能抛出的异常集合作为函数声明的一部分也就有价值了
void f( int a) throw(x2, x3);
这说明f()只能够抛出两个异常x2和x3,以及这些类的派生的异常,但是不会抛出其他的异常。如果在其中抛出了其他的异常,这个异常将被
转化为一个对std::unexpected()的调用,默认意义为调用std::terminate(),进而调用abort()终止程序。
void f() throw(x2, x3)
{
}
等价于:
void f()
{
try
{
}
catch(x2){ throw;}
catch(x3){ throw;}
catch(...)
{
std::unexpected();// 本函数不会返回
}
}
这种带有异常描述的函数比手工书写的等价版本更短更清晰。
简单的String模板:
template<class C> class String{
struct Srep;
Srep *rep;
public:
String();
String( const char C*);
String(const String&);
C read( int i) const;
// ...
};
template<class C>前缀说明了当前正在声明的是一个模板,它有一个将在声明中使用的类型参数 C。
引入之后,C的使用方式与其他的类型名相同,它的作用域一直延伸到由这个template<class C>作为前缀声明的结束处。
注:template<class C>只是说C是一个类型名,它不必一定是某个类的名字,也可以是基本类型,如char等。
模板是一种从相对较短的定义生成出代码的强有力方法。
模板参数:
模板可以有类型参数,也可以有常规类型的参数如int,还可以有模板参数,一个模板可以有多个参数。
如:
template<class T, T def_val> class Cont{ /*...*/};
一个模板参数可以用于定义跟随其后的模板参数。整数参数常常用于提供大小或者界限。
模板参数可以是常量表达式,具有外部链接的对象或者函数的地址,或者非重载的指向成员的指针。用作模板参数的指针必须具有&of的形式
其中of是对象或者函数的名字;或者具有f的形式,f必须是一个函数名。成员的指针必须具有&X::of的形式,of是一个成员名。
整数模板参数必须是常量:
void f(int i)
{
Buffer<int, i> bx; // 错误,需要一个常量表达式
}
函数模板:
在调用模板函数时,函数参数的类型决定到底应使用模板的那个版本,也就是说,模板的参数是由函数参数推断出来的。
模板函数必须在一些地方进行定义。
template<class T> void sort(vector<T>& v) // Shell Sort
{
const size_t n = v.size();
for( int gap = n / 2; 0 < gap; gap /= 2)
{
for( int i = gap; 0 < n; i++)
{
for( int j = i - gap; 0 <= j; j -= gap)
{
if(v[j+gap] < v[j])
{
T temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
}
}
}
函数模板的参数:
模板函数对于写出通用型算法是至关重要的,算法可以用于广泛且多样化的容器类型。对于一个调用
能够从函数的参数推断出模板参数的类型的能力是最关键的东西。
编译器能够从一个调用推断出类型参数和非类型参数,条件是由这个调用的函数参数表能够唯一标识出模板参数的一个集合。
template<class T, int i> T& lookup(Buffer<T, i>& b, const char* p);
class Record{
const char v[12];
//...
};
Record& f(Buffer<Record, 128>& buf, const char * p)
{
return lookup(buf, p);
}
从buf的类型可以推断,T为Record,i为128
注:绝对不会对类模板的参数做出任何推断。究其原因,对一个类可以提供多个构造函数,这种灵活性将会使有关的推断
在许多情况下无法完成,更多情况下是不清楚。
函数模板的重载:
可以声明多个具有同样名字的函数模板,甚至可以声明具有同一个名字的多个函数模板和常规函数的组合,当一个重载了的函数
被调用时,需要通过重载解析去找出应该调用的正确函数或模板函数。
template<class T> T sqrt(T);
template<class T> complex<T> sqrt(complex<T>);
double sqrt(double);
void f( complex<double> z)
{
sqrt(2); // sqrt<int>(int);
sqrt(2.0); // sqrt(double);
sqrt(z); // sqrt<double>(complex<double>);
}
出现函数模板时的解析规则也是对重载函数解析规则的推广。
1. 如果一个函数和一个实例化的模板函数具有同样好的匹配,那么就选用函数。
2. 找出一组能够参与这个重载解析的一组函数模板的实例化例子。
3. 如果两个模板函数都可以调用,那么选择其中更加专门的一个,在随后的解析中就只考虑这个最专门的模板函数。
4. 在这组函数上做重载解析
6. 异常处理:
错误处理:
检查到一个局部无法处理的问题时:
[1] 终止程序
对于大部分错误,我们都能够而且应该做得更好一些。特别地一个库并不知道它所嵌入其中的程序的用途和一般策略。不应该在库里面
简单地调用exit()或abort()结束掉程序。认识异常的方法是将它看作在无法局部地执行有意义的动作时,把控制权交给调用者。
[2] 返回一个表示"错误"的值
[3] 返回一个合法值,让程序员处于某种非法的状态
对于并行程序会有问题,并行下很难判断是何处出现问题。退一步很多C库都有全局错误标识,但是很难经常进行错误的检测。
[4] 调用一个预先中被好在出现"错误"的情况下用的函数
异常的结组:
一个异常也就是某个用于表示异常发生的类的一个对象。检查到一个错误的代码段throw一个对象。一个代码段用catch子句表明要处理某个异常。
一个throw的作用就是导致堆栈的一系列回退,知道找到某个适当的catch子句。
异常经常可以自然地形成一些族,这就意味着可以借助于继承来表示异常的结构,以帮助异常处理。例如:
class MathErr{};
class Overflow: public MathErr{};
class Underflow: public MathErr{};
class Zerodivide: public MathErr{};
这个时候就可以使得我们处理所有的MathErr,而不必去考虑它究竟属于哪一类。
void f()
{
try{
// ...
}
catch(Overflow){
// ...
}
catch(MathErr){
// ...
}
}
那么Overflow异常将作出特殊的处理,其他的MathErr类错误都将在MathErr子句中进行处理。
此时存在一个问题,由于捕捉到某个异常的经常是针对其基类的处理器,而不是针对这个异常本身的类的处理器。捕捉和命名异常的语义等同
于函数接受参数的语义。也就是 用实际参数的值对形式参数做初始化。也即隐含着抛出的异常可能因为捕捉而被"切割".
void f()
{
try{
g();
}
catch(MathErr m){
// ...
}
}
由于在catch时,使用抛出对象复制构造了m对象,此时,在catch中调用m的函数都是MathErr的成员,而非真正抛出对象对应的类的成员函数。
因此在这里可以使用指针或引用来避免对象的复制,从而可以避免对象被"切断"。
catch(MathErr &m)
也可以使用多继承,来使得一个异常可以被多个不同的种类的处理器处理。
重新抛出:
void f()
{
try{
g();
}
catch(MathErr m){
if(can_handle_it_completely){
return ; // 处理后直接返回
}
else
{
// 完成可以做的处理
throw; // 重新抛出异常
}
}
}
重新抛出还是将原来的异常进行抛出,而非构建新的对象。
捕获所有异常:
"捕获并重新抛出"技术的某种退化版本也非常重要,对于函数,省略号 ... 表示 "任何参数",同样catch(...)表示要 捕捉所有异常。
void m()
{
try{
//
}
catch(...)
{
// 清理工作
throw;
}
}
也即在代码中出现任何异常,处理器中的清理工作就会执行,一旦局部清理结束,这个异常被重新抛出。去激发进一步的错误处理工作。
注:在安排catch语句,也即处理器的顺序时一定注意,对于 基类异常,所有异常(...),以及派生类异常的顺序应该考虑清楚。
首先是派生类的对象,再是基类的对象,最后才是所有异常的catch语句,否则会中断异常捕捉。无法找到最匹配的处理器。
资源管理:
void use_file( const char* fn)
{
FILE * f = fopen(fn, 'r');
// ....
fclose(f);
}
一旦在fclose之前发生了异常,那么fclose就不会被调用。那么资源无法释放。
第一个修改版本
void use_file( const char* fn)
{
FILE * f = fopen(fn, 'r');
try{
}
catch(...){
fclose(f);
throw;
}
fclose(f);
}
让这个块捕获所有的异常,关闭文件并重新抛出异常。缺点是太罗嗦,而且代价高昂。
对于如下的形式,将使得代码很繁琐:
void acquire()
{
// 申请资源1
...
// 申请资源n
.
.
.
// 释放资源n
...
// 释放资源1
}
因此可以做如下的处理,使用局部对象的构造函数和析构函数实现。
class File_ptr{
FILE * p;
public:
File_ptr(const char* n, const char* a){ p = fopen(n, a)};
File_ptr(FILE *pp){p = pp;};
~File_ptr() {if(p) fclose(p);}
operator FILE*() {return p;};
};
void use_file( const char* fn)
{
File_ptr f(fn, "r");
//
}
这样的话,就不用在去考虑文件资源的释放问题。在抛出异常,"向上穿过堆栈"去为某个异常查询相应处理器的过程,也即"堆栈回退"。
在堆栈回退过程中,将会对所有构造起来的局部对象调用析构函数。
auto_ptr标准库提供了模板类auto_ptr,支持"资源申请即初始化"的技术,简言之,auto_ptr可以用指针去初始化,且能以指针同样的方式
间接访问,在auto_ptr退出作用域时,被它所指的对象将被隐式地自动删除。
为了获取这种所有权语义,auto_ptr具有与常规指针很不一样的复制语义,将一个auto_ptr复制给另外一个之后,原来的auto_ptr将不再
指向任何东西。那么const auto_ptr 就不能被复制。
异常的描述:
抛出或捕捉异常也对一个函数域其他函数的关系产生了影响,因此将爱那个可能抛出的异常集合作为函数声明的一部分也就有价值了
void f( int a) throw(x2, x3);
这说明f()只能够抛出两个异常x2和x3,以及这些类的派生的异常,但是不会抛出其他的异常。如果在其中抛出了其他的异常,这个异常将被
转化为一个对std::unexpected()的调用,默认意义为调用std::terminate(),进而调用abort()终止程序。
void f() throw(x2, x3)
{
}
等价于:
void f()
{
try
{
}
catch(x2){ throw;}
catch(x3){ throw;}
catch(...)
{
std::unexpected();// 本函数不会返回
}
}
这种带有异常描述的函数比手工书写的等价版本更短更清晰。
如果函数声明中不带有任何异常描述,那么假定他可以抛出任何异常。如 int f(); 对于不抛出任何异常的函数可以用空声明如 int g() throw()
2013-01-16