1. 从私有子对象中产生的异常
几部分来,我一直展示了一些技巧来捕获从对象的构造函数中抛出的异常。这些技巧是在异常从构造函数中漏出来后处理它们。有时,调用者需要知道这些异常,但通常(如我所采用的例程中)异常是从调用者并不关心的私有子对象中爆发的。使得用户要关心“不可见”的对象表明了设计的脆弱。
在历史上,(可能抛异常)的构造函数的实现者没有简单而健壮的解决方法。看这个简单的例子:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(new char[count])
{
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
buffer的构造函数接受字符数目并从自由空间分配内存,然后初始化buffer::p指向它。如果分配失败,构造函数中的new语句产生一个异常,而buffer的用户(这里是main函数)必须捕获它。
1.1 try块
不幸的是,捕获这个异常不是件容易事。因为抛出来自buffer::buffer,所有buffer的构造函数的调用应该被包在try块中。没脑子的解决方法:
try
{
buffer b(count);
}
catch (...)
{
abort();
}
do_something_with(b); // ERROR. At this point,
// 'b' no longer exists
是不行的。do_something_with()的调用必须在try块中:
try
{
buffer b(100);
do_something_with(b);
}
catch (...)
{
abort();
}
//do_something_with(b);
(免得被说闲话:我知道调用abort()来处理这个异常有些过份。我只是用它做个示例,因为现在关心的是捕获异常而不是处理它。)
虽然有些笨拙,但这个方法是有效的。接着考虑这样的变化:
static buffer b(100);
int main()
{
// buffer b(100);
do_something_with(b);
return 0;
}
现在,b被定义为全局对象。试图将它包入try块
try // um, no, I don't think so
{
static buffer b;
}
catch (...)
{
abort();
}
int main()
{
do_something_with(b);
return 0;
}
将不能被编译。
1.2 暴露实现
每个例子都显示了buffer设计上的基本缺陷:buffer的接口以外的实现细节被暴露了。在这里,暴露的细节是buffer的构造函数中的new语句可能失败。这个语句用于初始化私有子对象buffer::p――一个main函数和其它用户不能操作甚至根本不知道的子对象。当然,这些用户更不应该被要求必须关注这样的子对象抛出的异常。
为了改善buffer的设计,我们必须在构造函数中捕获异常:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(NULL)
{
try
{
p = new char[count];
}
catch (...)
{
abort();
}
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
异常被包含在构造函数中。用户,比如main()函数,从不知道异常存在过,世界又一次清静了。
1.3 常量成员
也这么做?注意,buffer::p一旦被设置过就不能再被改动。为避免指针被无意改动,谨慎的设计是将它申明为const:
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char * const p;
};
很好,但到了这步时:
buffer::buffer(size_t const count)
{
try
{
p = new char[count]; // ERROR
}
catch (...)
{
abort();
}
}
一旦被初始化,常量成员不能再被改变,即使是在包含它们的对象的构造函数体中。常量成员只能被构造函数的成员初始化列表设置一次。
buffer::buffer(size_t const count)
: p(new char[count]) // OK
这让我们回到了段落一中,又重新产生了我们最初想解决的问题。
OK,这么样如何:不用new语句初始化p,换成用内部使用new的辅助函数来初始化它:
char *new_chars(size_t const count)
{
try
{
return new char[count];
}
catch (...)
{
abort();
}
}
buffer::buffer(int const count)
: p(new_chars(count))
{
// try
// {
// p = new char[count]; // ERROR
// }
// catch (...)
// {
// abort();
// }
}
这个能工作,但代价是一个额外函数却仅仅用来保护一个几乎从不发生的事件。
1.4 函数try块
(WQ注:后面会讲到,function try块不能阻止构造函数的抛异常动作,它其实只起异常过滤的功能!!!见P14.3)
我在上面这些建议中没有发现哪个能确实令人满意。我所期望的是一个语言级的解决方案来处理部分构造子对象问题,而又不引起上面说到的问题。幸运的是,语言中恰好包含了这样一个解决方法。
在深思熟虑后,C++标准委员会增加了一个叫做“function try blocks”的东西到语言规范中。作为try块的堂兄弟,函数try块捕获整个函数定义中的异常,包括成员初始化列表。不用奇怪,因为语言最初没有被设计了支持函数try块,所以语法有些怪:
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch
{
abort();
}
看起来想是通常的try块后面的{}实际上是划分构造函数的函数体的。在效果上,{}有双重作用,不然,我们将面对更别扭的东西:
buffer::buffer(int const count)
try
: p(new char[count])
{
{
}
}
catch
{
abort();
}
(注意:虽然嵌套的{}是多余的,这个版本能够编译。实际上,你可以嵌套任意重{},直到遇到编译器的极限。)
如果在初始化列表中有多个初始化,我们必须将它们放入同一个函数try块中:
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
和普通的try块一样,可以有任意个异常处理函数:
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
catch (int)
{
// ...
}
catch (...)
{
// ...
}
古怪的语法之外,函数try块解决了我们最初的问题:所有从buffer子对象的构造函数抛出的异常留在了buffer的构造函数中。
因为我们现在期望buffer的构造函数不抛出任何异常,我们应该给它一个异常规格申明:
explicit buffer(size_t) throw();
接着一想,我们应该是个更好点的程序员,于是给我们所有函数加了异常规格申明:
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
// ...
};
// ...
static void do_something_with(buffer &) throw()
// ...
Rounding Third and Heading for Home
对我们的例子,最终版本是:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
private:
char *const p;
};
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch (...)
{
abort();
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &) throw()
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
用Visual C++编译,自鸣得意地坐下来,看着IDE的提示输出。
syntax error : missing ';' before 'try'
syntax error : missing ';' before 'try'
'count' : undeclared identifier
'<Unknown>' : function-style initializer appears
to be a function definition
syntax error : missing ';' before 'catch'
syntax error : missing ';' before '{'
missing function header (old-style formal list?)
噢!
Visual C++还不支持函数try块。在我测试过的编译器中,只有Edison Design Group C++ Front End version 2.42认为这些代码合法。
(顺便提一下,我特别关心为什么编译将第一个错误重复了一下。可能它的计算你第一次会不相信。)
如果你坚持使用Visual C++,你可以使用在介绍函数try块前所说的解决方法。我喜欢使用额外的new封装函数。如果你认同,考虑将它做成模板:
template <typename T>
T *new_array(size_t const count)
{
try
{
return new T[count];
}
catch (...)
{
abort();
}
}
// ...
buffer::buffer(size_t const count)
: p(new_array<char>(count))
{
}
这个模板比原来的new_chars函数通用得多,对char以外的类型也有能工作。同时,它有一个隐蔽的异常相关问题,而我将在下次谈到。