随笔-5  评论-33  文章-0  trackbacks-0
为了更好的讨论这个问题,假设我们要设计一个关于people的class,并进行如下约定:
1. age 小于 0 的baby不在我们的考虑范围之内.
2. age 大于 200 的people我们认为是不存在的.
即,关于people的age,我们考虑的范围是[0 , 200]
基于以上假设,我们设计如下一个class,这里为了简化代码,突出问题的主体,将部分成员函数及成员变量去掉.

class People
{
    
public:
        
explicit People( int iAge )
        : m_iAge( iAge )
        
{
        
        }

}
;

看起来很简单,下一步我们可以使用这个class了.

People janice( 28 );
janice.Sing();

如果类似于上面这样以合法的参数构造一个People,那么后续的操作也在我们的可控范围之内,也是理想的情况.
但是如果我们以非法的参数构造People并进行相关的函数调用,如下:

People hebe( -1 );
hebe.Sing();

People ella( 
201 );
ella.Dance();


那么这样实在是太糟糕了,因为在调用Sing()和Dancing()时我们甚至不知道调用的对象的内部状态是非法的,这也就成为bug的一个源泉.

总结一下上面的例子中遇到的问题:在以特定的参数构造一个对象时,如果参数非法或构造失败,我们应当向调用者反馈这一信息.

对于一般的成员函数,如果能够有返回值,那么我们可以通过返回值来标识传递给函数的参数非法或内部运行失败这种情况.
但是对于构造函数,因为它不能有返回值,所以,我们必须使用其它的方法来向调用者反馈"传递给构造函数的参数非法或构造失败".

针对于这一问题有三种解决方案:

第一种方案:在构造函数的参数中传递一个额外的参数用于标识构造是否成功.
在这种方案的指导下,代码如下:

class People
{
    
public:
        People( 
int iAge , bool &bInitStatus )
        : m_iAge( iAge )
        
{
            
if( ( iAge < 0 ) || ( iAge > 200 ) )
            
{
                bInitStatus 
= false;
            }

            
else
            
{
                bInitStatus 
= true;
            }

        }

}
;


然后我们可以这样使用:

bool bInitStatus = false;
People hebe( 
-1 , bInitStatus );
iffalse == bInitStatus )
{
    
// handle error
}

else
{
    hebe.Sing();
}


这种方法是可行的,但是代码看起来过于丑陋且不够直观,这里只是作为一种方案提出并不推荐使用.

第二种方案:使用两段构造的形式.
所谓的两段构造是指一个对象的内部状态的初始化分两步完成,将构造函数中的部分初始化操作转移到一个辅助初始化的成员函数中:
第一步是通过构造函数来完成部分内部状态的初始化.
第二步是通过类似于 Initialize 之类的成员函数来完成对象内部状态的最终初始化.

两段构造的形式在 MFC 中广泛使用.在MFC中我们经常看到类似于 Initialize , Create 之类的函数.
基于两段构造的形式,代码如下:

class People
{
    
public:
        People( 
void )
        : m_iAge( INVALID_AGE )
        
{
        
        }

        
        
bool Initialize( int iAge )
        
{
            
if( ( iAge < 0 ) || ( iAge > 200 ) )
            
{
                
return false;
            }

            
else
            
{
                m_iAge 
= iAge;
                
return true;
            }
            
        }

}
;


在这种情况下,我们应当这样来使用People:

People hebe;
const bool bStatus = hebe.Initialize( 20 );
iffalse == bInitStatus )
{
    
// handle error
}

else
{
    hebe.Sing();
}


这种方案似乎比第一种方案更优,但是仍有一个潜在的问题:对象是以两步构造完成的.
第一步构造是由构造函数来完的,OK,这一点我们不用担心,编译器帮我们保证.
但是第二步是由类似于 Initialize 之类的成员函数来完成的,如果我们在构造一个People对象之后忘记了调用 Initialize ,
那么这个对象的内部状态仍然是非法的,后续的操作也将由此引发bug.这也是"两段构造"这种形式受到诟病的原因之一.
另一方面,"两段构造"的形式与C++的"RAII",Resource Acquisition Is Initialization(资源获取即初始化),这一原则相违背.
因为以"两段构造"这种形式设计的class People 在构造一个对象时,它的内部状态实际上并没有完全初始化,我们需要调用 Initialize 来辅助完成最终的初始化.
所以,尽管"两段构造"这种方案可以解决我们所遇到的"对构造函数参数非法进行反馈"这个问题,但是这种方案并不够优雅.

