怎样在 C++ 中计算对象的实例化次数

来源:互联网 发布:中邮网络培训学院入口 编辑:程序博客网 时间:2024/04/27 18:16
                                                                         Objects Counting in C++

  在 C++ 中,对某个 class 所产生出来的 objects保持正确的计数,是办得到的,除非你面对一些疯狂份子。
译文保留原作时态,并未修改。以下是译文所采用的几个特别请读者注意的术语:
client :客端。
type:型别。为避免和其他近似术语混淆,本文译为「型别」而非「类型」。
instantiated:具现化。「针对一个 template,具体实现出一份实体」的意思。
instance:实体。「案例」是绝对错误的译法。
parameter:叁数。或称型式叁数,形叁。
argument:引数。或称实质叁数,实叁。

至於 class, object, data member, member function, constructor, destructor, template 等术语,皆保留不译。

有时候,容易的事情虽然容易,但它们还是隐藏着某种微妙。举个例子,假设你有个 class名为 Widget,你希望有某种办法找出程式执行期间究竟存在着多少个 Widget objects。方法之一(不但容易实作而且答案正确)就是为 Widget 准备一个 static 计数器,每当Widget constructor 被呼叫,就将该计数器加一,每当Widget destructor 被呼叫,就将该计数器减一。此外你还需要一个 static 成员函式 howMany( ),用来回报目前存在多少个 Widget objects。如果 Widget 什麽都没做,单单只是追踪其objects个数,那麽它看起来大约像这样:

class Widget {
public:
    Widget() { ++count; }
    Widget(const Widget&) { ++count; }
    ~Widget() { --count; }

    static size_t howMany()
    { return count; }

private:
    static size_t count;
};

// count 的定义。这应该放在一个实作档中。
size_t Widget::count = 0;


这可以有效运作。可别忘了实作出 copy constructor,因为编译器自动为 Widget 产生的那个 copy constructor 不会知道将 count 加一。

如果你只需要为 Widget 做计数工作,你已经完成了你的任务。但有时候你得为许多   classes 做相同的计数工作。一再进行重复的工作会令人沉闷而生厌,而沉闷与厌恶感会导致错误的发生。为了阻止这种局面,最好能够将上述的「物件计数」(object-counting)程式码包装起来,使它能够被任何 class重复运用。理想的包装应该符合以下条件:
容易使用 ─ 让那些需要此等服务的 class 设计者只需最小量的工作。最理想的情况是,他们不必做任何事情,只需宣称『我要对这种型别的 objects 进行计量』就好。
有效率 ─ 使用者不需被课徵任何非必要的空间税或时间税。
绝对安全 ─ 不可能突然导致一个错误计量。(我们不打算理会那些蓄意破坏的恶意使用者,他们会刻意试着混淆计量。在 C++ 中,诸如此类的使用者总是能找到办法来满足他们卑鄙而居心不良的行为)

暂停一下,想一想,你如何实作出一个可复用的「物件计数」套件,并满足以上所有目标。这或许比你所预想的还要困难。如果它真的如你所想像的那麽容易,你就不会在这本刊物上看到这篇文章了。
new, delete, 和 Exceptions

虽然你正全心全意地解决「物件计数」相关问题,请允许我把焦点暂时切换到一个似乎无关的主题上。这个主题就是「当 constructors 丢出异常,new 和 delete 之间的关系」。当你要求 C++ 动态配置一个 object,你会像这样地使用 new 运算式:

class ABCD { ... }; // ABCD = "A Big Complex Datatype"
ABCD *p = new ABCD; // 这是一个 new 运算式

这个 new 运算式的意义在语言层面已经确定,其行为无论如何无法被改变。它做两件事情。第一,它呼叫一个名为 operator new的记忆体配置函式。这个函式的责任是找出足够置放一个 ABCD object 的记忆体。如果配置成功,new 运算式接下来就唤起一个 ABCD constructor,在 operator new 找出的那块空间上建立一个 ABCD object。

但是,假设 operator new 丢出一个 std::bad_alloc 异常,会怎样?这种异常表示,动态配置记忆体的任务失败了。在上述的 new 运算式中,有两个函式可能发出这样的异常。第一个是 operator new,它企图找出足够的记忆体来放置一个 ABCD object。第二个是接下来执行的 ABCD constructor,它企图把生鲜记忆体转换为一个有效的 ABCD object。

