Guru of the Week 条款08:GotW挑战篇——异常处理的安全性 (转)[@more@]

GotW #08 CHALLENGE EDITION Exception Safety

著者:Herb Sutter

翻译:kingofark

[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。

Revision 1.0

Guru of the Week 条款08:GotW挑战篇——异常处理的安全性XML:namespace prefix = o ns = "urn:schemas-microsoft-com:Office:office" />

 

难度:9 / 10

 

(异常处理机制是解决某些问题的上佳办法,但同时它也引入了许多隐藏的控制流程;有时候,要正确无误的使用它并不容易。不妨试试自己实现一个简单的container(这是一种可以对其进行push和pop操作的栈),看看它在异常-安全的(exception-safe)和异常-中立的(exception-neutral)情况下,到底会发生哪些事情。)

 

 

[问题]

1.  实现如下异常-中立的(exception-neutral)container。要求:Stack对象的状态必须保持其一致性(consistent);即使有内部操作抛出异常,Stack对象也必须是可析构的(destructible);T的异常必须能够传递到其调用者那里。

template

// T 必须有缺省的构造函数和拷贝构造函数

class Stack

{

public:

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

unsigned Count();  // 返回T在栈里面的数目

void  Push(const T&);

T  Pop();  // 如果为空,则返回缺省构造出来的T

private:

T*  v_;  // 指向一个用于'vsize_' T对象的

//  足够大的内存空间

unsigned vsize_;  // 'v_' 区域的大小

unsigned vused_;  // 'v_' 区域中实际使用的T的数目

};

附加题:

2.  根据当前的C++标准,标准库中的container是异常-安全的(exception-safe)还是异常-中立的(exception-neutral)?

3.  应该让container成为异常-中立的(exception-neutral)吗?为什么?有什么折衷方案吗?

4.  Container应该使用异常规则吗?比如,我们到底应不应该作诸如“Stack::Stack()throw(bad_alloc);”的声明?

挑战极限的问题:

5.  由于在目前许多的编译器中使用try和catch会给你的程序带来一些额外的负荷,所以在我们这种低级的可复用(reusable)Container中,最好避免使用它们。你能在不使用try和catch的情况下,按照要求实现Stack所有的成员函数吗?

在这里提供两个例子以供参考(注意这两个例子并不一定符合上述题目中的要求,仅供参考,以便于你下手解题):

template

Stack::Stack()

: v_(0),

vsize_(10),

vused_(0)

{

v_ = new T[vsize_]; // 初始的内存分配(创建对象)

}

template

T Stack::Pop()

{

T result; //如果为空,则返回缺省构造出来的T

if( vused_ > 0)

{

result = v_[--vused_];

}

return result;

}

[解答]

[作者记:这里的解决方案并不完全正确。本文经修正的增强版本,你可以在C++Report 1997年的9月号、11月号和12月号上面找到;另外,其最终版本在我的《Exceptional C++》里面。]

重要的事项:我确实不敢保证下面的解决方案完全满足了我原题的要求。实际上我连能够正确编译这些代码的编译器都找不到!在这里,我讨论了所有我能想得到的那些交互作用;而本文的主要目的则是希望说明,在编写异常-安全的(exception-safe)代码时需要格外的小心。

另外,Tom Cargill也有一篇非常棒的文章《Exception Handling:A False Sense of Security》(C++Report, vol.9 no.6, Nov-Dec 1994)。他通过这篇文章来说明,异常处理是个棘手的小花招,技巧性非常强,但也并不就是笼统的说不要使用异常处理,而是说人们不要过分的迷信异常处理。只要认识到这一点,并在使用时小心一点就可以了。

[作者再记:最后再说一点。为了简化解决方案,我决定不去讨论用来解决异常-安全(exception-safe)资源之归属问题的基类技术(base class technique)。我会邀请Dave Abrahams(或者其他人)来继续讨论,阐述这个非常有效的技术。]

现在先回顾一下我们的问题。需要的接口如下:

template

// T 必须有缺省的构造函数和拷贝构造函数

class Stack

{

public:

Stack();

~Stack();

Stack(const Stack&);

Stack& operator=(const Stack&);

unsigned Count();  //返回T在栈里面的数目

void  Push(const T&);

T  Pop();  //如果为空,则返回缺省构造出来的T

private:

T*  v_;  //指向一个用于'vsize_' T对象的

//  足够大的内存空间

unsigned vsize_;  // 'v_'区域的大小

unsigned vused_;  // 'v_' 区域中实际使用的T的数目

};

现在我们来看看实现。我们对T有一个要求,就是T的析构函数(destructor)不能抛出异常。这是因为,如果允许T的析构函数(destructor)抛出异常,那我们就很难甚至是不可能在保证代码安全性的前提下进行实现了。

//----- DEFAULT CTOR ----------------------------------------------

template

Stack::Stack()

: v_(new T[10]),  // 缺省的内存分配(创建对象)

vsize_(10),

vused_(0)  // 现在还没有被使用

{

// 如果程序到达这里,说明构造过程没有问题,okay!

}

//----- 拷贝构造函数 -------------------------------------------------

template

Stack::Stack( const Stack& other )

: v_(0),  // 没分配内存,也没有被使用

vsize_(other.vsize_),

vused_(other.vused_)

{

v_ = NewCopy( other.v_, other.vsize_, other.vsize_ );

//如果程序到达这里,说明拷贝构造过程没有问题,okay!

}

//----- 拷贝赋值 -------------------------------------------

template

Stack& Stack::operator=( const Stack& other )

{

if( this != &other )

{

T* v_new = NewCopy( other.v_, other.vsize_, other.vsize_ );

//如果程序到达这里,说明内存分配和拷贝过程都没有问题,okay!

delete[] v_;

// 这里不能抛出异常,因为T的析构函数不能抛出异常;

// ::operator delete[] 被声明成throw()

v_ = v_new;

vsize_ = other.vsize_;

vused_ = other.vused_;

}

return *this;  // 很安全,没有拷贝问题

}

//----- 析构函数 ----------------------------------------------------

template

Stack::~Stack()

{

delete[] v_;  // 同上,这里也不能抛出异常

}

//----- 计数 -----------------------------------------------------

template

unsigned Stack::Count()

{

return vused_;  // 这只是一个内建类型,不会有问题

}

//----- push操作 -----------------------------------------------------

template

void Stack::Push( const T& t )

{

if( vused_ == vsize_ )  // 可以随着需要而增长

{

unsigned vsize_new = (vsize_+1)*2; // 增长因子

T* v_new = NewCopy( v_, vsize_, vsize_new );

//如果程序到达这里,说明内存分配和拷贝过程都没有问题,okay!

delete[] v_;  //同上,这里也不能抛出异常

v_ = v_new;

vsize_ = vsize_new;

}

v_[vused_] = t; // 如果这里抛出异常,增加操作则不会执行,

++vused_;  //  状态也不会改变

}

//----- pop操作 ------------------------------------------------------

template

T Stack::Pop()

{

T result;

if( vused_ > 0)

{

result = v_[vused_-1];  //如果这里抛出异常,相减操作则不会执行,

--vused_;  //  状态也不会改变

}

return result;

}

//

// 注意: 细心的读者Wil Evers第一个指出,

//  “正如在问题中定义的那样, Pop()强迫使用者编写非异常-安全的代码,

//  这首先就产生了一个负面效应(即从栈中间pop出一个元素);

//  其次,这还可能导致遗漏某些异常(比如将返回值拷贝到代码调用者的目标

//  对象上)。”

//

// 同时这也表明,很难编写异常-安全的代码的一个原因就是因为

// 它不仅影响代码的实现部分,而且还会影响其接口!

// 某些接口(比如这里的这一个)不可能在完全保证异常-安全的情况下被实现。

//

// 解决这个问题的一个可行方法是把函数重新构造成

// "void Stack::Pop( T& result)".

// 这样,我们就可以在栈的状态改变之前得知到结果的拷贝是否真的成功了。

// 举个例子如下,

// 这是一个更具有异常-安全性的Pop()

//

template

void Stack::Pop( T& result )

{

if( vused_ > 0)

{

result = v_[vused_-1];  //如果这里抛出异常,

--vused_;  //  相减操作则不会执行,

}  //  状态也不会改变

}

//

// 这里我们还可以让Pop()返回void,然后再提供一个Front() 成员函数,

// 用来访问顶端的对象

//

//----- 辅助函数 -------------------------------------------

// 当我们要把T从缓冲区拷贝到一个更大的缓冲区时,

//  这个辅助函数会帮助分配新的缓冲区,并把元素原样拷贝过来。

//  如果在这里发生了异常,辅助函数会释放占用得所有临时资源,

//  并把这个异常传递出去,保证不发生内存泄漏。

//

template

T* NewCopy( const T* src, unsigned srcsize, unsigned destsize )

{

destsize = max( srcsize, destsize ); // 基本的参数检查

T* dest = new T[destsize];

// 如果程序到达这里,说明内存分配和构造函数都没有问题,okay!

try

{

copy( src, src+srcsize, dest );

}

catch(...)

{

delete[] dest;

throw;  // 重新抛出原来的异常

}

// 如果程序达到这里,说明拷贝操作也没有问题,okay!

return dest;

}

对附加题的解答:

第2题:根据当前的C++标准,标准库中的container是异常-安全的(exception-safe)还是异常-中立的(exception-neutral)?

关于这个问题,目前还没有明确的说法。最近委员会也展开了一些相关的讨论,涉及到应该提供并保证弱异常安全性(即“container总是可以进行析构操作”)还是应该提供并保证强异常安全性(即“所有的container操作都要从语义上具有‘要么执行要么撤销(commit-or-rollback)’的特性”)。正如Dave Abrahams在委员会中的一次讨论以及随后通过电子邮件进行的讨论中所表明的那样,如果实现了对弱异常安全性的保证,那么强异常安全性也就很容易得到保证了。我们在上面提到的几个操作正是这样的。

第3题:应该让container成为异常-中立的(exception-neutral)吗?为什么?有什么折衷方案吗?

有时候,为了保证某些container异常-中立性(exception-neutrality),其内的某些操作将会不可避免的付出一些空间代价。可见异常-中立性(exception-neutrality)本身并不错,但是当实现强异常安全性所要付出的空间或时间代价远远大于实现弱异常安全性的付出的时候,要实现异常-中立性(exception-neutrality)就太不现实了。有一个比较好的折衷方案,那就是用文档记录下T中不允许抛出异常的操作,然后通过遵守这些文档规则来保证其异常-中立性(exception-neutrality)。

第4题:Container应该使用异常规则吗?  比如,我们到底应不应该作诸如“Stack::Stack()throw(bad_alloc);”的声明?

答案是否定的。我们不能这样做,因为我们预先并不知道T中哪些操作会抛出异常,也不知道会抛出什么样的异常。

应该注意的是,有些container的某些操作(例如,Count())只是简单的返回一个数值,所以我们可以断定它不会抛出异常。虽然我们原则上可以用throw()来声明这类操作,但是最好不要这么做;原因有两个:第一,如果你这样做了,那么当你以后想修改实现细节使其可以抛出异常的时候,就会发现其存在着很大的限制;第二,无论异常是否被抛出,异常声明(exception specification)都会带来额外的性能开销。因此,对于那些频繁使用的操作,最好不要作异常声明(exception specification)以避免这种性能开销。

对挑战极限题的解答:

第5题:由于在目前许多的编译器中使用try和catch会给你的程序带来一些额外的负荷,所以在我们这种低级的可复用(reusable)Container中,最好避免使用它们。你能在不使用try和catch的情况下,按照要求实现Stack所有的成员函数吗?

是的,这是可行的,因为我们仅仅只需要捕获“...”部分(见下面的代码)。一般,形如

try { TryCode(); } catch(...) { CatchCode(parms); throw; }

的代码都可以改写成这样:

struct Janitor {

Janitor(Parms p) : pa(p) {}

~Janitor() { if uncaught_exception() CatchCode(pa); }

Parms pa;

};

{

Janitor j(parms); // j is destroyed both if TryCode()

// succeeds and if it throws

TryCode();

}

我们只在NewCopy函数中使用了try和catch。下面就是重写的NewCopy函数,用以体现上面说的改写技术:

template

T* NewCopy( const T* src, unsigned srcsize, unsigned destsize )

{

destsize = max( srcsize, destsize ); // basic paRM check

struct Janitor {

Janitor( T* p ) : pa(p) {}

~Janitor() { if( uncaught_exception() ) delete[] pa; }

T* pa;

};

T* dest = new T[destsize];

// if we got here, the allocation/ctors were okay

Janitor j(dest);

copy( src, src+srcsize, dest );

// if we got here, the copy was okay... otherwise, j

// was destroyed during stack unwinding and will handle

// the cleanup of dest to avoid leaking memory

return dest;

}

我已经说过,我曾与几个擅长靠经验来进行速度测试的人讨论过上述问题。结论是在没有异常发生的情况下,try和catch往往要比其它方法快得多,而且今后还可能变得更快。但尽管如此,这种避免使用try和catch的技术还是非常重要的,一来是因为有时候就是需要写一些比较规整、比较容易维护的代码;二来是因为现有的一些编译器在处理try和catch的时候,无论在产生异常的情况下还是在不产生异常的情况下,都会生成效率极其低下的代码。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10752043/viewspace-990912/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10752043/viewspace-990912/

Guru of the Week 条款08:GotW挑战篇——异常处理的安全性 (转)相关推荐

  1. Guru of the Week 条款01: 变量的初始化

    GotW #01 Variable Initialization 著者:Herb Sutter 翻译:kingofark [声明]:本文内容取自www.gotw.ca网站上的Guru of the W ...

  2. 【Effection C++】读书笔记 条款07~条款08

    [Effection C++]读书笔记 Part2 构造/析构/赋值运算 条款07:为多态基类声明virtual析构函数 带有多态(polymorphic)性质的base classes应该声明一个v ...

  3. Effective C++ 条款08:别让异常逃离析构函数

    今天学习Effective C++时,我发现对于这个08条款我并不敏感,可能在目前的学习coding中并没有遇到过,现在就打算创建一个空的博客记录自己今天学了的东西的标题,然后以后coding中若遇到 ...

  4. 什么是真正的 HTAP ?(二)挑战篇

    上一篇文章中,我们从技术和商业角度分析了 HTAP 系统缘起的背景,本篇文章中,我们将从 HTAP 定义及其相关核心技术等方面来讨论:构建一个 HTAP 所面临的核心问题和挑战有哪些? HTAP 涉及 ...

  5. 数学建模清风微信公众号的习题答案(挑战篇-完结)

    以下题目是来自微信公众号数学建模清风老师的题目 以下是本人结合在微信公众号上学到的知识去做的,如有不正确或不足,欢迎指正! Q15.在本章3.3.5小节介绍sort函数时,我们留下了一个问题:如果存在 ...

  6. 【unity 保卫星城】--- 开发笔记08(太空站篇)

    [unity 保卫星城]--- 开发笔记 保卫星城-太空站篇 一.太空站的功能 二.功能的实现 说起来保卫星城最重要的星城还没讲,其实星城就是一个太空站,而我们的任务就是保护太空站不被敌人破坏. 保卫 ...

  7. 数学建模清风微信公众号的习题答案(挑战篇-数据异常值处理)

    以下题目是来自微信公众数学建模清风老师的题目 1.0数据异常处理 1.1正态分布的3σ\sigmaσ原则 1.2箱线图识别异常值 2.0 数据异常处理实例 3.0 总结分析 以下是个人结合在微信公众号 ...

  8. 数学建模清风微信公众号的习题答案(挑战篇-蒙特卡罗思想、枚举法和网格搜索法)

    以下题目来自微信公众数学建模清风老师的题目 蒙特卡罗模拟计算π\piπ 蒙特卡罗模拟求定积分 蒙特卡罗模拟计算概率 枚举法与网格搜索法 以下是个人结合在微信公众号是所学到的知识取做的,如有不正确或不足 ...

  9. 数学建模清风微信公众号的习题答案(挑战篇2)

    以下题目是来自微信公众号数学建模清风老师的题目 以下是个人结合在微信公众号上学到的知识去做的,如有不正确或不足之处,欢迎指正! Q6.最近短视频上有一个有趣的街头抽奖游戏,规则如下:摆摊的店家准备了2 ...

最新文章

  1. windows time 服务无法启动 错误1058 解决方法
  2. python与Excel的完美结合
  3. c++17进阶(3)-Boehm GC垃圾回收(1)
  4. SQL Server 2005与2000写法上的差别
  5. [JavaScript] 函数同名问题
  6. Codeforces 295A. Greg and Array
  7. C/C++的内存泄漏检测工具Valgrind memcheck的使用经历
  8. LinkedIn庄振运:从国家部委公务员到硅谷系统性能专家,创新是唯一主旋律
  9. 中国台湾研发miniLED技术进展迅速,将给面板技术带来变革
  10. 根据银行卡号查询银行名接口
  11. 【小样本基础】小样本学习方法总结:模型微调、数据增强、迁移学习
  12. python显示图片_python Image 模块处理图片
  13. Can't open /dev/sdb1 exclusively. Mounted filesystem?
  14. matlab怎么定义矩阵变量_MATLAB01:基本的数学运算与矩阵运算
  15. CH340国产USB转异步串口芯片替代CP2102对比CH340C与CH340G
  16. 地理信息系统GIS--介绍
  17. 异常:could not initialize proxy - the owning Session
  18. 提升技术团队战斗力的几件事
  19. Android游戏开发:游戏框架的搭建(1)
  20. D3.js音乐可视化

热门文章

  1. 为什么坚持用iPod?没有比它更好的赏乐方式了
  2. 上海中心大厦的镇楼神器动了
  3. 无人驾驶产业的国际比较和PEST分析
  4. 连接组学--神经科学的前沿 ------《连接组》读后感读书笔记
  5. 视频文件如何转换格式?爱奇艺qsv怎么转换mp4
  6. 加密javascript代码
  7. 通信软件行业规模及未来发展前景
  8. ArcGIS如何将地理要素数据和JSON进行互转
  9. linux透明防火墙接入fte 300 网络的问题
  10. 计算机应用 建模,第十八届“江泽涵杯”数学建模与计算机应用竞赛通知 - 团委学术科创部(XSKC)版 - 北大未名BBS...