但是为什么MFC会先择"两段构造"这种形式呢,因为在C++发展的初期,当异常机制还不是足够成熟,没有得到广泛的认可和使用时,
MFC中选择两段构造或许也是情理之中的,也许还有其它的原因,类似的类库还有ACE...

当然,在某些情况下,使用两段构造也有其独到的好处.
下面所设计的场景可能有一些牵强,但只是为了力求简单并能够说明问题.(代码进行了大量简化)

class Server
{
    
public:
        Server(
void)
        
{
            
// allocate a huge chunk of memory to store data
        }

        
        
~Server( void )
        
{
            
// free all the used resource
        }

}
;


然后在我们的系统中,我们需要使用一个 server pool , 在系统启动时,我们需要 server pool 中有 100 个 Server 可用.

Server serverPool[ 100 ];


在系统负载最大的时候,假定100个Server可以胜任,但是在大多数情况下,我们只需要少量的Server即可以完成任务.
在这种情况下: Server serverPool[ 100 ]; 将会消耗大量的资源(而且大部分资源我们并不会使用),这是我们不愿意接受的.
之所以出现这种情况,因为我们在构造函数中分配了大量资源,这种分配是随构造函数的调用而自动完成的.


这时,如果我们使用"两段构造"的方法就能在一定的程度上解决这个问题.

class Server
{
    
public:
        Server(
void)
        
{
            
// do nothing here.
        }

        
        
bool Initialize( void )
        
{
            
// allocate a huge chunk of memory to store data
        }

        
        
~Server( void )
        
{
            
// free all the used resource
        }

}
;


在这种情况下: Server serverPool[ 100 ]; 的开销就很小了,我们可以很好地控制对系统资源的使用,而不会浪费.
当然,当我们从 serverPool 中获取一个 Server 对象时,我们要调用 Initialize 进行最终的初始化操作.


第三种方案:使用异常
即是当用于构造 People 对象的参数非法时,我们选择在构造函数中抛出一个异常来反馈给调用者"参数非法,构造失败"的相关信息.

class People
{
    
public:
        
explicit People( int iAge ) throw( std::invalid_arguement )
        : m_iAge( iAge )
        
{
            
if( ( iAge < 0 ) || ( iAge > 200 ) )
            
{
                
throw std::invalid_arguement( "invalid argument" );
            }

        }

}
;


那么我们可以这样使用:

try
{
    People hebe( 
20 );
    hebe.Sing();
}

catchconst std::invalid_arguement &refExcept )
{
    
// handle exception here.
}


这种方案似乎是最优的:符合RAII原则,也符合B.S等一批老大推行的"现代C++程序设计风格".

但是很多在开发一线上的同学们都反对在代码中使用异常,实际上我也不愿意在代码中使用异常,
至少不愿意看到类似于java代码中那样铺天盖地的"throw try catch".
我对异常的使用也仅仅是局限在类似于那些适合"用异常来代替两段构造"的场景中,对于其它的情况,
我更愿意用返回错误码来标识函数内部的运行状态,而不是通过抛出异常的形式来告知调用者.


C++规定:如果执行构造函数的过程中产生异常,那么这个未被成功构造的对象的析构函数将不会被调用.
这一点在很大程度上为我们在构造函数中抛出异常的安全性提供了C++语言级的保证,当然,其它的安全性需要我们自己保证.

对于"向调用者反馈构造函数参数非法或构造失败的相关信息"这个问题,"基于异常"和"基于两段构造"这两种方案我都使用过一段时间,
目的是确定对于自己而言到底哪一种方案用起来更舒服更适合自己.最终的结果是我选择了"基于异常"这种形式.