如果异常来自 operator new,表示没有任何记忆体被配置出来。然而如果 operator new 成功而 ABCD constructor 发出异常,很重要的一件事就是将 operator new 所配置的记忆体释放掉。如果不这样,程式就会发生记忆体遗失(memory leak)。客端(也就是要求产生一个 ABCD object 的那段程式码)不可能知道到底哪一个函式发出异常。

多年以来,这是 C++ 标准草案中的一个漏洞,1995 年三月,C++ 标准委员会采纳了一个提议:如果在 new 运算式动作期间,operator new 配置记忆体成功而後继的 constructor 丢出异常,执行期系统(runtime system)就必须自动释放被 operator new 配置出来的记忆体。这个释放动作由 operator delete 执行,此函式类似 operator new。(详见本文最後方块栏目内的「placement new 和 placement delete」。)

这个「new 运算式和 operator delete 之间的关系」,会在我们企图将「物件计数机制」自动化的过程中,带来影响。
计算物件个数(Counting Objects)

有一种物件计数问题的解法,涉及发展出一个「物件计数专用」的 class。这个 class 看起来或许很像(甚至完全像)稍早展示的 Widget class:

// 稍後有一些讨论,告诉你为什麽这样的设计并不很正确

class Counter {  
public:          
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }
    static size_t howMany()
        { return count; }

private:
    static size_t count;
};

// 以下这行仍然应该放在一个实作档中。
size_t Counter::count = 0;

这里的想法是,任何 classes 如果需要记录当下存在的物件个数,只需使用 Counter来担任簿记工作即可。有两个明显的方法可以完成这项任务,其中之一是定义一个 Counter object,使它成为一个 class data member,像这样:

// 在需要计数的 class 中内嵌一个 Counter object。
class Widget {
public:
    .....  // Widget 该有的所有 public 成员,
           // 都放在这里。
    static size_t howMany()
    { return Counter::howMany(); }
private:
    .....  // Widget 该有的所有 private 成员,
           // 都放在这里。
    Counter c;
};

另一个方法是把 Counter 当做 base class,像这样:

// 让需要计数的 class 继承自 Counter
class Widget: public Counter {
    .....  // Widget 该有的所有 public 成员,
           // 都放在这里。
private:
    .....  // Widget 该有的所有 private 成员,
           // 都放在这里。
};

两种作法各有优劣。验证它们之前,我们必须注意,没有一个方法以其目前型式可以有效运作。问题在於 Counter 中的静态物件 count。这样的静态物件只有一个,但我们却需要为每一个使用Counter 的 class 准备一个。举个例子,如果我们打算对 Widgets 和 ABCDs 计数,我们需要两个 static size_t objects,而不是一个。让 Counter::count 成为 nonstatic,并不能解决这个问题,因为我们需要的是为每个 class 准备一个计数器,而不是为每个 object 准备一个计数器。

运用 C++ 中最广为人知但名称十分诡异的一个技俩,我们就可以取得我们想要的行为:我们可以把 Counter 放进一个 template 中,然後让每一个想要使用 Counter 的 class,「以自己为 template 引数」具现出这个 template。

让我再说一次。Counter 变成一个 template:

template<typename T>
class Counter {
public:
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }

    static size_t howMany()
    { return count; }

private:
    static size_t count;
};

template<typename T>
size_t
Counter<T>::count = 0; // 现在这一行可以放进表头档中了。

於是前述的第一种实作法改变为这样:

// 在需要计数的 class T 中内嵌一个 Counter<T> object。
class Widget {
public:
    .....
    static size_t howMany()
    {return Counter<Widget>::howMany();}
private:
    .....
    Counter<Widget> c;
};

第二种作法(继承法)则改变为这样:

// 让需要计数的 class T 继承自 Counter<T>
class Widget: public Counter<Widget> {    
    .....
};

注意在这两个情况中,我们是如何地将 Counter 取代为 Counter<Widget>。一如我稍早所说,每一个使用 Counter 的 class,都以它自己为引数,具现出那个 template。

「在一个 class 中,以自己为 template 引数,具现出一个 template,给自己使用」,这种策略最早系由 Jim Coplien 公开。他以多种语言(不只 C++)展示这种策略,并称此为一个「curiously recurring(诡异而循环的)template pattern」[注1]。我不认为 Jim 故意这麽命名,不过,他对此一  pattern(样式、模式) 的描述的确比他所取的名称好得多。那可真糟糕,因为 pattern 的名称很重要,而你现在所看到的这一个名称,无法涵盖「做了什麽,如何做出」等讯息。

