条款 4:非必要不提供 default constructor
所谓 default constructor(也就是说不给任何自变量就可调用者)是 C++一种“无中生有”的方式。Constructors 用来将对象初始化,所以 default constructors 的意思是在没有任何外来信息的情况将对象初始化。有时候可以想象,例如,数值之类的对象,可以被合理地初始化为 0 或一个无意义值。其他诸如指针之类的对象(条款 28)亦可被合理地初始化为 null 或无意义值。数据结构如 linked lists,hash tables,maps等,可被初始化为空容器。
但是并非所有对象都落入这样的分类。有许多对象,如果没有外来信息,就没办法执行一个完全的初始化动作。例如,一个用来表现通信簿字段的 class,如果没有获得外界指定的人名,产生出来的对象将毫无意义。在某些公司,所有仪器设备都必须贴上一个识别号码。为这种用途(用以模拟出仪器设备)而产生的对象,如果其中没有供应适当的 ID 号码,将毫无意义。
在一个完美的世界中,凡可以“合理地从无到有生成对象”的 classes,都应该内含 default constructors,而“必须有某些外来信息才能生成对象”的 classes,则不必拥有 default constructors。但我们的世界毕竟不是完美的世界,所以我们必须纳入其他考虑。更明确地说,如果 class 缺乏一个 default constructor,当你使用这个class 时便会有某些限制。
考虑下面这个针对公司仪器而设计的 class,在其中,仪器识别码是一定得有的
一个 constructor 自变量:
由于 EquipmentPiece 缺乏 default constructor,其运行可能在 3种情况下出现问题。第一个情况是在产生数组的时候。一般而言没有任何方法可以为数组中的对象指定 constructor 自变量,所以几乎不可能产生一个由 EquipmentPiece objects 构成的数组:
有 3 个方法可以侧面解决这个束缚。第一个方法是使用 non-heap 数组,于是便能够在定义数组时提供必要的自变量:
不幸的是此法无法延伸至 heap 数组。
更一般化的做法是使用“指针数组”而非“对象数组”:
此法有两个缺点。第一,你必须记得将此数组所指的所有对象删除。如果你忘了,就会出现 resource leak(资源泄漏)问题;第二,你需要的内存总量比较大,因为你需要一些空间用来放置指针,还需要一些空间用来放置 EquipmentPiece objects。
“过度使用内存”这个问题可以避免,方法是先为此数组分配 raw memory,然后使用“placement new”(见条款 8)在这块内存上构造 EquipmentPiece objects。//分配足够的 raw memory,给一个预备容纳 10 个 EquipmentPiece
//objects 的数组使用。条款 8对于 operator new[] 有详细说明。
//让 bestPieces 指向此块内存,使这块内存
//被视为一个 EquipmentPiece 数组。
//利用“placement new”(见条款 8)构造这块
//内存中的 EquipmentPiece objects。
注意,你还是必须供给 constructor 一个自变量,作为每个 EquipmentPiece objects 的初值。这项技术(以及“由指针构成数组”的主意)允许你在“缺乏 default constructor”的情况下仍能产生对象数组;但并不意味你可以因此回避供给constructor 自变量。噢,那是不可能的。如果可能,constructors 的目标便受到了严厉的挫败,因为 constructors 保证对象会被初始化。
placement new 的缺点是,大部分程序员不怎么熟悉它,维护起来比较困难。此外,你还得在数组内的对象结束生命时,以手动方式调用其 destructors,最后还得以调用 operator delete[](见条款 8)的方式释放 raw memory:
//将 bestPieces 中的各个对象,以其构造顺序的相反顺序析构掉。
//释放 raw memory。
如果你对 rawMemory 采用一般的数组删除语法,程序行为将不可预期。因为,删除一个不以 new operator 获得的指针,其结果没有定义:
关于 new operator 和 placement new,以及它们如何与 constructors 和destructors 互动,细节请见条款 8。
Classes 如果缺乏 default constructors,带来的第二个缺点是:它们将不适用于许多 template-based container classes。对那些 templates 而言,被实例化(instantiated)的“目标类型”必须得有一个 default constructors。这是一个普遍的共同需求,因为在那些 templates 内几乎总是会产生一个以“template 类型参数”作为类型而架构起来的数组。例如,一个为 Array class 而写的 template 可能看起来像这样:
大部分情况下,如果谨慎设计 template,可以消除对 default constructor 的需求。例如,标准的 vector template(会产生出行为类似“可扩展数组”的各种 classes),就不要求其类型参数拥有一个 default constructor。不幸的是许多 templates 的设计什么都有,独缺谨慎。因此缺乏 default constructors 的 classes 将不兼容于许多(不够严谨的)templates。当 C++程序员学到更多的 template 设计技术与观念后,这个问题的重要性应该会降低。至于这一天什么时候才会到来,嗯,每个人猜测的都不一样。
到底“要还是不要”提供一个 default constructor 呢?就像哈姆雷特的难题一样,to be or not to be?在进退维谷的情况下,最后一个考虑点和 virtual base classes (见条款 E43)有关。Virtual base classes 如果缺乏 default constructors,与之合作将是一种刑罚。因为 virtual base class constructors 的自变量必须由欲产生的对象的派生层次最深(所谓 most derived)的 class 提供。于是,一个缺乏 default constructor的 virtual base class,要求其所有的 derived classes——不论距离多么遥远——都必须知道、了解其意义,并且提供 virtual base class 的 constructors 自变量。derived classes 的设计者既不期望也不欣赏这样的要求。
由于“缺乏 default constructors”带来诸多束缚,有些人便认为所有 classes 都应该提供 default constructors——甚至即使其 default constructor 没有足够信息将对象做完整的初始化。依照这样的哲学,EquipmentPiece 可能会被修改如下:
这几乎总是会造成 class 内的其他 member functions 变得复杂,因为这便不再保证一个 EquipmentPiece object 的所有字段都是富有意义的初值。如果“一个无ID 值的 EquipmentPiece object”竟然是可以生存的,大部分 member functions 便必须检查 ID 是否存在,否则就会出现大问题。通常,这部分的实现策略并不明朗,许多编译器选择的解决办法是:什么都不做,仅提供便利性,它们抛出一个exception,或是调用某函数将程序结束掉。这样的事情一旦发生,我们实在很难认为软件的整体质量会因为“为一个不需要 default constructor 的 class 画蛇添足地加上一个 default constructor”而获得提升。
添加无意义的 default constructors,也会影响 classes 的效率。如果 member functions 必须测试字段是否真被初始化了,其调用者便必须为测试行为付出时间代价,并为测试代码付出空间代价,因为可执行文件和程序库都变大了。万一测试结果为否定,对应的处理程序又需要一些空间代价。如果 class constructors 可以确保对象的所有字段都会被正确地初始化,上述所有成本便都可以免除。如果 default constructors 无法提供这种保证,那么最好避免让 default constructors 出现。虽然这可能会对 classes 的使用方式带来某种限制,但同时也带来一种保证:当你真的使用了这样的 classes,你可以预期它们所产生的对象会被完全地初始化,实现上亦富有效率。
[1] dynamic_cast 的第二个(与第一个不相干)用途是找出被某对象占用的内存的起始点。我将在条款 27解释这项能力。