对于"基于异常"和"基于两段构造",没有哪一种能在所有的情况下都是最优的解决方案,印证了那句名言"there is no silver bullet".
如何在不同的场景中选择其一作为最优的解决方案是我们在设计时需要权衡的问题.


个人愚见,错漏之处还请指正,欢迎大家踊跃发言:)

posted on 2010-03-04 21:23 luckycat 阅读(2721) 评论(18)  编辑 收藏 引用 所属分类: C++

评论:
# re: 设计的两难:选择异常还是两段构造 2010-03-04 22:03 | Corner Zhang
呵呵!如果你要传统的编程风格,就用Initialize() Shutdown()的“两段构造”;若要趋向于捕捉异常的try catch的风格,就用构造器上的异常处理呗!

具体取舍,看项目人员本身素质,按我的经验,传统风格的代码易于调试跟踪,错误就在附近。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-04 22:22 | qiaojie
两种方法都是错误的用法,这地方应该使用断言而不是异常或者错误返回值。传入错误的参数值是程序的BUG而不是真正的异常,异常是由于外部运行环境的错误引起而非程序本身的BUG(例如,内存耗尽,网络错误,文件错误等等),异常处理只应该用在真正属于异常的情况。当然,有的时候People的age参数来自于用户的输入,这个时候也不应该使用异常,而是在用户输入完成,对话框结束,构造People之前,加入一个ValidateUserInput()函数来校验用户输入,如果age属于非法值,弹出一个错误对话框向用户说明错误的原因。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-04 22:32 | luckycat
@Corner Zhang
关于这一点,我们的观点还是很相近的,不同的编程风格决定了不同的设计取舍以及代码风格,
没有哪一种一直都是最优的,选择一种适合自己的并一直坚持下去(当然,适当的时候还是要变通一下).
实际上我并不愿意在代码中大量使用"throw try catch",所以,基本上我不愿意去看java代码,
就如你所说的,传统的代码风格易于跟踪调试;在我看来,传统的"基于错误码"的代码比基于"使用异常"的代码
要紧凑得多,因为我们可以在错误发生的地方立即处理错误,而不像"基于异常"的代码中我们要向下跨越N行代码
来进行错误处理(这一点使得代码的可读性很差).
而且,如果try代码块中太大,那么在对应的catch块中尽管我们可以进行相应的异常处理,但是此时我们却失去了
对发生错误的代码上下文的必要了解.这一点使得我们降低了对代码整体运行流程的可预知性,
更重要的是也降低了错误处理的针对性,因为同一种类型的异常可能由try代码块中的多个地方throw.具体是哪一个throw的无从了解.
so,我的观点是:让构造函数尽量简单,减少误用的可能性,并增加构造函数的安全性(尽量减少构造函数构造失败的可能性).
这样我们也就能在一定程度上减少对异常机制的依赖.至于其它的可带有返回值的成员函数都使用"返回错误码"来取代"抛出异常".
  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-04 23:25 | luckycat
@qiaojie
呵呵,可不能一棒子打死啊.
至于说,类似于"用户输入非法数据"之类的问题到底是算作错误还是异常情况,这一点依赖每个人对同一个事物的认知,
有的人认为这是异常情况,有的人认为这是错误,这个认知层面上的问题我们先不讨论,尊重每个人的看法.
实际上,即使存在上面的认知差异也没有关系,因为问题的本质是对"用户输入非法数据"这种异常也好,错误也好,
我们在代码逻辑中应该如何处理,你觉得应该用类似于"对话框+ValidateUserInput"之类的方法来处理,
我觉得可以通过返回错误码或抛出异常的形式来做处理. 本质上都是在处理一种"非正常情况",只是我们的处理方式不同,
你说应该用你的方法比较好,我觉得用我的方法处理也是可行的,到底用哪一种呢,
即使在这种情况下,我们还是很难选择一个所有人都接受的处理方式. 这里就涉及到设计的权衡和取舍了,有很多种方法都可行,我们尊重每个人在特定的环境中所做出的选择.