patterns 的命名是一种艺术,我对此并不擅长。不过我或许会把这个 pattern 称为诸如「Do It For Me」这类名称。根本上,每一个「被 Counter 产生出来的 class」,都能够针对「将 Counter 具现化」的那个 class,提供一种服务,计算现有多少个 objects。所以,class Counter<Widget> 可以计算 Widget 的物件数量,class Counter<ABCD> 可以计算 ABCD 的物件数量。

现在,Counter 成了一个 template,不论内嵌式设计或继承式设计,都可以运作,所以我们接下来面临的是,评估其相对的强度和弱点。我们的一个设计标准是,对使用者而言,「物件计数」机能应该很容易获得。上述程式码很清楚地告诉我们,继承式设计比内嵌式设计容易,因为前者只要求提及「Counter 是一个 base class」,後者却要求必须定义一个 Counter data member,并要求使用者实作出一个 howMany( ) 以唤起 Counter 的 howMany( ) [注2]。虽然这样的工作也不是太多(客端的 howMany( ) 只需是个简单的 inline 函式),但只做一件事终究比做两件事容易些。所以让我们先把注意力放在继承式设计上。
使用 Public Inheritance(公开继承)

上述的继承式设计之所以能够运作,是因为 C++ 保证,每当一个 derived class object 被建构(或解构)时,其中的 base class 成份会先被建构(或後被解构)。让 Counter 成为一个 base class,便能确保:针对继承自 Counter 的 class,每当有一个 object 被产生或被摧毁,一定会有一个 Counter constructor 或 destructor 被唤起。

然而,任何时候只要牵涉 base classes 这个主题,就不要忘记 virtual destructors。Counter 应该有这样一个东西吗?良好的 C++ 物件导向设计规范说,是的,它应该有一个 virtual destructor。如果它没有,那麽当我们透过一个 base class pointer来删除一个 derived class object 时,会导致不可预期(未有定义)的结果,而且通常是不受欢迎的:

class Widget: public Counter<Widget>
{ ... };
Counter<Widget> *pw =
    new Widget;  // 获得一个 base class pointer,
                 // 指向一个 derived class object。    
......
delete pw; // 此将导致未知的结果 ─ 如果 base class
           // 缺乏一个 virtual destructor。

如此的行为将违反我们的需求条件:由於上述程式码没有任何不合理之处,我们的「物件计数」设计理应有绝对安全的表现。因此,这是 Counter 应该有个 virtual destructor 的强烈理由。

另一个需求条件是最佳效率(也就是不因「物件计数」而被课徵任何非必要的速度税和空间税),但我们在这里遇到一点麻烦。因为,virtual destructor(或任何虚拟函式)的出现,意味每一个 Counter(或其衍生类别)的 objects 都必须内含一个(隐藏的)虚拟指标,而这会增加物件的大小 ─ 如果它们原本并没有虚拟函式的话 [注3]。也就是说,如果 Widget 本身并无任何虚拟函式,型别为 Widget 的物件将会因为继承了 Counter<Widget> 而使大小扩张。我们不希望看到这种情况。

唯一的避免之道就是,找出一种方法,阻止客端「透过一个 base class pointer 删除一个 derived class object」。将 Counter 中的  operator delete 宣告为 private,似乎是一个合情合理的办法:

template<typename T>
class Counter {
public:
    .....
private:
    void operator delete(void*);
    .....
};

但如此一来,delete 运算式无法编译成功:

class Widget: public Counter<Widget> { ... };
Counter<Widget> *pw = new Widget;  ......
delete pw; // 错误。因为我们无法唤起 private operator delete

真是不幸。不过,真正有趣的是,new 运算式也不应该通过编译:

Counter<Widget> *pw =
    new Widget;  // 这一行应该无法通过编译,
                 // 因为 operator delete 是 private

请回忆一下稍早我对於 new, delete, exceptions(异常)的讨论,我说 C++ 的执行期系统(runtime system)有责任释放被 operator new 配置的记忆体 ─ 如果後继被呼叫的 constructor 失败的话。同时也请回忆一下,operator delete 是用来执行记忆体释放动作的函式。由於我们将 Counter 的 operator delete 宣告为 private,这会使得「藉由 new,将 objects 产生於 heap」的企图永远失败。

