分享
 
 
 

C与C++中的异常处理14

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

1. 模板安全

上次,我开始讨论异常安全。这次,我将探究模板安全。

模板根据参数的类型进行实例化。因为通常事先不知道其具体类型,所以也无法确切知道将在哪儿产生异常。你大概最期望的就是去发现可能在哪儿抛异常。这样的行为很具挑战性。

看一下这个简单的模板类:

template <typename T>

class wrapper

{

public:

wrapper()

{

}

T get()

{

return value_;

}

void set(T const &value)

{

value_ = value;

}

private:

T value_;

wrapper(wrapper const &);

wrapper &operator=(wrapper const &);

};

如名所示,wrapper包容了一个T类型的对象。方法get()和set()得到和改变私有的包容对象value_。两个常用方法--拷贝构造函数和赋值运算符没有使用,所以没有定义,而第三个--析构函数由编译器隐含定义。

实例化的过程很简单,例如:

wrapper<int> i;

包容了一个int。i的定义过程导致编译器从模板实例化了一个定义为wrapper<int>的类:

template <>

class wrapper<int>

{

public:

wrapper()

{

}

int get()

{

return value_;

}

void set(int const &value)

{

value_ = value;

}

private:

int value_;

wrapper(wrapper const &);

wrapper &operator=(wrapper const &);

};

因为wrapper<int>只接受int或其引用(一个内嵌类型或内嵌类型的引用),所以不会触及异常。wrapper<int>不抛异常,也没有直接或间接调用任何可能抛异常的函数。我不进行正规的分析了,但相信我:wrapper<int>是异常安全的。

1.1 class类型的参数

现在看:

wrapper<X> x;

这里X是一个类。在这个定义里,编译器实例化了类wrapper<X>:

template <>

class wrapper<X>

{

public:

wrapper()

{

}

X get()

{

return value_;

}

void set(X const &value)

{

value_ = value;

}

private:

X value_;

wrapper(wrapper const &);

wrapper &operator=(wrapper const &);

};

粗一看,这个定义没什么问题,没有触及异常。但思考一下:

l wrapper<X>包容了一个X的子对象。这个子对象需要构造,意味着调用了X的默认构造函数。这个构造函数可能抛异常。

l wrapper<X>::get()产生并返回了一个X的临时对象。为了构造这个临时对象,get()调用了X的拷贝构造函数。这个构造函数可能抛异常。

l wrapper<X>::set()执行了表达式value_ = value,它实际上调用了X的赋值运算。这个运算可能抛异常。

在wrapper<int>中针对不抛异常的内嵌类型的操作现在在wrapper<X>中变成调用可能抛异常的函数了,同样的模板,同样的语句,但极其不同的含义。

由于这样的不确定性,我们需要采用保守的策略:假设wrapper会根据类来实例化,而这些类在其成员上没有异常规格申明,它们可能抛异常。

1.2 使得包容安全

再假设wrapper的异常规格申明承诺其成员不产生异常。至少,我们必须在其成员上加上异常规格申明throw()。我们需要修补掉这些可能导致异常的地方:

l 在wrapper::wrapper()中构造value_的过程。

l 在wrapper::get()中返回value_的过程。

l 在wrapper::set()中对value_赋值的过程。

另外,在违背throw()的异常规格申明时,我们还要处理std::unexpected。

1.3 Leak #1:默认构造函数

对wrapper的默认构造函数,解决方法看起来是采用function try块:

wrapper() throw()

try : T()

{

}

catch (...)

{

}

虽然很吸引人,但它不能工作。根据C++标准(paragraph 15.3/16,“Handling an exception”):

对构造或析构函数上的function-try-block,当控制权到达了异常处理函数的结束点时,被捕获的异常被再次抛出。对于一般的函数,此时是函数返回,等同于没有返回值的return语句,对于定义了返回类型的函数此时的行为为未定义。

换句话说,上面的程序相当于是:

X::X() throw()

try : T()

{

}

catch (...)

{

throw;

}

这不是我们想要的。

我想过这样做:

X::X() throw()

try

{

}

catch (...)

{

return;

}

但它违背了标准的paragraph 15:

如果在构造函数上的function-try-block的异常处理函数体中出现了return语句,程序是病态的。

我被标准卡死了,在用支持function try块的编译器试验后,我没有找到让它们以我所期望的方式运行的方法。不管我怎么尝试,所有被捕获的异常都仍然被再次抛出,违背了throw()的异常规格申明,并打败了我实现接口安全的目标。

原则:无法用function try块来实现构造函数的接口安全。

引申原则1:尽可能使用构造函数不抛异常的基类或成员子对象。

引申原则2:为了帮助别人实现引申原则1,不要从你的构造函数中抛出任何异常。(这和我在Part13中所提的看法是矛盾的。)