/*
"而是在用户输入完成,对话框结束,构造People之前,加入一个ValidateUserInput()函数来校验用户输入,
如果age属于非法值,弹出一个错误对话框向用户说明错误的原因"
*/
你这里只是对一种特例的处理,实际上我们很难在所有的情况都保证传入构造函数的参数是合法的,要是我们真的找到了这样一种方法,
那么"there is a silver bullet !" , 接下来,对于所有奋斗在开发一线的同学们而言,生活就要美好很多,应该再也不会发生类似于"小贝"的悲剧了:)

在你处理的特例中,既然我们能够保证传入构造函数的参数一定是合法的,那确实太好了,"使用异常"和"两段构造"都是多余的.
对于那种我们不能确保传入构造函数的参数是一定是合法的情况,我们该选择哪种处理方式呢,这是这篇文章讨论的根本问题.
如果因为构造函数的参数不合法,或者因为其它的原因构造失败,最基本的一点,我们应当让调用者知道这一情况,
至于调用者如何处理就不在我们关心的范围之内了,是"弹出对话框告知用户重试","忽略这个错误",还是直接"abort",不同的场景下也有不用的选择.
我们要做的就是在"构造一个对象发生异常时"告知调用者"发生了非正常情况".

这篇文章的主题也就是讨论"在构造发生非正常情况时采取何种方式来告知调用者这一情况".
对于这个问题很难有一个"放之四海而皆准"的处理方案,因为这涉及到不同的编程风格,应用场景和设计时的取舍.

不过我们还是可以踊跃地发表自己的看法,在讨论和交流的过程中我们总能发现思维的闪光点.互相学习:)  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 08:34 | 饭中淹
我一般會用一種類似工廠的方法。

class CPeople
{

int m_iAge;

CPeople() : m_iAge(-1) {}
~CPeople() {}
bool _Init( int iAge )
{
if( iAge <= 0 || iAge > 200 )return false;
m_iAge = iAge;
return true;
}
public:
static CPeople * Create( int iAge )
{
CPeople * people = new CPeople();
if( people != NULL &&
!people->_Init(iAge) )
{
people->Release();
people = NULL;
}
return people;
}
void Release() { delete this;}
};


私有的構造函數和西狗函數確保他們不能被單獨分配和刪除
所有的途徑只有create和release。

  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 09:04 | LOGOS
@饭中淹
agree
这正是我想说的。另外,在一些情形下构造函数不易调试,而两段构造则能避开这一调试,选择更好的调试的init  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 10:31 | 肥仔
@饭中淹
每个对象都要显示的create和release。那基本上等价于放弃了C++构造函数和析构函数这两个特性。
对于栈对象,这样会很累,也很容易泄漏。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 12:02 | luckycat
@饭中淹
将"两段构造"与"Fatcory Pattern"结合起来确实是一种巧妙的设计!
内部实现上还是"两段构造",但是对于 class 的用户而言,class CPeople 展现的却是一个单一的"构造接口",
用户一旦调用这个接口"构造对象",那么"两段构造"自动完成,极大地减少了"两段构造"中因为忘记调用"Initialize"所带来的问题.
class CPeople 中的 Create 和 Release 所扮演的角色类似于"构造函数和析构函数",都是进行资源的分配与回收操作.
单纯从"资源管理"的角度来说,肯定是"构造函数和析构函数"相比如"Create 和 Release"更优一些,
因为"构造函数和析构函数"对于"非动态分配的对象以及非placement new方式生成的对象",
构造和析构都会由编译器保证正确自动地调用,大大简化了对资源的管理,或许这也是C++设计构造和析构的出发点之一.

在"两段构造" & "Fatcory Pattern"这种模式下,所有的CPeople对象将都由 Create 接口创建,这势必需要我们管理大量的动态分配的对象,
在这种情况下,如果稍有不慎,我们将面临"resource leak"的问题.这个时候如果我们能将动态分配的CPeople对象用一种更方便安全的方式来管理就更好了,
于是我想到了boost::shared_ptr,不知道大家想到了什么?
类似于下面这样:

void FreeResource( CPeople *pPeople )
{
if( NULL != pPeople )
{
pPeople -> Release();
}
}

CPeople *pHebe = CPeople::Create( 2 );
if( NULL == pHebe )
{
// handle error
}

boost::shared_ptr< CPeople > pPeople( pHebe , FreeResource );

下面我们就可以使用 pPeople 这个智能指针"do whatever you want" :) ,而且使用起来直观方便:
pPeople -> Sing();
也减少了对动态分配资源进行管理的复杂度.



  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 17:36 | 望见
为了程序的健壮性,多余的操作也是必须的。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 18:00 | qiaojie
@luckycat
你的根本问题在于你没理解异常的使用哲学,在错误的前提下去谈什么设计的选择根本就是一件毫无意义的事情。你如果要坚持自己的错误认识,去谈什么尊重每个人的选择,那我只能自认是对牛弹琴了。

再来看为什么我说要用ValidateUserInput而不是其他什么方法,因为验证用户的输入通常来说都是一个复杂的且经常可变的需求,我们假设Person的构造参数里有一个UserName,通常UserName的验证会比较复杂,有长度限制,非法字符限制,重名限制等等,对于重名限制往往还要去用户管理器或者后台数据库查询一下,现在来看看把这些验证逻辑都写到Person的构造函数里是多么傻X的做法啊,首先,Person的构造函数需要依赖用户管理器或者后台数据库才能验证UserName的合法性。其次,当你的构造函数返回了错误值或者异常的时候,外部的处理代码却根本不知道为什么这个用户名是非法的,所以要么还得再写一遍验证逻辑来检查哪里非法,要么直接告诉用户输入非法,让用户摸不着头脑。
  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 19:33 | luckycat
@qiaojie
也许你的异常哲学是正确的并值得大家学习,还请你发文一篇让大家有一个学习的机会,
如果从你的文章中我确实发现了自己的错误,也会从中有所改正,当然,你也不需要"对牛弹琴"这个词语.
我这篇文章中的"People"只是一个用于作为讨论基础的例子,根本的问题是对于"构造函数的参数非法或是构造失败"时,我们应当如果告知调用者.
我并没有说一定要把参数的合法性全部放在构造函数中完成,但是在构造函数中检查参数的合法性是应该的,
就像上面的同学说的"为了程序的健壮性,多余的操作也是必须的"。
在这个例子中,你可以往自己熟悉的GUI方向进行特化,所以你可以使用"对话框"之类的工具来进行传入构造函数之前的参数合法性检验以及进行相关的错误处理,
但是在那些"非GUI"的领域,在那些"我们不能确保传入构造函数的参数一定是合法的,不能保证构造函数一定会构造成功"的情况下,我们到底该如何处理,
我考虑到可以使用"基于异常"或"基于两段构造的形式".
C++提供的异常机制是一种工具,可以作为"函数内部向函数的调用者传递函数内部非正常运行状态"的一种方法.
就如同你说的"内存耗尽,网络错误,文件错误"这种情况下是异常,也许这种情况下我们应当使用"异常机制"(希望没有理解错).
但是如果一个函数内部可能出现"内存耗尽"也会出现"参数非法的问题"(再重申一遍,我们不能永远都保证传入每一个函数的参数都是合法的).
"内存耗尽"这种情况我们使用异常,但是"参数非法问题"我们使用什么呢,
按照你的看法,"参数非法"不属于异常的范围之内,我们不应该使用"异常的形式",但我们还是要告知用户"参数非法"的信息,
假定这里我们"无法使用类似于弹出对话框的形式来告知用户参数非法",那么我可以想到的告知调用者这一信息的方式是"使用错误码",
当然,我们还可以选择"errno"的形式.
这样一来,我们就面临一个问题"一个函数会以异常和错误码两种方式来告知调用者相关的非正常运行信息",
接下来,调用者就要同时使用"try catch"和检查函数的错误码两种方式来检查函数的运行状态,
我觉得如果真的这样设计函数的话,这就是一种很糟糕的设计,不知道你怎么认为.

