那么这样实在是太糟糕了,因为在调用Sing()和Dancing()时我们甚至不知道调用的对象的内部状态是非法的,这也就成为bug的一个源泉.
总结一下上面的例子中遇到的问题:在以特定的参数构造一个对象时,如果参数非法或构造失败,我们应当向调用者反馈这一信息.
对于一般的成员函数,如果能够有返回值,那么我们可以通过返回值来标识传递给函数的参数非法或内部运行失败这种情况.但是对于构造函数,因为它不能有返回值,所以,我们必须使用其它的方法来向调用者反馈"传递给构造函数的参数非法或构造失败".
针对于这一问题有三种解决方案:第一种方案:在构造函数的参数中传递一个额外的参数用于标识构造是否成功.在这种方案的指导下,代码如下:
然后我们可以这样使用:
这种方法是可行的,但是代码看起来过于丑陋且不够直观,这里只是作为一种方案提出并不推荐使用.第二种方案:使用两段构造的形式.所谓的两段构造是指一个对象的内部状态的初始化分两步完成,将构造函数中的部分初始化操作转移到一个辅助初始化的成员函数中:第一步是通过构造函数来完成部分内部状态的初始化.第二步是通过类似于 Initialize 之类的成员函数来完成对象内部状态的最终初始化.
两段构造的形式在 MFC 中广泛使用.在MFC中我们经常看到类似于 Initialize , Create 之类的函数.基于两段构造的形式,代码如下:
在这种情况下,我们应当这样来使用People:
这种方案似乎比第一种方案更优,但是仍有一个潜在的问题:对象是以两步构造完成的.第一步构造是由构造函数来完的,OK,这一点我们不用担心,编译器帮我们保证.但是第二步是由类似于 Initialize 之类的成员函数来完成的,如果我们在构造一个People对象之后忘记了调用 Initialize ,那么这个对象的内部状态仍然是非法的,后续的操作也将由此引发bug.这也是"两段构造"这种形式受到诟病的原因之一.另一方面,"两段构造"的形式与C++的"RAII",Resource Acquisition Is Initialization(资源获取即初始化),这一原则相违背.因为以"两段构造"这种形式设计的class People 在构造一个对象时,它的内部状态实际上并没有完全初始化,我们需要调用 Initialize 来辅助完成最终的初始化.所以,尽管"两段构造"这种方案可以解决我们所遇到的"对构造函数参数非法进行反馈"这个问题,但是这种方案并不够优雅.
但是为什么MFC会先择"两段构造"这种形式呢,因为在C++发展的初期,当异常机制还不是足够成熟,没有得到广泛的认可和使用时,MFC中选择两段构造或许也是情理之中的,也许还有其它的原因,类似的类库还有ACE...
当然,在某些情况下,使用两段构造也有其独到的好处.下面所设计的场景可能有一些牵强,但只是为了力求简单并能够说明问题.(代码进行了大量简化)
然后在我们的系统中,我们需要使用一个 server pool , 在系统启动时,我们需要 server pool 中有 100 个 Server 可用.
在系统负载最大的时候,假定100个Server可以胜任,但是在大多数情况下,我们只需要少量的Server即可以完成任务.在这种情况下: Server serverPool[ 100 ]; 将会消耗大量的资源(而且大部分资源我们并不会使用),这是我们不愿意接受的.之所以出现这种情况,因为我们在构造函数中分配了大量资源,这种分配是随构造函数的调用而自动完成的.
这时,如果我们使用"两段构造"的方法就能在一定的程度上解决这个问题.
在这种情况下: Server serverPool[ 100 ]; 的开销就很小了,我们可以很好地控制对系统资源的使用,而不会浪费.当然,当我们从 serverPool 中获取一个 Server 对象时,我们要调用 Initialize 进行最终的初始化操作.第三种方案:使用异常即是当用于构造 People 对象的参数非法时,我们选择在构造函数中抛出一个异常来反馈给调用者"参数非法,构造失败"的相关信息.
那么我们可以这样使用:
这种方案似乎是最优的:符合RAII原则,也符合B.S等一批老大推行的"现代C++程序设计风格".
但是很多在开发一线上的同学们都反对在代码中使用异常,实际上我也不愿意在代码中使用异常,至少不愿意看到类似于java代码中那样铺天盖地的"throw try catch".我对异常的使用也仅仅是局限在类似于那些适合"用异常来代替两段构造"的场景中,对于其它的情况,我更愿意用返回错误码来标识函数内部的运行状态,而不是通过抛出异常的形式来告知调用者.
C++规定:如果执行构造函数的过程中产生异常,那么这个未被成功构造的对象的析构函数将不会被调用.这一点在很大程度上为我们在构造函数中抛出异常的安全性提供了C++语言级的保证,当然,其它的安全性需要我们自己保证.对于"向调用者反馈构造函数参数非法或构造失败的相关信息"这个问题,"基于异常"和"基于两段构造"这两种方案我都使用过一段时间,目的是确定对于自己而言到底哪一种方案用起来更舒服更适合自己.最终的结果是我选择了"基于异常"这种形式.
对于"基于异常"和"基于两段构造",没有哪一种能在所有的情况下都是最优的解决方案,印证了那句名言"there is no silver bullet".如何在不同的场景中选择其一作为最优的解决方案是我们在设计时需要权衡的问题.
个人愚见,错漏之处还请指正,欢迎大家踊跃发言:)