我发现C++标准的规则非常奇怪,因为它们减弱了function try的实际价值:在进入包容对象的构造函数(wrapper::wrapper())前捕获从子对象(T::T())构造函数中抛出的异常。实际上,function try块是你捕获这样的异常的唯一方法;但是你只能捕获它们却不能处理掉它们!

(WQ注:下面的文字原载于Part15上,我把提前了。

上次我讨论了function try块的局限性,并承诺要探究其原因的。我所联系的业内专家没人知道确切答案。现在唯一的共识是:

l 如我所猜测,标准委员会将function try块设计为过滤而不是捕获子对象构造函数中发生的异常的。

l 可能的动机是:确保没人误用没有构造成功的包容对象。

我写信给了Herb Sutter,《teh Exceptional C++》的作者。他从没碰过这个问题,但很感兴趣,以至于将其写入“Guru of the Week”专栏。如果你想加入这个讨论,到新闻组comp.lang.c++.moderated上去看“Guru of the Week #66: Constructor Failures”。

注意function try可以映射或转换异常:

X::X()

try

{

throw 1;

}

catch (int)

{

throw 1L; // map int exception to long exception

}

这样看,它们非常象unexpected异常的处理函数。事实上,我现在怀疑这才是它们的设计目的(至少是对构造函数而言):更象是个异常过滤器而不是异常处理函数。我将继续研究下去,以发现这些规则后面的原理。

现在,至少,我们被迫使用一个不怎么直接的解决方法:

template <typename T>

class wrapper

{

public:

wrapper() throw()

: value_(NULL)

{

try

{

value_ = new T;

}

catch (...)

{

}

}

// ...

private:

T *value_;

// ...

};

被包容的对象,原来是在wrapper::wrapper()进入前构造的,现在是在其函数体内构造的了。这个变化可以让我们使用普通的方法来捕获异常而不用function try块了。

因为value_现在是个T *而不是T对象了,get()和set()必须使用指针的语法了:

T get()

{

return *value_;

}

void set(T const &value)

{

*value_ = value;

}

1.4 Leak #1A:operator new

在构造函数内的try块中,语句

value_ = new T;

隐含地调用了operator new来分配*value_的内存。而这个operator new函数可能抛异常。

幸好,我们的wrapper::wrapper()能同时捕获T的构造函数和operator new函数抛出的异常,因此维持了接口安全。但,记住这个关键性的差异:

l 如果T的构造函数抛了异常,operator delete被隐含调用了来释放分配的内存。(对于placement new,这取决于是否存在匹配的operator delete,我在part 8和9说过了的。)

l 如果operator new抛了异常,operator delete不会被隐含调用。

第二点本不该有什么问题:如果operator new抛了异常,通常是因为内存分配失败,operator delete没什么需要它去释放的。但,如果operator new成功分配了内存但因为其它原因而仍然抛了异常,它必须负责释放内存。换句话说,operator new自己必须是行为安全的。

(同样的问题也发生在通过operator nwe[]创建数组时。)

1.5 Leak #1B:Destructor

想要wrapper行为安全,我们需要它的析构函数释放new出来的内存:

~wrapper() throw()

{

delete value_;

}

这看起来很简单,但请等一下说大话!delete value_调用*value_的析构函数,而这个析构函数可能抛异常。要实现~wrapper()的接口异常,我们必须加上try块:

~wrapper() throw()

{

try

{

delete value_;

}

catch (...)

{

}

}

但这还不够。如果*value_的析构函数抛了异常,operator delete不会被调用了来释放*value_的内存。我们需要加上行为安全:

~wrapper() throw()

{

try

{

delete value_;

}

catch (...)

{

operator delete(value_);

}

}

仍然没结束。C++标准运行库申明的operator delete为

void operator delete(void *) throw();

它是不抛异常了,但自定义的operator delete可没说不抛。要想超级安全,我们应该写:

~wrapper() throw()

{

try

{

delete value_;

}

catch (...)

{

try

{

operator delete(value_);

}

catch (...)

{

}

}

}

但这还存在危险。语句

delete value_;

隐含调用了operator delete。如果它抛了异常,我们将进入catch块,一步步执行下去并再次调用同样的operator delete!我们将程序连续暴露在同样的异常下。这不会是个好程序的。

最后,记住:operator delete在被new出对象的构造函数抛异常时被隐含调用。如果这个被隐含调用的operator delete也抛了异常,程序将处于两次异常状态并调用terminate()。

原则:不要在一个可能在异常正被处理过程被调用的函数中抛异常。尤其是,不要从下列情况下抛异常:

l destructors

l operator delete

l operator delete[]

几个小习题:用auto_ptr代替value_,然后重写wrapper的构造函数,并决定其虚构函数的角色(如果需要的话),条件是必须保持异常安全。

1.6 题外话

我本准备一次维持异常安全的。但现在是第二部分,并仍然有足够的素材写成第三部分(我发誓那是最后的部分)。下次,我将讨论get()和set()上的异常安全问题,和今天的内容同样精彩。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有