条款26:限制某个类所能产生的对象数量(下)
允许对象来去自由
我们知道如何设计只允许建立一个实例的类,我们知道跟踪特定类的对象数量的工作是复杂的,因为在三种不同的环境中都可能调用对象的构造函数,我们知道消除对象计数中混乱现象的方法是把构造函数声明为private。还有最后一点值得我们注意:使用thePrinter函数封装对单个对象的访问,以便把Printer对象的数量限制为一个,这样做的同时也会让我们在每一次运行程序时只能使用一个Printer对象。导致我们不能这样编写代码:
建立 Printer 对象 p1;
使用 p1;
释放 p1;
建立Printer对象p2;
使用 p2;
释放 p2;
...
这种设计在同一时间里没有实例化多个Printer对象,而是在程序的不同部分使用了不同的Printer对象。不允许这样编写有些不合理。毕竟我们没有违反只能存在一个printer的约束。就没有办法使它合法化么?
当然有。我们必须把先前使用的对象计数的代码与刚才看到的伪构造函数代码合并在一起:
class Printer {
public:
class TooManyObjects{};
// 伪构造函数
static Printer * makePrinter();
~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...
private:
static size_t numObjects;
Printer();
Printer(const Printer& rhs); //我们不定义这个函数
}; //因为不允许
//进行拷贝
// (参见Effective C++条款27)
// Obligatory definition of class static
size_t Printer::numObjects = 0;
Printer::Printer()
{
if (numObjects >= 1) {
throw TooManyObjects();
}
继续运行正常的构造函数;
++numObjects;
}
Printer * Printer::makePrinter()
{ return new Printer; }
当需要的对象过多时,会抛出异常,如果你认为这种方式给你的感觉是unreasonably harsh,你可以让伪构造函数返回一个空指针。当然客户端在使用之前应该进行检测。
除了客户端必须调用伪构造函数,而不是真正的构造函数之外,它们使用Printer类就象使用其他类一样:
Printer p1; // 错误! 缺省构造函数是
// private
Printer *p2 =
Printer::makePrinter(); // 正确, 间接调用
// 缺省构造函数
Printer p3 = *p2; // 错误! 拷贝构造函数是
// private
p2->performSelfTest(); // 所有其它的函数都可以
p2->reset(); // 正常调用
...
delete p2; // 避免内存泄漏,如果
// p2 是一个 auto_ptr,
// 就不需要这步。
这种技术很容易推广到限制对象为任何数量上。我们只需把hard-wired常量值1改为根据某个类而确定的数量,然后消除拷贝对象的约束。例如,下面这个经过修改的Printer类的代码实现,最多允许10个Printer对象存在:
class Printer {
public:
class TooManyObjects{};
// 伪构造函数
static Printer * makePrinter();
static Printer * makePrinter(const Printer& rhs);
...
private:
static size_t numObjects;
static const size_t maxObjects = 10; // 见下面解释
Printer();
Printer(const Printer& rhs);
};
// Obligatory definitions of class statics
size_t Printer::numObjects = 0;
const size_t Printer::maxObjects;
Printer::Printer()
{
if (numObjects >= maxObjects) {
throw TooManyObjects();
}
...
}
Printer::Printer(const Printer& rhs)
{
if (numObjects >= maxObjects) {
throw TooManyObjects();
}
...
}
Printer * Printer::makePrinter()
{ return new Printer; }
Printer * Printer::makePrinter(const Printer& rhs)
{ return new Printer(rhs); }
如果你的编译器不能编译上述类中Printer::maxObjects的声明,这丝毫也不奇怪。特别是应该做好准备,编译器不能编译把10做为初值赋给这个变量这条语句。给static const成员(例如int, char, enum等等)确定初值的功能是最近才加入到C++中的,所以一些编译器还不允许这样编写。如果没有及时更新你的编译器,可以把maxObjects声明为在一个private内匿名枚举类型里的枚举元素,
class Printer {
private:
enum { maxObjects = 10 }; // 在类中,
... // maxObjects为常量10
};
或者象non-const static成员一样初始化static常量:
class Printer {
private:
static const size_t maxObjects; // 没有赋给初值
...
};
// 放在一个代码实现的文件中
const size_t Printer::maxObjects = 10;
后面这种方法与原来的方法有一样的效果,但是显示地确定初值能让其他程序员更容易理解。当你的编译器支持在类定义中给const static成员赋初值的功能时,你应该尽可能地利用这个功能。
一个具有对象计数功能的基类
把初始化静态成员撇在一边不说,上述的方法使用起来就像咒语一样灵验,但是另一方面它也有些繁琐。如果我们有大量像Printer需要限制实例数量的类,就必须一遍又一遍地编写一样的代码,每个类编写一次。这将会使大脑变得麻木。应该有一种方法能够自动处理这些事情。难道没有方法把实例计数的思想封装在一个类里吗?
我们很容易地能够编写一个具有实例计数功能的基类,然后让像Printer这样的类从该基类继承,而且我们能做得更好。我们使用一种方法封装全部的计数功能,不但封装维护实例计数器的函数,而且也封装实例计数器本身。(当我们在条款29中测试引用计数时,将看到需要同样的技术。有关这种设计的测试细节,参见我的文章counting objects)
Printer类的计数器是静态变量numObjects,我们应该把变量放入实例计数类中。然而也需要确保每个进行实例计数的类都有一个相互隔离的计数器。使用计数类模板可以自动生成适当数量的计数器,因为我们能让计数器成为从模板中生成的类的静态成员:
template<class BeingCounted>
class Counted {
public:
class TooManyObjects{}; // 用来抛出异常
static int objectCount() { return numObjects; }
protected:
Counted();
Counted(const Counted& rhs);
~Counted() { --numObjects; }
private:
static int numObjects;
static const size_t maxObjects;
void init(); // 避免构造函数的
}; // 代码重复
template<class BeingCounted>
Counted<BeingCounted>::Counted()
{ init(); }
template<class BeingCounted>
Counted<BeingCounted>::Counted(const Counted<BeingCounted>&)
{ init(); }
template<class BeingCounted>
void Counted<BeingCounted>::init()
{
if (numObjects >= maxObjects) throw TooManyObjects();
++numObjects;
}
从这个模板生成的类仅仅能被做为基类使用,因此构造函数和析构函数被声明为protected。注意private成员函数init用来避免两个Counted构造函数的语句重复。
现在我们能修改Printer类,这样使用Counted模板:
class Printer: private Counted<Printer> {
public:
// 伪构造函数
static Printer * makePrinter();
static Printer * makePrinter(const Printer& rhs);
~Printer();
void submitJob(const PrintJob& job);
void reset();
void performSelfTest();
...
using Counted<Printer>::objectCount; // 参见下面解释
using Counted<Printer>::TooManyObjects; // 参见下面解释
private:
Printer();
Printer(const Printer& rhs);
};
Printer使用了Counter模板来跟踪存在多少Printer对象,坦率地说,除了Printer的编写者,没有人关心这个事实。它的实现细节最好是private,这就是为什么这里使用private继承的原因(参见Effective C++条款42)。另一种方法是在Printer和counted<Printer>之间使用public继承,但是我们必须给Counted类一个虚拟析构函数。(否则如果有人通过Counted<Printer>*指针删除一个Printer对象,我们就有导致对象行为不正确的风险——参见Effective C++条款14。)条款24已经说得很明白了,在Counted中存在虚函数,几乎肯定影响从Counted继承下来的对象的大小和布局。我们不想引入这些额外的负担,所以使用private继承来避免这些负担。
Counted所做的大部分工作对于Printer的客户端来说都是隐藏的,但是这些客户端可能很想知道有当前多少Printer对象存在。Counted模板提供了objectCount函数,用来提供这种信息,但是因为我们使用private继承,这个函数在Printer类中成为了private。为了恢复该函数的public访问权,我们使用using声明:
class Printer: private Counted<Printer> {
public:
...
using Counted<Printer>::objectCount; // 让这个函数对于Printer
//是public
...
};
这样做是合乎语法规则的,但是如果你的编译器不支持命名空间,编译器就不允许这样做。如果这样的话,你应使用老式的访问权声明语法:
class Printer: private Counted<Printer> {
public:
...
Counted<Printer>::objectCount; // 让objectCount
// 在Printer中是public
...
};
这种更传统的语法与uning声明具有相同的含义。但是我们不赞成这样做。TooManyObjects类应该也应用同样的方式来处理,因为Printer的客户端如果要捕获这种异常类型,它们必须有能力访问TooManyObjects。.
当Printer继承Counted<Printer>时,它可以忘记有关对象计数的事情。编写Printer类时根本不用考虑对象计数,就好像有其他人会为它计数一样。Printer的构造函数可以是这样的:
Printer::Printer()
{
进行正常的构造函数运行
}
这里有趣的不是你所见到的东西,而是你看不到的东西。不检测对象的数量就好像限制将被超过,执行完构造函数后也不增加存在对象的数目。所有这些现在都由Counted<Printer>的构造函数来处理,因为Counted<Printer>是Printer的基类,我们知道Counted<Printer>的构造函数总在Printer的前面被调用。如果建立过多的对象,Counted<Printer>的构造函数就会抛出异常,甚至都没有调用Printer的构造函数。
最后还有一点需要注意,必须定义Counted内的静态成员。对于numObjects来说,这很容易——我们只需要在Counted的实现文件里定义它即可:
template<class BeingCounted> // 定义numObjects
int Counted<BeingCounted>::numObjects; // 自动把它初始化为0
对于maxObjects来说,则有一些技巧。我们应该把它初始化为什么值呢?如果你想允许建立10个printer对象,我们应该初始化Counted<Printer>::maxObjects为10。另一方面如果我们向允许建立16个文件描述符对象,我们应该初始化Counted<Printer>::maxObjects为16。到底应该怎么做呢?
简单的方法就是什么也不做。我们不对maxObject进行初始化。而是让此类的客户端提供合适的初始化。Printer的作者必须把这条语句加入到一个实现文件里:
const size_t Counted<Printer>::maxObjects = 10;
同样FileDescriptor的作者也得加入这条语句:
const size_t Counted<FileDescriptor>::maxObjects = 16;
如果这些作者忘了对maxObjects进行初始化,会发生什么情况呢?很简单:连接时会发生错误,因为maxObjects没有被定义。如果我们提供了充分的文档对Counted客户端说明了需求,他们会回去加上这个必须的初始化。
译者注:
translation unit - a source file presented to a complier with an object file produced as a result.
linkage - refers to whether a name is visible only inside or also outside its translation unit.