是的,这是违反直觉的,如果你的编译器不支援它,请不要惊讶。但是请注意,我所描述的行为是正确的。除此之外再无其他明显方法能够阻止「透过 Counter* pointer 删除 derived class objects」。由於我们已经拒绝「在 Counter 中设置一个 virtual destructor」的想法(它会引起非必要的空间税),所以我说,让我们放弃这个设计吧,让我们把注意力放在「使用一个 Counter data member」上面。
使用一个 Data Member

我们已经看过了「设置一个 Counter data member」这种设计所带来的缺点:客端必须同时定义一个 Counter data member 并撰写一个 inline 版的 howMany( ),用来呼叫 Counter 的 howMany( ) 函式。这些工作比我们希望加诸於客端程式身上的,多了一些,但它还不至於难以控制或管理。但是,除此之外还有另一个缺点:为某个 class增加一个 Counter data member,往往会扩张其 objects 的大小。

这几乎谈不上是什麽重大的启示。毕竟,增加一个 data member 而导致 objects 的大小增加,会令你惊讶吗?但是再看一眼,再想一想,请注意 Counter 的定义:

template<typename T>
class Counter {
public:
    Counter();
    Counter(const Counter&);
    ~Counter();

    static size_t howMany();
private:
    static size_t count;
};

请注意它并没有 nonstatic data members,意味每一个型别为  Counter 的 object 其实没有内含任何东西。也许我们会以为每一个型别为 Counter 的 object,大小为 0?也许吧,但那并不正确。在这一点上,C++ 的表现相当清楚。所有 objects 都有至少 1 byte 的大小,甚至即使这些 objects 没有任何 nonstatic data members。根据这样的定义,对於具现自 Counter template 的每一个 class,sizeof 会获得某个正值。所以每一个「内含一个 Counter object」的 class 将比「不内含 Counter object」者拥有更多资料。

(有趣的是,这并不意味一个「不含 Counter」的 class,其大小就一定比「内含一个 Counter」的兄弟有更大的体积。那是因为边界排列限制(alignment restrictions)可能会造成影响。举个例子,如果 class Widget 内含两个 bytes 的资料,但系统要求必须以 4-byte 来进行边界排列,所以每一个 Widget object 将内含两个 bytes 的补白,而 sizeof(Widget) 的结果为 4。如果,就像普遍的情况那样,编译器都满足「任何物件的大小不可能为 0」这一条件,於是将一个 char 安插到 Counter<Widget> 内,那麽 sizeof(Widget) 还是传回 4,纵使 Widget 内含一个 Counter<Widget> object 亦然。那个被含入的 Counter<Widget> object 仅仅取代了原本被补白的两个 bytes 中的一个。不过,这并不是常见情节,因此我们当然不能够在设计一个「物件计数」套件时,把它放进计划内。)

我在耶诞假期开始的时候,动笔写这篇文章(正确日期是感恩节当天,这也许能够让你了解,我是如何地庆祝这个重要的节日...),现在我的情绪已经很不好了。我要做的只不过是物件计数,我再也不想要东拉西扯什麽奇怪而额外的讨论了。
使用 Private Inheritance(私有继承)

再一次看看继承式设计的代码,那导致我们必须为 Counter 考虑一个 virtual destructor:

class Widget: public Counter<Widget>
{ ... };
Counter<Widget> *pw = new Widget;            
......
delete
pw;  // 导致未定义的(未知的)结果 ─
     // 如果 Counter 缺乏一个 virtual destructor。

稍早我们曾经试着藉由「阻止 delete 运算式顺利编译」而阻止这一系列动作,但是我们发现,那同时也阻止了 new 运算式的顺利编译。除此之外,还有其他某些东西也是我们可以禁止的。我们可以禁止一个 Widget* pointer(这是 new 的回传值)被隐式转型为一个 Counter<Widget>* pointer。换句话说,我们可以阻止继承体系中的指标型别转换。我们唯一需要做的就是将「public 继承」改为「private 继承」:

class Widget: private Counter<Widget>
{ ... };
Counter<Widget> *pw =
    new Widget;  // 错误! 没有隐式转换函式(implicit conversion)可以
                 // 将 Widget* 转为 Counter<Widget>*