在告知调用者一个函数内部的"非正常状态"时,我只会择优使用"错误码"或"异常这两种形式"之一,不会同时使用.
基于这一点,如果我选择"以错误码的形式"来反馈给调用者,那么在函数内部"网络错误"时我也会使用错误码来告知调用者(按你的看法,这种情况应该使用异常),
如果我选择"基于异常"的形式,那对"参数非法"的信息我也会抛出"std::invalid_arguement".这是设计上的取舍产生的必然选择.


说到这里,不知道你对于作为std异常类型之一的"std::invalid_arguement"这个词语有什么感想,
我觉得你应该向标准委员会指明"std::invalid_arguement"这个词语,
"从使用异常的哲学上的角度上来看这个概念是错误的,因为参数非法根本就不是异常,我们又怎么能因为参数的非法而throw std::invalid_arguement,
这是在误导广大的std用户,所以必须去掉".  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 21:56 | qiaojie
@luckycat
保证参数的正确性是调用者的责任,而不是被调用的函数的责任,如果你对此有疑问的话可以去阅读一下契约式编程。当然,我并没有说不需要去检查参数的合法性,而是强调应该用assert(0 < age && age < 200);这样的断言来检查参数合法性,这个为什么强调不要用异常或者错误返回值,是因为盲目的大量使用这类错误处理机制会导致整个项目变得混乱,如果任意一个带参数的函数都会因为参数非法而抛异常,那么我在外面接到异常的时候会非常困惑,到底该如何正确处理那么多可能出现的异常?如果是用错误返回值的话就更麻烦,每个函数调用都要进行检查,代码及其繁琐。错误处理机制是整个软件设计的重要环节,他不像接口设计那么显而易见,所以在设计上更应该小心规划合理使用,在
哪里会出异常,哪里该接异常应该做到心中有数正确处理,否则就会陷入混乱。
当然,凡事无绝对,我说的这种使用方式并不适用于组件级或者系统级程序,系统级程序必须要采用防御性编程策略来检测参数的合法性并向调用者报告错误(因为无法预期调用者如何调用函数),这常需要付出代价,常导致系统级提供的API接口跟应用程序之间阻抗适配,需要在应用程序内进行适当的封装。而你的例子显然不属于这类程序。

另外你拿std::invalid_arguement出来我觉得毫无意义,C++标准里不成熟不完善甚至很烂的东西多了去,说上三天三夜也说不完。C++的异常机制尤其不完善,备受争议。像std::invalid_arguement基本没人会去用。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-05 22:40 | luckycat
@qiaojie
谢谢赐教!

"像std::invalid_arguement基本没人会去用"这句话说得有点绝对了,用的人应该还是有一些的,可能我们没有接触到.
另外,我们总是被"天朝"代表,想不到这次被 qiaojie 代表了:)

你说"保证参数的正确性是调用者的责任,而不是被调用的函数的责任",
这一点我也同意,不过我觉得作为函数的设计者,我们不应当对用户所传递的参数有太多理想化的假设,所以我们应当在函数中进行参数合法性的检查,
一方面,可以在函数的入口处尽早发现非法参数的问题,这样就不至于后续会使用错误的参数在函数中进行一些无意义的操作.
另一方面,在函数入口处检查参数的合法性,可以增强函数的健壮性,进一步增强系统的健壮性.
举个例子,如果传递给函数的实参不应该是NULL指针,用户却以NULL作为实参调用函数,假设我们没有进行对应参数合法性检查,
那么后续基于这个NULL实参的操作可能会导致系统"coredump".

对于参数的合法性检查,在debug版本和release版本下应该都需要进行,类似于"assert(0 < age && age < 200);"这种检测参数的合法性的代码只在debug版本下可以起作用,
在release版本下就不起用了,也就不能在release版本下作为参数合法性检查的工具.
在debug版本下,如果assert断言失败,那么我们可以看到对应的abort信息,然后程序异常退出.
实际上这样做可能有的时候并不合适,因为在一些情况下,仅仅是参数非法,我们可以进行相应的处理而不需要系统因此而退出运行.

