游戏开发论坛

 找回密码
 立即注册
搜索
查看: 8588|回复: 2

C++基本功和 Design Pattern系列(11) Exception

[复制链接]

27

主题

179

帖子

259

积分

中级会员

Rank: 3Rank: 3

积分
259
发表于 2006-12-31 03:55:00 | 显示全部楼层 |阅读模式
======================================================
大家请把我的文章当参考,详细内容 还请参照 权威书
籍如果文中有错误和遗漏, 请指出,Aear会尽力更正,
谢谢!
Aear Blog: http://blog.sina.com.cn/u/1261532101
======================================================

说到Exception就要说下相关的Error Handling. 比较常用的Error Handling一般有如下几种类方式:
    1. Return Value
    2. Assert
    3. Debug Output
    4. Exception

相对于其他三种错误处理方式, Exception更加容易使用,而且使得错误代码相对集中,同时使得独立函数库的开发更加方便。同样,对于C++来说, Exception提供了Class的Constructor 和 Operator = 错误处理机制,因为这两者都不是能够通过return value进行报错的。

但是就游戏开发来说, Exception最大的缺点是内存和CPU的开销。当然,不是说游戏的代码中不应该使用Exception。 Aear见过用Exception的游戏代码,也有完全不用Exception的代码。因为对游戏来说,应该在运行过程中保持自身状态的正确性,不应该产生任何的无法处理的Exception。而所有能够自己处理的错误情况,都是能够通过Return Value 来解决的。唯一可能产生Exception的地方,就是系统资源,比如磁盘文件,网络等。不过大部分系统掉用都提供非Exception的错误处理。不过程序开发各不相同,用不用Exception可能还是需要大家自行决定。

Aear个人观点是能不用Exception,就不用Exception,但是应该用Exception的时候,一定不要省。比如constructor里。

============ Exception的用法 ============

要使用Exception, 要么用系统的Exception的类,要么定义自己的类。在定义自己类的时候,可以继承STD里边的Exception类,也可以创建自己新的类。比如:

class ExceptionBase{
    ...
};

class ExceptionDerived : public ExceptionBase{
    ...
};

需要注意的是,通常定义自己的Exception类的时候,都要有一个公共的Base Exception Class, 这样能够保证写代码的时候catch所有的你自定义的Exception,比如:

try {
    ...
}catch( ExceptionDerived & e ) {
    ...
}catch( ExceptionBase & e ) {

    // Catch 其他的Exception, 这样的设计即使今后添加新的Exception,只要
    // 是从ExceptionBase继承来的,都会被catch到。

}catch( ... ) {

    // 这里最好再加上 catch(...)来catch所有的exception,防止有未catch的    // exception. 因为如果有unexpected exception, C++的缺省动作是直接
    // 终止程序的运行。

};

============ Exception in Constructor ============

如果一个Constructor产生exception而且没有被程序catch到的话,那么这个object的创建就会失败, 比如:

class MemoryBlock {
private:
    void * _pMem;
public:
    MemoryBlock ( UINT32 size )
    {
       _pMem = new char[size];
    };
    ....
};

MemoryBlock myMemory(100000000000000000000000000);

如果new在分配内存的过程中throw一个Exception ,通常是 bad_alloc,那么myMemory的创建就会失败,以后任何对 myMemory的成员访问,都是非法的,会导致程序的崩溃。

让我们看看另一中写法:

class MemoryBlock {
private:
    void * _pMem;
public:
    MemoryBlock ( UINT32 size ) :
       _pMem(new char[size])
    { };
    ....
};
上面也是合法的,不过会产生同样的问题。但是区别在于如果在代码中catch到exception,那么第一种写法,能够保证object被创建,而第二种写法不能。比如:

    // MemoryBlock 能够被创建
    MemoryBlock ( UINT32 size )
    {
       try {
           _pMem = new char[size];
       } catch(...) {}
    };

    // MemoryBlock 创建失败
    MemoryBlock ( UINT32 size )
    try
       : _pMem(new char[size])
    { } catch(...) {};


============ Exception in Destructor ============

其实对于Destructor来说就一句话,不能在Destructor中Throw Exception。原因很简单,因为通常Destructor要么在Delete Object中掉用,要么在已经Throw了Exception的时候,由系统掉用。如果在Throw Exception的情况下再Throw Exception的话,那么程序就会强制终止。

============ Exception in Operator ============

这个是比较麻烦的,通常的Exception的处理有好几个级别, Basic, Strong, Nofail.我们这里只说下Strong Exception Safety。 下面是个例子:

class X {
    ...
private:
    void * _pMem1;
    UINT32 _pMemSize1;
    void * _pMem2;
    UINT32 _pMemSize2;

public:
    X& operator = ( const X & xo )
    {
        if( _pMem1 ) delete _pMem1;
        if( _pMem2 ) delete _pMem2;
        
        _pMem1 = new char[xo._pMemSize1];
        _pMem2 = new char[xo._pMemSize1];
        ...
    };
};

这里如果 _pMem2 = new char[xo._pMemSize1]; Throw一个Exception,那么X只是被Copy了一半。状态是不完整的。但是原来在pMem1&2中的数据已经消失了。如果是Strong Exception Safety,那么要求如果throw excpetion,那么class的数据应该恢复在之前的状态,比如经典的exception safe operator = 如下:

    X& operator = ( const X & xo )
    {
        X temp(xo);
        swap( *this, temp );
        return *this;
    };

swap是交换*this 和 Temp的所有数据。通常我们能够保证这个过程没有任何exception的产生。因此即使 temp(xo) throw一个exception, 也不会影响当前类的任何状态变化。