此外,我们很开心地发现,以 Counter 做为 base class,并不会增加 Widget 的大小 ─ 如果和 Widget 独立个体的大小相比的话。是的,我知道我才刚刚告诉过你,没有任何 class 的大小为 0,但是 ─ 唔,那并不是我真正的意思。 我的真正意思是,没有任何一个 objects 的大小为 0。C++ 标准规格说得很清楚,一个 derived object 之中的「base-class 成份」的大小可以是 0。事实上,许多编译器都发展出所谓的「空白基础类别最佳化技术」(empty base optimization) [注4]。

因此,如果一个 Widget 内含一个 Counter,Widget 的大小一定会增加。因为 Counter 的 data member 完全属於自己,而不是别人的base-class 成份,因此它必须有非零大小。但如果 Widget 继承自 Counter,编译器便得以将 Widget 的大小保持在原先状态。这个事实为那些「记忆体使用状态非常紧绷而类别设计中涉及空白基础类别」的设计,提出了一个有趣的规则:当「private 继承」和「复合技术(containment, composition)」都能完成相同目的时,尽量选用「private 继承」。(译注:这一点乍见之下和 Scott Meyers 的《Effective C++ 2/e》条款42有所抵触。该条款最後一段建议大家,如果「private 继承」和「复合技术」都能完成相同目的,尽量选用复合技术。然而请你注意,本文所给的这个建议是有前提的。)

最後的设计几近完美。它实践了效率的要求,前提是你的编译器具有「空白基础类别最佳化」(empty base optimization)的能力,如此一来「继承自 Counter」这一事实才不会增加下层类别的物件大小。此外,所有的 Counter member functions 都必须是 inlin 函式。这样的设计也实践了安全需求,因为计数动作是由 Counter member functions 自动处理,那些函式会自动被 C++ 呼叫,而 private 继承机制的使用则阻止了隐式转型 ─ 「隐式转型」允许 derived-class objects 被当做 base-class objects 一样地处理。(好吧,我承认,它并非绝对安全:Widget 的作者可能荒谬地以一个 Widget 以外的类别来具现化 Counter,也就是说,他可能让 Widget 继承自 Counter<Gidget>。我对这种可能性所采取的态度是:不加理会。)

这样的设计对客端而言很容易使用,但是可能有人会咕哝说,还可以更简单。使用 private 继承机制,意味 howMany( ) 会在衍生类别中成为 private,所以衍生类别中必须含入一个 using declaration,使 howMany( ) 成为 public,才能被客端所用:

class Widget: private Counter<Widget> {
public:
    // 让 howMany 成为 public
    using Counter<Widget>::howMany;

    ..... // Widget 的剩馀部份没有改变。
};

class ABCD: private Counter<ABCD> {
public:
    // 让 howMany 成为 public
    using Counter<ABCD>::howMany;

    ..... // ABCD 的剩馀部份没有改变。
};

对那些并不支援 namespaces(命名空间)的编译器而言,以上目的也可以改用旧有的存取层级来完成(但并不被鼓励):

class Widget: private Counter<Widget> {
public:
    // 让 howMany 成为 public
    Counter<Widget>::howMany;

    ..... // Widget 的剩馀部份没有改变。
};

至此,有必要执行「物件计数」的那些客端程式,以及有必要让该计数器为其客户所用(亦即成为 class 介面的一份子)的classes,必须做两件事情:将 Counter 宣告为一个 base class 并让 howMany( ) 可被取用 [注5]。

然而,继承机制的使用,会导致两个值得注意的情况。第一件事是模棱两可(ambiguity)。假设我们打算对 Widgets 计数,而我们希望让这个计数值供一般运用。一如先前所展示,我们令 Widget 继承自 Counter<Widget>,并令 Widget::howMany( ) 成为 public。 现在假设我们有一个 class SpecialWidget,以 public 方式继承自 Widget,我们希望提供给 SpecialWidget 使用者一如 Widget 使用者所能享受的机能。没问题,只需令 SpecialWidget 继承自 Counter<SpecialWidget> 即可。

但这里出现了模棱两可(ambiguity)的问题。哪一个 howMany( ) 对 SpecialWidget 而言才是可用的呢?是继承自 Widget 的那个,或是继承自 Counter<SpecialWidget> 的那个?我们所希望的,当然是来自 Counter<SpecialWidget> 的那个,但是我们没办法在未明确写出 SpecialWidget::howMany( ) 的情况下说出我们的心愿。幸运的是,它只是一个简单的 inline 函式:

class SpecialWidget: public Widget,
                  private Counter<SpecialWidget> {
public:
    .....
    static size_t howMany()
    { return Counter<SpecialWidget>::howMany(); }
    .....
};

关於「使用继承机制来完成物件计数工作」的第二个意见是,Widget::howMany( ) 传回的值不只包括 Widget objects 的个数,也包括 Widget 衍生类别所产生的 objects。如果 Widget 的唯一衍生类别是 SpecialWidget,而一共有五个 Widget 独立物件和三个 SpecialWidgets独立物件,那麽 Widget::howMany( ) 将传回 8。毕竟,每一个 SpecialWidget 的建构,也同时会完成其基础类别(Widget 成份)的建构。
摘要

以下数点是你需要记住的:
物件计数工作的自动化并不困难,但也并非直观想像中的那麽简单。运用 "Do It For Me" pattern(Coplien 所谓的 "curiously recurring template pattern")便有可能产生正确数量的计数器。运用 private 继承机制,可以提供物件计数能力,而又不扩张物件的大小。
当客端有机会选择「继承自一个 empty class」或「内含某个 class object 做为 data member」时,继承是比较好的选择,因为它允许更紧密的物件。
由於 C++ 尽一切努力要在 heap objects 建构动作失败时避免发生记忆体漏洞(memory leaks),所以凡是需要用到 operator new 之程式码,通常也需要用到对应的 operator delete。
Counter class template 并不在乎你是否继承它,或内含它的一个 object。它看起来都一样。因此,客端可以自由选择使用「继承机制」或「复合(组合)技术」,甚至在同一个应用程式或程式库的不同地点使用不同的策略。
注解与叁考资料

[1] James O. Coplien. "The Column Without a Name: A Curiously Recurring Template Pattern," C++ Report, February 1995.

[2] 另一种方法是忽略 Widget::howMany( ),让客端直接呼叫 Counter<Widget>::howMany( )。然而,对本文目的而言,我们将假设我们希望 howMany( ) 是 Widget 介面的一部份。

[3] Scott Meyers. More Effective C++ (Addison-Wesley, 1996), pp. 113-122.

[4] Nathan Myers. "The Empty Member C++ Optimization," Dr. Dobb''s Journal, August 1997。可自以下网站获得:
http://www.cantrip.org/emptyopt.html.

[5] 只要对这个设计做一点简单的变化,就可以让 Widget 以 Counter<Widget> 计算物件个数,并且不让这个计数值被 Widget 的客户所用,甚至不允许 Counter<Widget>::howMany( ) 被直接呼叫。下面这个练习留给时间充裕的读者:继续讨论更多变化。
进一步的读物

如果想要学习更多关於 new 和 delete 的细节,请阅读 Dan Saks 在 CUJ 1997年一月至七月所主持的专栏,或是我的 More Effective C++ (Addison-Wesley, 1996) 条款8。如果想要更广泛地验证物件计数(object-counting)问题,包括如何限制某个 class 被具现化的次数,请看 More Effective C++ 条款 26。
致谢

Mark Rodgers, Damien Watkins, Marco Dalla Gasperina, 和 Bobby Schmidt 针对本文草稿提出了一些意见。他们的洞见和提议,使本文在许多方面有了更好的改善。
作者

Scott Meyers 是畅销书籍 Effective C++ 第二版和 More Effective C++ 的作者(两本书都由 Addison Wesley 出版)。你可以从 http://www.aristeia.com中找到更多有关於他、他的书、他的那只狗的讯息。译注:《Effective C++》第二版以 Meyers的狗为封面。
译者

陈崴,自由撰稿人,专长 C++/Java/OOP/GP/DP。惯以热情的文字表现冰冷的技术,以冷冽的文字表现深层的关怀。

sidebar : Placement new 与 placement delete

malloc( ) 在 C++ 中的对等物是 operator new,free( ) 在 C++ 中的对等物则是 operator delete。和 malloc( ) 及 free( ) 不同的是是,operator new 和 operator delete 都可以被重载,重载後的版本可接受与母版不同个数、不同型别的叁数。这对 operator new 来说一向正确,但直到最近,才对 operator delete 也成立。

operator new 的正常标记(signature)是:

void * operator new(size_t) throw (std::bad_alloc);

(从现在起,为了简化,我将刻意忽略 exception specifications(译注:就是上述的 throw (std::bad_alloc)),因为它们和我目前要说的重点没有什麽密切关系。)operator new 的重载版本只能增加新叁数,所以一个 operator new 重载版本可能长这个样子:

void * operator new(size_t, void *whereToPutObject)
{ return whereToPutObject; }

这个特殊版本的 operator new 接受一个额外的 void* 引数,指出此函式应该回传什麽指标。由於这个特殊形式在 C++ 标准程式库中是如此常见而有用(宣告於表头档 <new>),因而有了一个属於自己的名称:"placement new"。这个名称表现出其目的:允许程序员指出「一个 object 应该诞生於记忆体何处」。

随着时间过去,任何「要求额外引数」的 operator new 版本,也都渐渐采用 placement new 这个术语。事实上这个术语已经被铭记於 C++ 标准规格中。因此,当 C++ 程序员谈到所谓的 placement new 函式,他们所谈的可能是上述那个「需要额外一个 void* 叁数,用以指出物件置於何处」的版本,但也可能是指那些「所需引数比单一而必要之 size_t 引数更多」的任何 operator new 版本,包括上述函式,也包括其他「引数更多」的 operator new 函式。

换句话说,当我们把焦点集中在记忆体配置时,"placement new" 意味「operator new 的某个版本,接受额外引数」。这个术语在其他场合可能有其他意义,但我们不需继续深入,所以,到此为止。如果你需要更多细节,请叁考本文最後所列的叁考读物。

和 placement new 类似,术语 "placement delete" 意味「operator delete 的某个版本,接受额外引数」。operator delete 的「正常」标记如下:

void operator delete(void*);

所以,任何版本的 operator delete,只要接受的引数多於上述的 void*,就是一个 placement delete 函式。

现在让我们重回本文所讨论的一个主题。当 heap object 在建构期间丢出一个异常,会发生什麽事?再次考虑以下这个简单例子:

class ABCD { ... };
ABCD *p = new ABCD;

假设产生 ABCD object 时导致了一个异常。前列的主文内容指出,如果异常来自 ABCD 建构式,operator delete 会自动被唤起,释放 operator new 所配置的记忆体。但如果 operator new 被多载化,情况将如何?如果不同版本的 operator new 以不同的方式配置记忆体,情况又将如何?operator delete 如何知道该怎麽做才能正确释放记忆体?此外,如果 ABCD object 系以 placement new 产生出来(像下面这样),又该如何:

void *objectBuffer = getPointerToStaticBuffer();
ABCD *p = new (objectBuffer) ABCD; // 在一个静态缓冲区中产生一个 ABCD object

上述那个 placement new 并不配置任何记忆体。它只是传回一个指标,指向那个它所接受的静态缓冲区。也因此,不需要任何释放动作。

很显然,operator delete 所采取的行动(用以回复其对应之 operator new 的行为)必须视配置记忆体时所采用的 operator new 版本而定。

为了让程式员有机会指示「如何回复某个特殊版本之 operator new 的行为」,C++ 标准委员会扩展了 C++,允许 operator delete 也能够被多载化。当 heap object 的 constructor 丢出一个异常,整个游戏便改走另一条路,呼叫起特殊的 operator delete 版本,此一版本带有额外叁数型别,这些型别将对应於先前被唤起之 operator new 版本。

如果没有任何一个版本的 placement delete 的额外叁数能够对应於「被唤起之 placement new 的额外叁数」,那麽,就不会有任何 operator delete 被唤起。於是,operator new 的行为所带来的影响就无法被抹除。对於那些「placement 版」的 operator new,这没问题,因为它们并不真正配置记忆体。然而,一般而言,如果你产生一个自定的「placement 版」的 operator new,你也应该产生一个对应的自定的「placement 版」operator delete。

啊呀,大部份编译器都还没有支援 placement delete。这种编译器所产生出来的程式码,使你几乎总得蒙受一个记忆体漏洞(memory leak)。如果在 heap object 建构期间有一个异常被丢出,因为不会有任何人企图释放 constructor 被唤起之前被配置的记忆体。

译注:根据我的测试,GNU C++ 2.9, Borland C++Builder 40, Microsoft Visual C++ 6.0三家编译器都已经支援 placement 版本的operator delete。其中以 Visual C++ 最为体贴,当「没有任何一个版本的 placement delete 的额外叁数能够对应於被唤起之 placement new 的额外叁数」时,Visual C++ 会给你一个警告讯息:
no matching operator delete found; memory will not be freed if initialization throws an exception. 
原创粉丝点击