"强调不要用异常或者错误返回值,是因为盲目的大量使用这类错误处理机制会导致整个项目变得混乱"
这句话如果仅仅是理论上来探讨"如何让系统设计的更优雅",那么这无疑可以作为一个"系统设计准则",
但是在实际的开发过程中,有的时候一个函数内部出现"非正常情况"的可能性实在是太多了,我们必须要进行相应的处理.
如果我们既不使用"异常"也不使用"返回错误码"的形式来告知调用者,
那么在反馈给调用者"函数内部出现非正常情况"这一点上我们将"无能为力",但我们又必须在这一点有所作为.

在大多数情况下,"异常"和"错误码"可能是我们仅有的两个选择方案,如何选择其一作为最终的处理方案,
甚至如何在不使用"异常"和"错误码"的前提下也达到相同的效果,这是一件很"纠结"的事情.


追求系统在架构和代码设计上的完美是开发者的一个方向,但是有时我们需要考虑"追求完美的代价",
在时间,人力以及成本的多重影响下,很多时候我们必须放弃对最优方案的探索,而选择一种"不那么完美但是可行,可以很好解决问题"的方案.
也许这个时候作为函数调用状态反馈的"异常"和"错误码"机制会在我们的思考和运用范围之内.


  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-06 08:55 | chentan
qiaojie 讨论很精彩
我的习惯是 模块边界会检查参数合法性,并报告给调用者
模块内部的代码尽量多的用assert  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-06 21:06 | 陈梓瀚(vczh)
对于类库的设计者,其实有一个很简单的判断标准。

1:如果一个输入错误,调用者必须知道并处理,那么就采用“不处理就崩溃”方法,迫使调用者处理。在C++里面的唯一方法就是使用异常了。举个例子,对一个容器的下标访问越界,这个时候operator[]不可能返回一个可用的返回值,而且也不能用错误信息去污染返回值,譬如返回pair<bool, T>,因此直接抛出异常。

2:如果一个构造函数发生错误之后,这个对象是绝对不能被碰的,那么采用异常。因为在try-catch结构里面,构造函数完蛋了,那么那个正在被构造的对象你是没有办法获取的。

3:异常可以用来在大量函数互相递归的时候(譬如说语法分析器)迅速跳到最外层,此处作为控制代码流程而使用。

这里我也接受一个例外,譬如说2,如果构造函数发生错误但是我并不想立刻抛出异常(因为有些状态在构造函数里面想知道也比较麻烦),那么除了需要一个类似bool IsAvailable()const函数来告诉你以外,所有该类成员的非法操作(譬如说在错误的构造状态下调用一个成员函数)都必须抛出异常,或者【绝对不产生任何副作用】的同时给一个返回值说明调用失败。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-06 21:27 | luckycat
@陈梓瀚(vczh)
你列举的判断标准都值得借鉴,不过你后续补充的对"例外情况"的处理方式不敢苟同:
因为构造可能没有成功,那么我们需要调用IsAvailable之类的函数,甚至于后续需要因为判断之前构造函数的状态来
对调用的每个成员函数进行"try catch"或者还要从"每个成员函数的返回值中来判断之前的构造操作是否成功".
这种设计是可行的,但对类的使用者来说太复杂.
这种情况下我觉得使用"两段构造"可能更好一些,我们只需要判断"两段构造"是否成功即可,如果构造成功,在后续的成员函数调用过程中,
就再也不用为了确认构造函数的状态来对每个被调用的成员函数进行"try catch"或检查返回值的操作,这样的设计应该更简洁一些.  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2010-03-08 01:28 | 陈梓瀚(vczh)
@luckycat
两段构造的话,你的类的其他成员函数也应该在构造不成功的时候抛出异常,并没有任何区别,我的例外还减少了别人可能忘记第二段构造的概率。

当然这是对一个库的要求,应用程序不一定要如此严格。  回复  更多评论
  
# re: 设计的两难:选择异常还是两段构造 2015-01-27 23:09 | tsgsz
抛异常不析构可以令所有的成员变量都是 std::unique_ptr  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理