============ RAII ============

最后说一种不使用Exception而能保证没有Resource Leakage的技术。那就是 Resource Aquisition Is Initialization ( RAII ). 其原理很简单,就是C++标准保证一个被成功创建的 Object,无论任何情况下(即使是在Throw exception ), 它的 Destructor都会被掉用。 因此,我们可以用一个object 的constructor 来获取资源,用Destructor来释放资源。下面举个最简单的应用,thread 的 asynchronization:

class CriticalSection {
public:
    CriticalSection( CRTICIAL_SECTION *pCs ) :
       _pCs(pCS)
    { EnterCriticalSection( _pCS ) };

    ~CriticalSection( )
    { LeaveCriticalSection( _pCS ) };
private:
    CRTICIAL_SECTION * _pCs;
};

通常我们使用Critical Section的时候,用下列方式:

void threadXX( CRTICIAL_SECTION * pCs)
{
    EnterCriticalSection( pCS );

    void * pTemp = new char[100000000];

    LeaveCriticalSection( pCS );
}

问题是如果     void * pTemp = new char[100000000]; Throw一个 bad_alloc,那么 LeaveCriticalSection( pCS );就不会被掉用而直接返回,很容易导致死锁。类似的代码在游戏服务器端的设计是很常见的,正确的做法是使用上面定义的类:

void threadXX( CRTICIAL_SECTION * pCs)
{
    CriticalSection temp( pCS );

    void * pTemp = new char[100000000];
}

由于即使throw exception, C++保证temp的destructor一定会被调用。因此不会产生死锁的情况。

============ 其他 ============

比如下面的代码是很容易产生问题的:
    function( new char[100], new char[300] );
如果new char[300]throw exception,那么 new char[100]很有可能就不会被释放。

推荐使用auto_ptr或者boost中的Shared_ptr,特别是在class 的initialization list 中, 比如下列做法不使用catch exception也不会产生内存泄露:

class X{
    X() :
    _ptr1(new XXX() ),
    _ptr2(new XXX() )
    {};

private:
    auto_ptr<void *> _ptr1;
    auto_ptr<void *> _ptr2;
}

Destructor中不需要catch exception,因为destructor主要是调用其他的destructor,没有任何的destructor会throw exception的,所以没必要catch.

这次就说这么多,大家过的开心,下次见!

















8

主题

716

帖子

716

积分

高级会员

Rank: 4

积分
716
发表于 2007-1-4 14:42:00 | 显示全部楼层

Re:C++基本功和 Design Pattern系列(11) Exception

很高兴地看到aear把<<Exceptional C++ Style>>的第11,12条以自己理解的方式重述了一遍
对于exception,c++ fans是即爱又恨,在使用上往往是举棋不定,欲言又止
而delphi fans却早已把它当作"本该如此"得使用得大义凛然,毫不脸红,很自然的事情
我觉得既然谈到exception,那么需要着重理解的应该是abrahams异常安全保证,aear只谈到一小部分,大家可以继续深入了解一下,在有exception的日子里,一个可以保证不抛出异常的swap实现意味着什么

当然,exception因此会被认为是sin,是上帝给我们c++ fans的惩罚,而且一些大型或嵌入的项目里coding standard通常也不允许使用的
但是,生活正是因为充满了无数的选择和烦恼,所以才有存在的意义

同样,吾辈c++ fans应该因为我们比其他语言的programmer拥有更多的选择而庆幸,那个TMD的什么保证不抛异常的swap虽然明显是那么得over-engineering,但就像咱们可以用template写递归一样,这是咱们的乐趣,All his geese are swans.

1

主题

5

帖子

9

积分

新手上路

Rank: 1

积分
9
发表于 2010-2-2 17:09:00 | 显示全部楼层

Re: C++基本功和 Design Pattern系列(11) Exception

我是支持在游戏中使用异常的。

理由一:异常真会成为性能瓶颈吗。

    大家都知道,解决性能问题优先从性能的瓶颈入手。异常开销会成为瓶颈吗?异常引入的性能开销主要是在抛出异常的时候,但游戏大多时间是在无异常的情况下进行的,也就是大多时候影响不大。面对大量的图形、物理、AI的性能开销,异常机制的开销和它们根本不是一个数量级的。大多对异常的性能担心的人都是停留在理论上的,有几个提出不用异常的人是真的在游戏中使用了异常机制最后发现它是瓶颈的?当然异常需要合理使用。

理由二:游戏是复杂的,异常就是为良好的架构而生的

    忘了在那本书里看到:  异常——先让程序更脆弱,再让程序更坚强。有多少次程序出现的一个BUG,你会奇怪原来都是好好的,怎么就错了。最后才发现该错误早就存在,只是刚写的一段代码触发了它,这是为什么?主要是因为不用异常的程序太“健壮”了,开发者容易忘了处理return false,开发过程中老老实实的按正常流程操作,加上数据量少不容易引发错误。开发期的“健壮”带来就是面对上千万玩家的脆弱。

    当然有人担心运营时的脆弱,例如场景服务器一个请求异常了或一个玩家逻辑刷新中异常了,服务器就挂了,这样不太好。别忘了,异常机制最大的优点是可以集中处理异常,你可以在架构的不同层面处理异常。你只要把处理请求和某个玩家刷新的代码前后catch异常就行了。依然可以让服务器运行,同时通过日志,还可以及时修改BUG。

    另外异常的引入有利于查错。游戏的代码实在太庞大了,开发者有多少次需要费时去调是一个BUG,最终发现一处return false,结果层层跟进才找到根源。而配合log系统的异常机制,可以让你更快定位到错误。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-6-16 00:36

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表