- 条款25:将constructor和non-member functions虚化
- 条款26:限制某个class所能产生的对象数量
- 条款27:要求(或禁止)对象产生于heap之中
- 条款28:Smart Pointers(智能指针)
- 条款29:Reference counting(引用计数)
- 条款30:Proxy classes(替身类、代理类)
- 条款31:让函数根据一个以上的对象类型来决定如何虚化
设计C++软件时,有一些问题会不断重复出现。例如,如何让constructors及nonmember
functions像虚函数一样地作用?如何限制class实体个数?如何阻止对象产生于heap内?如何保证对象产生于heap内?如何能够产生某种对象,使它再某些class的member
functions被调用时,自动执行某些动作?如何令不同的对象共享同一份数据结构,却让用户错以为每个对象各自有一份数据?如何区分operator[]的读写用途?如何产生一个虚函数,使其行为视多个对象的动态类型而定?
条款25:将constructor和non-member functions虚化
- 当你手上有一个对象的pointer或reference,而你不知道该对象的真正类型是什么的时候,你会调用virtual function(虚函数)以完成因类型而异的行为。当你尚未获得对象,但已经知道需要什么类型的时候,你会调用constructor以构造对象。那么virtual constructors是什么?假如设计一个软件,用来处理时事新闻,内容由文字和图形构成。组织如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57// 抽象基类,时事消息的组件(components),至少一个纯虚函数
class NLComponent
{
public:
virtual NLComponent* clone() const = 0;
...
};
class TextBlock : public NLComponent
{
public:
virtual TextBlock* clone() const
{
return new TextBlock(*this);
}
...
};
class Graphic : public NLComponent
{
public:
virtual Graphic* clone() const
{
return new Graphic(*this);
}
...
};
class NewsLetter
{
public:
NewsLetter(istream &str);
NewsLetter(const NewsLetter &rhs);
...
private:
list<NLComponent*> components;
// 从str读取下一个NLComponent的数据,产生组件并返回一个指向它的指针
static NLComponent* readComponent(istream &str);
};
NewsLetter::NewsLetter(istream &str)
{
while (str)
{
components.push_back(readComponent(str));
}
}
NewsLetter::NewsLetter(const NewsLetter &rhs)
{
// 迭代遍历rhs的list,运用每个元素的virtual copy constructor,将元素复制
for (list<NLComponent*>::const_iteraotr it = rhs.components.begin();
it != rhs.components.end();
++it)
// 调用it当前指向rhs.components的元素,然后调用该元素的clone取得副本加到本对象的list尾端
components.push_back((*it)->clone());
}readComponent
产生了一个崭新的对象,是TextBlock是Graphic视读入的数据而定。由于它产生了新对象,行为仿若constructor,但它能产生不同类型的对象,所以它是一个virtual constructor。所谓virtual constructor是某种函数,它获得输入,可产生不同类型的对象。 - 有一种特别的virtual constructor——所谓virtual copy constructor。它返回一个指针,指向其调用者的一个新副本。virtual copy constructors通常以copySelf或cloneSelf命名: class的virtual copy constructor只是调用真正的copy constructor而已。真正的copy constructor执行的是浅拷贝(shallow copy),virtual copy constructor一样,如果真正的copy constructor执行的是深复制(deep copy),virtual copy constructor亦然。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class NLComponent
{
public:
virtual NLComponent* clone() const = 0;
...
};
class TextBlock : public NLCommponent
{
public:
// virtual copy constructor
virtual TextBlock* clone() const
{
return new TextBlock(*this);
}
...
};
class Graphic : public NLComponent
{
public:
// virtual copy constructor
virtual Graphic* clone() const
{
return new Graphic(*this);
}
...
};
上述实现手法是利用“虚函数之返回类型”规则中的一个宽松点,它是晚些才被接纳的规则。当derived class重新定义其base class的虚函数时,不再需要得声明与其原本相同的返回类型。将Non-Member Functions的行为虚化
- 像constructors无法真正被虚化一样,non-member functions也是。让output操作符虚化(operator<<),获得一个ostream&作为其左端自变量,因此它不可能成为member function(其实可以,但会发生): 如果我们用虚函数(如print)来作为打印,就跟其他类型对象语法不一致了,很不自然。我们需要的是名为operator<<的non-member function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class NLComponent
{
public:
virtual ostream& operator<<(ostream &str) const = 0;
...
};
class TextBlock : public NLComponent
{
public:
virtual ostream& operator<<(ostream &str) const;
};
class Graphic : public NLComponent
{
public:
virtual ostream& operator<<(ostream &str) const;
};
TextBlock t;
Graphic g;
...
// 此些语法与传统不符,Clients必须把stream对象放在<<左侧
t << cout;
g << cout;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class NLComponent
{
public:
virtual ostream& print(ostream &s) const = 0;
...
};
class TextBlock : public NLComponent
{
public:
virtual ostream& print(ostream &s) const;
...
};
class Graphic : public NLComponent
{
public:
virtual ostream& print(ostream &s) const;
...
};
inline ostream& operator<<(ostream &s, const NLComponent &s)
{
return c.print(s);
}
条款26:限制某个class所能产生的对象数量
允许零个或一个对象
- 没当产生一个对象,会有constructor被调用。阻止某个对象被产出的方法是将constructor声明为private: 这个设计由三个成分。第一,Printer class的constructor的属性为private,可以压制对象诞生;第二,全局函数thePrinter被声明为class的一个friend,使thePrinter不受private constructors的约束;第三,thePrinter内含一个static Printer对象,意指只有一个Printer对象被产出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class PrintJob;
class Printer
{
public:
void submitJob(const PrintJob &job);
void reset();
void performSelfTest();
...
friend Printer& thePrinter();
private:
Printer();
Printer(const Printer &rhs);
...
};
Printer& thePrinter()
{
static Printer p;
return p;
}
class PrintJob
{
public:
PrintJob(const string &whatToPrint);
...
};
string buffer;
..
thePrinter().reset();
thePrinter().submitJob(buffer);
在此thePrinter的实现代码中,有两个精细的地方值得探讨。第一个细微点是,形成唯一的Printer对象,是函数中的static对象而非class中的staic对象。“class拥有一个static对象意思是:即使从未被用过,它也会被构造(及析构)。相反”函数拥有一个static对象“指的是,此对象在函数第一次被调用才产生。让打印机成为class static而非一个function static有个缺点,那就是不知道它的初始化时机,而function static的初始化实际是确切知道的。
第二个细微点是函数的”static对象与inlining的互动“。这个函数未被声明为inline。因为声明static意味着只需要唯一一个对象,但对于inline non-member function其中内含local static对象,意味着你这个函数有内部连接(internal linkage)。你的程序可能会拥有多份该static对象的副本。(新版编译期这个问题已经消除)
不同的对象构造状态
- 或许有人认为使用计数器来限制对象数量更简单。甚至是更一般化,可以使对象的最大数量可以设定为1以外的值。但这策略有问题。当其他对象继承Printer对象或者包含Printer时,就会抛出TooManyObjects exception。除非避免具体类继承其他具体类。问题出在Printer对象可在3种不同状态下生存:(1)它自己,(2)派生物的base class成分,(3)内嵌于较大对象之中。通常你只对上述(1)感兴趣。如果采用原先的策略,很容易达成。因为constructor是private的,如果没有声明任何friend的话,是不能被用来当作base classes的,也不能内嵌于其他对象内。
允许对象生生灭灭
- 知道了对象的constructor可于3种情况下被调用,知道了令constructors成为private可以混淆的对象计数。虽然用thePrinter函数封装起来,虽然限制了Printer对象的个数为1,但也限制了每次执行程序只能有唯一一个Printer对象(???),因此不能写出一下代码: 唯一的做法是将稍早的对象计数(object-counting)码和先前所见的伪构造函数(pseudo-constructors)结合起来:
1
2
3
4
5
6create Printer object p1;
use p1;
destroy p1;
create Printer object p2;
use p2;
destroy p2;这就被泛化伪任意个数(不限一个)的对象,它将原本的常量1改为class专属的一个数值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53class Printer
{
public:
class TooManyObjects {};
// pseudo-constructor
static Printer* makePrinter();
static Printer* makePrinter(const Printer &rhs);
~Printer();
void submitJob(const PrintJob &job);
void reset();
void performSelfTest();
...
private:
static size_t numObjects;
static const size_t Printer::maxObjects = 10; //
Printer();
// 不允许拷贝 E27
Printer(const Printer &rhs);
};
// class static
size_t Printer::numObjects = 0;
const size_t Printer::maxObjects;
Printer::Printer()
{
if (numObjects >= maxObjects)
throw TooManyObject();
proceed with normal object construction here;
++numObjects;
}
PrinterPrinter(const Printer &rhs)
{
if (numObjects >= maxObejcts)
throw TooManyObjects();
...
}
Printer* Printer::makePrinter()
{ return new Printer; }
Printer* Printer::makePrinter(const Printer &rhs)
{ return new Printer(rhs); }
// old
Printer p1; // error, default ctor is private
Printer *p2 = Printer::makePrinter(); // ok
Printer p3 = *p2; // error
p2->performSelfTest(); //
p2->reset();
...
delete p2; // 如果p2是smart pointer就不用
一个用来计算对象个数的Base Class
- 我们可以轻易完成一个base class,用来当作对象计数来使用,并让Printer之类的classes继承它。确保计算对象的每个class都有各自的计数器。设计一个class template: 修改Printer class,让它运用Counted template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33template<class BeingCounted>
class Counted
{
public:
class TooManyObject {}; // 可能抛出exceptions
static int objectCount()
{
return numObjects;
}
protected:
Counted();
Counted(const Counted &rhs);
~Counted() { --numObjects; }
private:
static int numObjects;
static const size_t maxObjects;
void init(); // 避免ctor重复出现
};
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;
}Counted的大部分作为都隐藏起来不让Printer的用户知道,但用户可能希望知道有多少个Printer对象存在。Counted template提供了objectCount函数,提供信息。然而该函数在Printer中变成了private访问层级,因为上面用的是private inheritance,为了恢复public访问层级,采用了using表达式。关于Counted内的statics义务性定义,只要将:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Printer : private Counted<Printer>
{
public:
// pseudo-constructors
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);ss
};放进某个Counted的某个实现文件就可以了。而maxObjects则放在Printer作者的某个实现文件中就可以了:1
2template<class BeingCounted>
int Counted<BeingCounted>::numObjects; // 定义并自动初始化为01
const size_t Counted<Printer>::maxObjects = 10;
条款27:要求(或禁止)对象产生于heap之中
要求对象产生于heap之中(Heap-Based Objects)
- 阻止clients不得使用new以外的方法产生对象,这很容易办到,只要让那些被隐式调用的构造动作和析构动作不合法就可以了。把constructors和destructor声明为private就太过了,那么只将destructors成为private就可以了。而用一个pseudo destructor函数来调用真正的destructor: 把constructors声明为private的缺点是,必须记住每一个constructor都声明为private。只要限制destructor或constructors的运用,就可以阻止non-heap objects的诞生,但是,它妨碍了继承(inheritance)和内含(containment)。这个困难可以克服,只需要将UPNumber的destructor成为protected就可以解决:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class UPNumber
{
public:
UPNumber();
UPNumber(int initValue);
UPNumber(double initValue);
UPNumber(const UPNumber &rhs);
// pseudo destructor
void destroy() const { delete this; }
...
private:
~UPNumber();
};
UPNumber n; // error
UPNumber *p = new UPNumber; // ok
...
delete p; // error
p->destroy(); // ok1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class UPNumber { ... };
class NonNegativeUPNumber : public UPNumber { ... };
class Asset
{
public:
Asset(int initValue);
~Asset();
...
private:
UPNumber *value;
};
Asset::Asset(int initValue) : value(new UPNumber(initValue)) { ... }
Asset::~Asset()
{ value->destroy(); }判断某个对象是否位于heap内
- 没有办法侦测出一个construcotr是不是在heap内。或许可以通过设置标志位,但对于operator new[]来说,设置标志位却只有一次机会设立标志信息,而第二次就会exception。或者是利用系统的一个特点:stack(栈)高地址往低地址成长,heap(堆)由低地址往高地址成长。但是这是不可移植的。这个函数的观念很有趣。一个临时stack是个局部变量,它会被放在stack的顶端,而stack向低地址成长,所以这个stack的地址一定比任何一个位于stack中的变量(或对象)更低。因此,如果一个位于heap的对象,其地址一定比临时stack的地址更低。但这个想法不完善,很多系统把static对象放置在heap的底下。
如果真实目的只是为了想知道对象调用delete是否安全,那就跟判断对象是否在heap是两回事了:operator new负责把一些条目(entires)加到一个由“动态分配而得的地址”所形成的集合中,operator delete负责把这些条目移除;isSafeToDelete负责查找集合,看看地址是否在其中。这些operator new和operator delete函数都在全局范围内,这应该堆所有类型都管用(甚至内置类型)。但这不是我们想要的——当对象存在多重继承或虚拟继承时,可能拥有多地址,这就是非自然多态(unnatural polymorphism)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void* operator new(size_t size)
{
void *p = getMemory(size);
// add p to the collection of allocated addresses;
return p;
}
void operator delete(void *ptr)
{
releaseMemory(ptr); // free store
// remove ptr from the collection of allocated addresses;
}
bool isSafeToDelete(const void *address)
{
// return whether address is in collection of allocated addresses
}
我们需要的是一个提供函数机能,但不符带全局命名空间的污染问题、额外的义务性负担,以及正确性的疑惑。那么abstract mixin base class(抽象混合式基类)可以满足需求。抽象基类不能被实例化,因为它至少有一个纯虚函数。mix in class则提供了一组定义好的能力,能与derived class兼容:凡涉及“多重或虚拟基类”的对象,会拥有多个地址,如果写在全局函数就会很复杂,但是isOnHeap只施行于HeapTracked对象身上,所以只要简单的将指针dynamic_cast为**void*(或const void*或volatile void*或const volatile void***),就会获得一个指针,指向原指针所指对象的内存起始处。不过dynamic_cast只适用于有至少一个虚函数的指针身上。之所以前面的isSafeToDelete撰写很复杂,就是因为它可以对任何类型起作用,因此dynamic_cast无法帮助它。isOnHeap有所选择(只针对HeapTracked对象的指针)。这个技术有移植性,只要编译器支持dynamic_cast。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55class HeapTracked
{
public:
class MissingAddress(); // exception class
virtual ~HeapTracked() = 0;
static void* operator new(size_t size);
static void operator delete(void *ptr);
bool isOnHeap() const;
private:
typedef const void *RawAddress;
static list<RawAddress> addresses;
};
// static class member的义务性定义
list<RawAddress> HeapTracked::addresses;
HeapTracked::~HeapTracked() {}
void* HeapTracked::operator new(size_t size)
{
void *memPtr = ::operator new(size); // 取得内存
addresses.push_front(memPtr); // 将地址放到list头部
return memPtr;
}
void HeapTracked::operator delete(void *ptr);
{
list<RawAddress>::iterator it = find(address.begin(), address.end(), ptr);
if (it != address.end()) {
addresses.erase(it);
::operator delete(ptr);
} else throw MissingAddress();
}
bool HeapTracked::isOnHeap() const
{
const void *rawAddress = dynamic_cast<const void*>(this);
list<RawAddress>::iterator it = find(address.begin(), address.end(), rawAddress);
return it != addresses.end();
}
class Asset : public HeapTracked
{
private:
UPNumber value;
...
};
void inventoryAsset(const Asset *ap)
{
if (ap->isOnHeap())
ap is a heap-based asset -- inventory it as such;
else
ssap is a non-heap-based asset -- record it that way;
}
禁止对象产生于heap之中
- 一般有3种情况:
- 对象被直接实例化
- 对象被实例化为derived class objects内的base class成分
- 对象被内嵌于其他对象之中。
阻止clients直接将对象实例化于heap之中,很容易,只需要将operator new和operator delete声明为private就可以了,如果想禁止对象所组成的数组,可以将operator new[]和operator delete声明为private。
条款28:Smart Pointers(智能指针)
- Smart poinnters是一种像内建指针,却提供了更多机能的对象。当以smart pointers取代C++的内建指针(dumb pointer),你会获得各种指针行为的控制权:
- 构造和析构(Construction and Destruction)。你可以决定smart pointer被产生以及被构造时发生什么事。通常给smart pointers一个默认值0,以避免指针未获初始化。
- 复制和复制(Copying and Assignment)。当一个smart pointer被复制或涉及复制动作时,可以控制发生什么事。
- 解引(Dereferencing)。当client解引(取用)smart pointer所指的对象时,有权决定发生什么事情。
Smart pointers由templates产生出来。由于像内建指针一样,所以它必须由强烈的类型性(strongly teyped)。
Smart Pointers的构造、复制、析构
- auto_ptr template可能实现如下: 在同一个对象只可被一个auto_ptr拥有的前提下,上述做法有效运转。但一旦auto_ptr被复制或被赋值,会发生什么?会导致两个auto_ptrs指向同一对象。这当在销毁对象时,可能会删除两次,往往会导致未定义。
1
2
3
4
5
6
7
8
9
10template<class T>
class auto_ptr
{
public:
auto_tpr(T *ptr = 0) : pointee(ptr) {}
~auto_ptr() { delete pointee; }
...
private:
T *pointee;
};
另一个做法是以new操作符为所指对象产生一个新副本。而auto_ptr不得指向一个类型为T的对象,可以指向一个T派生类型的对象。auto_ptr采用了一个富弹性的解法:当auto_ptr被复制或被赋值,其对象拥有权会转移: 由于auto_ptr的copy constructor被调用时,对象拥有权转移了,所以以by value方式转递auto_ptrs往往是个糟糕的主意:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27template<class T>
class auto_ptr
{
public:
...
auto_ptr(auto_ptr<T> &rhs); // copy constructor
auto_ptr<T>& operator=(auto_ptr<T> &rhs); // assignment operator
...
};
template<class T>
auto_ptr<T>::auto_ptr(auto_ptr<T> &rhs);
{
pointee = rhs.pointee;
rhs.pointee = 0;
}
template<class T>
auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T> &rhs)
{
if (this == &rhs);
return *this;
delete pointee; // 删除原有的对象
pointee = rhs.pointee;
rhs.pointee = 0;
return *this;
}当printTreeNode的参数被p初始化(通过auto_ptr的copy constructor),ptn所指的对象拥有权被转移至p。当printTreeNode结束,p离开生存空间,destructor被调用。然而ptn不再指向任何东西,这将产生未定义行为。所以Pass-by-reference-to-const才是适当的途径:1
2
3
4
5
6
7
8
9void printTreeNode(ostream &s, auto_ptr<TreeNode> p)
{ s << *p; }
int main()
{
auto_ptr<TreeNode> ptn(ew TreeNode);
...
printTreeNode(cout, ptn); // 以by value传递
}在此函数中,p是个referene而不是对象。所以不会由constructor被用来为p设定初值,ptn将保留拥有权。1
2void printTreeNode(ostream &s, const auto_ptr<TreeNode> &p)
{ s << *p; }
实现Dereferencing Operators(解引操作符)
- 现在注意力放在smart pointers的核心:operator*和operator->函数身上: 这个函数首先做任何必要的初始化动作或是让pointee获得有效值的任何动作。
1
2
3
4
5
6template<class T>
T& SmartPtr<T>::operator*() const
{
perform "smart pointer" processing;
return *pointee;
}
检验operator->前,看一下此函数的不寻常意义:其中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 产生smart ptrs,用来指向分布式数据库(DB)内的对象
template<class T>
class DBPtr
{
public:
DBPtr(T *realPtr = 0);
DBPtr(DataBaseID id);
...
};
// 用来表现数据库中的一笔数据(tuples)
class Tuple
{
public:
...
void displayEditDialog(); // 呈现图形式对话框,供输入tuple
bool isValid() const; // 检验*this是否有效
};
// class template,用来在T对象被修改时,完成运转记录(log entires)。
template<classs T>
class LogEntry
{
public:
LogEntry(const T &objectToBeModified);
~LogEntry();
};
void editTuple(DBPtr<Tuple &pt)
{
LogEntry<Tuple> entry(*pt);
// 反复显示编辑对话框,知道获得有效值为止
do pt->displayEditDialog();
while (pt->isValid() == false);
}pt->displayEditDialog();
会被编译器解释为:(pt.operator->())->displayEditDialog();
这个意味着,不论operator->返回什么,在该回传值身上施行->操作符都是合法的。因此operator->只能返回两个东西:一个dumb pointer或是一个smart pointer:1
2
3
4
5
6template<class T>
T* SmartPtr<T>::operator->() const
{
perform "smart pointer" processing;
return pointee;
}
测试Smart Pointers是否为NULL
- 我们可以产生、销毁、赋值、复制、解引smart pointers。但有一件事没办法做,就是判断是否为NULL: 为我们smart pointer class添加一个isNULL很容易,但不自然。另一个做法是,提供一个隐式转换操作符,允许上述测试动作通过编译。这个转换的传统目标是void*:
1
2
3
4
5SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ... // error
if (ptn) ... // error
if (!ptn) ... // error这个做法的缺点是,它竟然可以用来对完全不同类型的对象比较(我好像没出现这样的情况……)!这个问题无法解决,但有个差强人意的做法,允许提供测试nullness的合理语法,并能够将意外引起不同类型之smart pointers相互比较的机会降到最低,那就是将!操作符重载,并在其调用者是null的情况下,返回true:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<class T>
class SmartPtr
{
public:
...
operator void*(); // 如果dumb ptr是null,返回0,否则是非0值
...
};
SmartPtr<TreeNode> ptn;
...
if (ptn == 0) ... // ok
if (ptn) ... // ok
if (!ptn) ... // ok这个有风险的做法的原因是很少有程序员这么个写法。在C++标准程序库中,隐式转换为void*已被隐式转换为bool取代,而operator bool总是返回operator!的反相。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template<class T>
class SmartPtr
{
public:
...
bool operator!() const;
...
};
SmartPtr<TreeNode> ptn;
...
if (!ptn) ... // ok ptn is null
else ... // ptn is not null
if (ptn == 0) ... // error
if (ptn) ... // error
SmartPtr<Apple> pa;
SmartPtr<Orange> po;
...
if (!pa == !po) ... // ok
将Smart Pointers转换为Dumb Pointers
- 有时候你希望将smart pointers加入已使用的dumb pointers应用软件中, 那个转换动作很难看,如果为smart pointer-to-T template加上隐式类型转换操作符,使之转换为dumb pointer-to-T,先前的调用就能成功。
1
2
3
4
5
6
7class Tuple { ... };
void normalize(Tuple *pt); // by dumb pointer
DBPtr<Tuple> pt;
...
normaize(pt); // error
normalize(&*pt);// ok上述函数一加上,nullness测试问题也一并解决了。不过这样的转换也有阴暗面,它式clients得以轻易地直接对dumb pointers做动作,因而回避了smart pointer的最初目的。允许直接适用dumb pointers有灾难,它导致class的计数簿记工作方面的错误。造成引用计数所用的数据结构崩溃。1
2
3
4
5
6
7
8
9
10
11
12template<class T>
class DBPtr
{
public:
...
operator T*() { return pointee; }
...
};
DBPtr<Tuple> pt;
...
normalize(pt); // ok
即使提供了一个隐式转换操作符,smart pointer还是无法完全取代dumb pointer。因为从smart pointer转换为dumb pointer是一种用户定制的转换行为,而编译器禁止一次施行一次以上这类转换。举个例子,有个class,考虑一个函数用来整合两个TupleAccessors对象的信息:这是因为从DBPtr1
2
3
4
5
6
7
8
9
10
11
12
13
14class TupleAccessors
{
public:
TupleAccessors(const Tuple *pt);
...
};
TupleAccessors merge(const TupleAccessors &ta1, const TupleAccessors &ta2);
Tuple *pt1, *pt2;
merge(pt1, pt2); // ok
DBPtr<Tuple> pt1, pt2;
merge(pt1, pt2); // error转换为TupleAccessors需要两个用户定制转换,而着C++不允许,所以第一个将DBPtr 转换为Tuple*,而第二个将需要将Tuple*转环为TupleAccessors)。所以不要提供对dumb pointers的隐式转换操作符,除非不得已。
Smart Pointers和与继承有关的类型转换
- 假设有一个public inheritance继承体系,构建出消费性音乐产品: 之所以无法通过编译,是因为编译器所看见的是3个互不相干的classs。有个办法可以绕弯解除这一束缚:令每个smart pointer class有个隐式类型转换操作符,用来转换至另一个smart pointer class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42class MusicProduct
{
public:
MusicProduct(const string &title);
virtual void play() const = 0;
virtual void displayTitle() const = 0;
...
};
class Cassette : public MusicProduct
{
public:
Cassette(const string &title);
virtual void play() const;
virtual void displayTitle() const;
...
};
class CD : public MusicProduct
{
public:
CD(const string &title);
virtual void play() const;
virtual void displalyTitle() const;
...
};
void displayAndPlay(const MusicProduct *pmp, int numTimes);
{
for (int i = 1; i <= numTimes; ++i)
{
pmp->displayTitle();
pmp->play();
}
}
// dumb pointers的方式没问题,smart pointer取代之则另一种情况了
void displayAndPlay(const SmartPtr<MusicProduct> &pmp, int numTimes);
SmartPtr<Cassette> funMusic(new Cassette("Alapalooza"));
SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // error
displayAndPlay(nightmareMusic, 0); // error这个做法有两个缺点。第一,必须为每一个“SmartPtr class实例”加入上述例子;第二,需要加上很多这样的转换操作符,因为所指的对象位于继承体系的底层,必须为对象直接继承或间接继承的每个base class提供一个转换函数。由于编译器禁止一次执行一个以上的用户定制类型转换函数,所以无法将smart pointer-to-T转换为一个smart pointer-to-indirect-base-class-of-T。刚好有个语言扩充性质,它可以将nonvirtual member function声明为template,可以用它来产生smart pointer的转换函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class SmartPtr<Cassette>
{
public:
operator SmartPtr<MusicProduct>()
{ return SmartPtr<MusicProduct>(pointee); }
...
private:
Cassette *pointee;
};
class SmartPtr<CD>
{
public:
operator SmartPtr<MusicProduct>()
{ return SmartPtr<MusicProduct>(pointee); }
...
private:
CD *pointee;
}funMusic对象属于SmartPtr1
2
3
4
5
6
7
8
9
10
11
12
13
14// template class,用于smart pointer-to-T对象
template<class T>
class SmartPtr
{
public:
SmartPtr(T *realPtr = 0);
T* operator->() const;
T& operator*() const;
// template function,用于隐式转换操作符
template<class newType>
operator SmartPtr<newType>()
{ return SmartPtr<newType>(pointee); }
,,,
};类型,而displayAndPlay函数期望得到一个SmartPtr 对象。编译器发现类型不吻合,于是企图将funMusic转换其期望的对象。编译器在SmartPtr class内企图找一个”单一变量之construcotr“,其自变量类型为SmartPtr ,但没有找到;于是企图在SmartPtr class内找一个隐式类型转换操作符,希望产出已给SmartPtr class,但也失败了;接下来再试图寻找一个”可实例化以导出合宜转换函数“的member function template。它在SmartPtr 找到一个东西,当它被实例化并令newType绑定MusicProduct时,编译器将之实例化,可得: 这其中涵盖的技术不简单,它包括:(1)函数调用的自变量匹配规则、(2)隐式类型转换函数、(3)template functions的暗自实例化、(4)member function templates等技术。1
2SmartPtr<Cassette>::operator SmartPtr<MusicProduct>()
{ return SmartPTr<MusicProduct>(pointee); }
Smart Pointers与const
- const可以修饰被指的东西 很自然,我们想要smart pointers也有同样的弹性。但Smart pointer只能有一个地方放置const,只能施行与指针身上,不能及于所指对象,不过我们可以对const以及non-const的对象及指针,产生4种组合:
1
2
3
4CD goodCD("Flood");
const CD *p; // p时non-const指针,指向const CD object
CD *const p = &goodCD; // p是哟个const指针,指向non-const CD object,必须有初值
const CD *const p = &goodCD; //p是一个const指针,指向一个const CD object如果适用dumb pionters,可以non-const指针作为const指针的初值,也可以指向non-const独享的指针作为指向const对象指针的初始值,赋值规则类似:1
2
3
4SmartPtr<CD> p;
Smart<const CD> p;
const SmartPtr<CD> p = &goodCD;
const SmartPtr<const CD> p = &goodCD;类型转换如果涉及const,便是一条单行道:从non-const到const是安全的,从const到non-const是不安全的。解决上面的方法是,将derived class object转换为base class object,令smart pointer-to-T class公开继承一个对应的smart pointer-to-const-T class:1
2
3
4CD *pCD = new CD("Famous Movie Themes");
const CD *pConstCD = pCD; // ok
SmartPtr<CD> pCD = new CD("Famous Movie Themes");
SmartPtr<const CD> pConstCD = pCD; // error运用这个设计,我们获得了自己希望的行为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<class T>
class SmartPtrToConst
{
...
functions
protected:
union(
{
const T *constPointee;
T *pointee;
};
};
template<class T>
class SmartPtr : public SmartPtrToConst<T>
{
...
};1
2SmartPtr<CD> pCD = new CD("Famous Movie Themes");
SmartPtrToConst<CD> pConstCD = pCD; // ok
条款29:Reference counting(引用计数)
- Reference counting这项技术,允许多个等值对象共享同一个实值。Reference counting可以消除记录对象拥有权的负荷,因为当对象用reference counting技术,它拥有它自己,一旦没人使用它,便自动销毁自己。因此,reference counting建构出垃圾回收机制(garbage colection)的一个简单形式。
Reference Counting(引用计数)的实现
- 基本设计像这样: 内嵌的结构体StringValue主要用于存储引用计数和字符串值,并使得引用计数和字符串值相关联.StringValue的实现像这样:
1
2
3
4
5
6
7class String {
public:
...
private:
struct StringValue { ... }; // 包含引用计数和字符串值
StringValue *value; // value of this String
};StringValue只对String类可见,而对客户不可见,接口由String定义并提供给客户。String的构造函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class String {
private:
struct StringValue {
int refCount;
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};
String::StringValue::StringValue(const char *initValue) : refCount(1)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::~StringValue()
{
delete [] data;
}第一个构造函数的实现较简单,根据传入的char数组构造StringValue对象,然后使String中的指针指向这个String即可:1
2String(const char *initValue = "");
String(const String& rhs);但这样的实现导致”分开构造,但拥有相同初值的String对象,并不共享同一个数据结构”,因此像这样的代码:1
String::String(const char *initValue): value(new StringValue(initValue)){}
尽管是s1和s2的值相同,但它们却并不共享同一个块内存,而是各自拥有独立内存。1
2String s1("More Effective C++");
String s2("More Effective C++");
拷贝构造函数可以使用引用计数,并共享内存,像这样:析构函数负责在引用计数为0的时候撤销内存:1
String::String(const String& rhs): value(rhs.value){ ++value->refCount; }
赋值操作符要注意自身赋值的情况:1
2
3
4
5String::~String()
{
if (--value->refCount == 0)
delete value;
}1
2
3
4
5
6
7
8
9
10
11
12String& String::operator=(const String& rhs)
{
if (value == rhs.value) { //处理自身赋值
return *this;
}
if (--value->refCount == 0) {
delete value;
}
value = rhs.value;
++value->refCount;
return *this;
}
Copy-on-Write(写时才复制)
- 对operator[]的重载比较复杂:const版本是只读动作,因而只返回指定字符即可,像这样: 但non-const版本面临着被写入新的值的可能,由于对当前String的修改不应影响到共享内存的其他String对象,因此需要先为当前String分配独立内存并将原值进行拷贝,像这样:
1
const char& String::operator[](int index) const{ return value->data[index]; }
不仅是operator[],其他可能改变String对象的操作也应该采取和non-cons版本operator[]相同的动作.这其实是lazy evaluation的一种应用.1
2
3
4
5
6
7
8char& String::operator[](int index)
{
if (value->refCount > 1) {
--value->refCount;
value =new StringValue(value->data);
}
return value->data[index];
}
Pointers,References,以及Copy-on-Write
- 3中对operator[]的重载解释并解决了可能的写操作篡改共享内存的问题,但是却无法阻止外部指针或引用对共享内存的篡改,像这样: 对p所指向的内存的任何写操作都会同时更改s1和s2的值,但是s1却对此一无所知,因为p和s1没有内在联系.解决办法并不难:为每一个StringValue对象加一个标志(flag)变量,用表示是否可以被共享,开始时先将flag设为true(可以共享),一旦non-const operator[]被调用就将该flag设为false,并可能永远不许再更改(除非重新为StringValue分配更大内存而导致指针失效),StringValue的修改版像这样:
1
2
3String s1 = "Hello";
char *p = &s1[1];
String s2 = s1;String的member function在企图使用共享内存前,就必须测试内存是否允许被共享:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class String {
private:
struct StringValue {
int refCount;
bool shareable;
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};
String::StringValue::StringValue(const char *initValue): refCount(1),shareable(true)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::~StringValue()
{
delete [] data;
}其他返回引用的member function(对于String只有operator[])都涉及到对flag的修改,而其他可能需要共享内存的member function都涉及到对flag的检测。 条款30的proxy class技术可以将operator[]的读和写用途加以区分,从而降低”需被标记为不可共享”之StringValue对象的个数。1
2
3
4
5
6
7
8
9
10String::String(const String& rhs)
{
if (rhs.value->shareable) {
value = rhs.value;
++value->refCount;
}
else {
value = new StringValue(rhs.value->data);
}
}
一个Reference-Counting(引用计数)基类
任何要支持内存共享的class都可以使用reference-counting,因此可以考虑把它抽象为一个类,任何需要reference-counting功能的class只要使用这个类即可。
第一步就是产生一个base class RCObject,执行引用计数的功能并标记对象是否可被共享,像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class RCObject {
public:
RCObject();
RCObject(const RCObject& rhs);
RCObject& operator=(const RCObject& rhs);
virtual ~RCObject() = 0;
void addReference();
void removeReference();
void markUnshareable();
bool isShareable() const;
bool isShared() const;
private:
int refCount;
bool shareable;
};由于RCObject的作用只是实现引用计数的辅助功能,然后让StringValue继承它,因此StringValue被设为一个抽象基类——通过将析构函数设为纯虚函数,但仍需要为析构函数提供定义.RCObject的实现像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14RCObject::RCObject(): refCount(0), shareable(true) {}
RCObject::RCObject(const RCObject&):refCount(0),shareable(true) {}
RCObject& RCObject::operator=(const RCObject&)
{ return *this; }
RCObject::~RCObject() {} // virtual dtors must always
void RCObject::addReference() { ++refCount; }
void RCObject::removeReference()
{ if (--refCount == 0) delete this; }
void RCObject::markUnshareable()
{ shareable = false; }
bool RCObject::isShareable() const
{ return shareable; }
bool RCObject::isShared() const
{ return refCount > 1; }RCObject的实现非常简单,但是其拷贝构造函数和赋值操作符有些特殊——它们的参数没有名字,也就是说参数没有作用,其拷贝构造函数和赋值操作符都只是形式上的:
RCObjetc拷贝构造函数与RCObject的作用相对应——RCObject一旦被构造,就说明一个新的对象被产生出来,那么RCObject对象本身的初始值和默认构造函数相同,至于refCount设为0而不是1,这要求对象创建者自行将refCount设为1.
RCObject的赋值操作符什么也不做,仅仅返回*this,因为它不应该被调用,正如之前的StringValue,如果对String对象赋值,那么或者StringValue被共享,或者拷贝构造一个新的StringValue,实际上StringValue的赋值操作永远不会被调用.即使要对StringValue做赋值操作,像这样:
1
sv1=sv2;//sv1和sv2是StringValue型对象
指向sv1和sv2的对象数目实际上并未改变,因此sv1的基类部分RCObject什么也不做仍然是正确的.
removeReference的责任不仅在于将refCount减1,实际上还承担了析构函数的作用——在refCount=1的时候delete销毁对象,从这里可以看出RCObject必须被产生于heap中.
StringValue要直接使用RCObject,像这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class String {
private:
struct StringValue: public RCObject {
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};
String::StringValue::StringValue(const char *initValue)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::~StringValue()
{
delete [] data;
}StringValue类public继承自RCObject,因此它继承了RCObject的接口并供String使用,StringValue也必须构造在heap中.
自动操作Reference Count(引用计数)
- RCObject提供了一定程度的代码复用功能,但还远远不够——String类仍然需要手动调用RCObject的成员函数来对引用计数进行更改.解决方法就是”计算机科学领域中大部分问题得以解决的原理”——在中间加一层,也就是在String和StringValue中间加一层智能指针类对引用计数进行管理,像这样: 之前RCPtr是一个类模板,String之前有一个StringValue*成员,现在只要将它替换为RCPtr
1
2
3
4
5
6
7
8
9
10
11
12
13
14//管理引用计数的智能指针类
template<class T>
class RCPtr {
public:
RCPtr(T* realPtr = 0);
RCPtr(const RCPtr& rhs);
~RCPtr();
RCPtr& operator=(const RCPtr& rhs);
T* operator->() const; // see Item 28
T& operator*() const; // see Item 28
private:
T *pointee;
void init(); //将构造函数中的重复操作提取成一个函数
};即可.
RCPtr的构造函数像这样:init中使用了new关键字,它调用T的拷贝构造函数,为防止编译器为StringValue合成的拷贝构造函数执行浅复制,需要为StringValue定义执行深度复制的拷贝构造函数,像这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template<class T>
RCPtr<T>::RCPtr(T* realPtr): pointee(realPtr)
{
init();
}
template<class T>
RCPtr<T>::RCPtr(const RCPtr& rhs): pointee(rhs.pointee)
{
init();
}
template<class T>
void RCPtr<T>::init()
{
if (pointee == 0) {
return;
}
if (pointee->isShareable() == false) {
pointee = new T(*pointee);
}
pointee->addReference();//引用计数的更改负担转移到这里
}此外,由于多态性的存在,尽管pointee是T*类型,但它实际可能指向T类型的派生类,在此情况下new调用的却是T的拷贝构造函数,要防止这种现象,可以使用virtual copy constructor(见条款25),这里不再讨论1
2
3
4
5String::StringValue::StringValue(const StringValue& rhs)
{
data = new char[strlen(rhs.data) + 1];
strcpy(data, rhs.data);
}
把所有努力放到这里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
template<class T>
class RCPtr
{
public:
RCPtr(T *realPtr = 0);
RCPtr(const RCPtr &rhs);
~RCPtr();
RCPtr& operator=(const RCPtr &rhs);
T* operator->() const;
T& operator*() const;
private:
T *pointee;
void init();
};
template<class T> void RCPtr<T>::init()
{
if (pointee == 0) return;
if (pointee->isShareable() == false)
pointee = new T(*pointee);
pointee->addReference();
}
template<class T> RCPtr<T>::RCPtr(T *realPtr) : pointee(realPtr) { init(); }
template<class T> RCPtr<T>::RCPtr(const RCPtr &rhs) : pointee(rhs.pointee) { init(); }
template<class T> RCPtr<T>::~RCPtr() { if (pointee) pointee->removeReference(); }
template<class T> T* RCPtr<T>::operator->() const { return pointee; }
template<class T> T& RCPtr<T>::operator*() const { return *pointee; }
template<class T> RCPtr<T>& RCPtr<T>::operator=(const RCPtr &rhs)
{
if (pointee != rhs.pointee)
{
if (pointee) pointee->removeReference();
pointee = rhs.pointee;
init();
}
return *this;
}
class RCObject
{
public:
void addReference();
void removeReference();
void markUnshareable();
bool isShareable() const;
bool isShared() const;
protected:
RCObject();
RCObject(const RCObject &rhs);
RCObject& operator=(const RCObject &rhs);
virtual ~RCObject() = 0;
private:
int refCount;
bool shareable;
};
RCObject::RCObject() : refCount(0), shareable(true) {}
RCObject::RCObject(const RCObject&) : refCount(0), shareable(true) {}
RCObject& RCObject::operator=(const RCObject&) { return *this; }
RCObject::~RCObject() {}
void RCObject::addReference() { ++refCount; }
void RCObject::removeReference() { if (--refCount == 0) delete this; }
void RCObject::markUnshareable() { shareable == false; }
bool RCObject::isShareable() const { return shareable; }
bool RCObject::isShared() const { return refCount > 1; }
class String
{
public:
String(const char *value = "");
const char& operator[](int index) const;
char& operator[](int index);
private:
struct StringValue : public RCObject
{
char *data;
StringValue(const char *initValue);
StringValue(const StringValue &rhs);
void init(const char *initValue);
~StringValue();
};
RCPtr<StringValue> value;
};
void String::StringValue::init(const char *initValue)
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}
String::StringValue::StringValue(const char *initValue) { init(initValue); }
String::StringValue::StringValue(const StringValue& rhs) { init(rhs.data); }
String::StringValue::~StringValue() { delete []data; }
String::String(const char *initValue) : value(new StringValue(initValue)) {}
const char& String::operator[](int index) const { return value->data[index]; }
char& String::operator[](int index)
{
if (value->isShared())
value = new StringValue(value->data);
value->markUnshareable();
return value->data[index];
}
条款30:Proxy classes(替身类、代理类)
- 通过一个类对象来象征一个其他对象,常被称为proxy objects(替身对象)。它允许我们完成某些十分困难或几乎不可能完成的行为。多维数组是其中之一,左值/右值的区分是其中之二,压抑隐式转换是其中之三。
- https://www.cnblogs.com/reasno/p/4858490.html
条款31:让函数根据一个以上的对象类型来决定如何虚化
- 假设设计一个游戏,根据宇宙飞船、太空站和小行星三者的共同特征,设计为以下情况: 整个继承体系:
1
2
3
4graph BT
A[SpaceShip] --> S(GameObject)
B[SpaceStation] --> S
C[Asteroid] --> S不同对象相撞要有不同的规则,处理碰撞的函数声明像这样:1
2
3
4class GameObject { ... };
class SpaceShip : public GameObject { ... };
class SpaceStation : public GameObject { ... };
class Asteroid : public GameObject { ... };现在挑战出来了,处理他们之间的碰撞,首先得知道它们的动态类型。人们常把面向对象里虚函数的调用动作称为>message dispatch(消息分派)。然而C++并不直接支持multiple dispatch。1
void checkForCollision(GameObject &object1, GameObject &object2);
虚函数 + RTTI(运行时期类型识别)
- 最一般化的double-dispatching实现方法是用if-then-elses来仿真虚函数: 我们只需要决定碰撞双方的其中一个类型,因为另一个对象是*this,它的类型被虚函数机制决定下来了,是SpaceShip对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33class GameObject {
public:
virtual void collide(GameObject &otherObject) = 0;
...
};
calss SpaceShip : public GameObject {
public:
virtual void collide(GameObject &otherObject);
...
};
// 如果和一个未知类型对象相撞,就抛出以下exception
class CollisionWithUnknownObject {
public:
CollisionWithUnknownObject(GameObject &whatWeHit);
...
};
void SpaceShip::collide(GameObject &otherObject) {
const type_info &objectType = typeid(otherObject);
if (objectType == typeid(SpaceShip)) {
SpaceShip &ss = static_cast<SpaceShip&>(otherObject);
// process a SpaceShip-SpaceShip collision;
} else if (objectType == typeid(SpaceStation)) {
SpaceStation &ss = static_cast<SpaceStation&>(otherObject);
// process a SpaceShip-SpaceStation collision;
} else if (objectType == typeid(Asteroid)) {
Asteroid &a = static_cast<Asteroid&>(otherObject);
// sprocess a SpaceShip-Asteroid collision;
} else
throw CollisionWithUnknownObject(otherObejct);
}
这种以类型为行事基准的方法有个缺点:它会造成程序难以维护。这就是虚函数当初被发明出来的原因:把生产维护以类型为行事基准的函数的负担,从程序员转到编译器。但现在如果使用RTTI来实现double-dispatching,等于回到老而糟糕的年代。
只使用虚函数
- 看看如何只以虚函数来解决问题: 这种方法将double-dispatching以两个分离的虚函数调用实现出来的。这并不是递归调用。第一个虚函数调用动作针对的是接收GameObject&参数的collide函数,有点简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class SpaceShip; // forward declarations
class SpaceStation;
class Asteroid;
class GameObject {
public:
virtual void collide(GameObjet &otherObject) = 0;
virtual void collide(SpaceShip &otherObject) = 0;
virtual void collide(SpaceStation &otherObject) = 0;
virtual void collide(Asteroid &otherObject) = 0;
...
};
class SpaceShip : public GameObject {
public :
virtual void collide(GameObjet &otherObject);
virtual void collide(SpaceShip &otherObject);
virtual void collide(SpaceStation &otherObject);
virtual void collide(Asteroid &otherObject);
...
};但这种做法成本太高,每次修改需要在每个class中增添(E34)。但它也有好处,那就是不再因为为止类型而不得不抛出异常。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void SpaceShip::collide(GameObject &otherObject)
{
otherObject.collide(*this);
}
void SpaceShip::collide(SpaceShip &otherObject)
{
// process a SpaceShip-SpaceShip collision;
}
void SpaceShip::collide(SpaceStation &otherObject)
{
// processa SpaceShip-SpaceStation collision;
}
void SpaceShip:collide(Asteroid &otherObject)
{
// process SpaceShip-Asteroid collision;
}
自行仿真虚函数表格(Virtual Function Tables)
- 回忆条款24,编译器通过vtbl直接索引取得函数指针,而不必条条框框if-then-else运算。这样一来效率也高,也可以当使用RTTI的时候隔离至一个点:vtbl的初始化处。
先从GameObject继承体系内的函数开始:跟一开始说的RTTI解法一样,GameObject class只含有一个碰撞处理函数,这个函数执行两个必要的single-dispatches中的第一个。而其他互动函数不再使用同一个collide名称,放弃了重载。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62class GameObject {
public:
virtual void collide(GameObject &otherObject) = 0;
...
};
class SpaceShip : public GameObject {
public:
virtual void collide(GameObject &otherObject);
virtual void hitSpaceShip(GameObject &spaceShip);
virtual void hitSpaceStation(GameObject &spaceStation);
virtual void hitAsteroid(GameObject &asteroid);
...
private:
typeef void (SpaceShip::*HitFunctionPtr)(GameObject&);
typedef map<string, HitFunctionPtr> HitMap;
// 产生中介函数lookup
static hitFunctionPtr lookup(const GameObject &whaWeHit);
static HitMap* initializeCollisionMap();
...
};
void SpaceShip::hitSpaceShip(SpaceShip &otherObject)
{
SpaceShip &otherShip = dynamic_cast<SpaceShip&>(spaceShip);
// process a SpaceShip-SpaceShip collision;
}
void SpaceShip::hitSpaceStation(SpaceStation &otherObject)
{
SpaceStation &station = dynamic_cast<SpaceStation&>(spaceStation);
// process a SpaceShip-SpaceStation collision;
}
void SpaceShip::hitAsteroid(Asteroid &otherObject)
{
Asteroid &theAsteroid = dynamic_cast<Asteroid&>(asteroid);
// process a SpaceShip-Asteroid collision;
}
void SpaceShip::collide(GameObject &otherObject)
{
// 找出调用的函数
HitFunctionPtr hfp = lookup(otherObject);
if (hfp) (this->*hfp)(otherObject);
else throw CollisionWithUnknownObject(otherObject);
}
SpaceShip::HitMap* SpaceShip::initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSapceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject &whatWeHit)
{
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
HitMap::iterator mapEntry = collisionMap.find(typeid(whatWeHit).name());
if (mapEntry == collisionMap.end())
return 0;
return (*mapEntry).second;
}
这里我们需要交付给一个中介函数lookup一个GameObject,它会返回一个指向”当和GameObject相撞时“必须调用的函数的指针(函数指针)。为了能够动态映射某个member function指针,一个简单的方法是产生一个关系型(associative)数组,只要获得class名字,导出member function指针(key-value?)。
将自行仿真的虚函数表格(Virtual Function Tables)初始化
- 对于collisionMap的初始化问题,只需要写一个private static member function,名为initializeCollisionMap,用来初始化,然后返回值作为初值就可以了。然而返回值Map按值传递意味着构造和析构成本,如果返回指针,又要苦恼map对象的delete时宜,那么用smart pointer吧(见上面完整实现)。
使用”非成员(Non-Member)函数“的碰撞处理函数
- 当有新的class加入时,继承体系的每个类都需要添加处理新型碰撞的代码.这是因为此前的策略都是将处理碰撞的任务交由碰撞的某一方来执行,仿真虚函数表策略也不例外——每个class内含一个仿真的虚函数表,内含的指针也都指向成员函数。将碰撞处理函数设为non-member,就可以使得class定义式不包含碰撞处理函数,当需要添加碰撞处理函数时也就不需要修改class定义。将碰撞处理函数移出class外,成为中立的第三者处理,则构筑processCollision函数: 这份实现和先前的member functions版相同,但略有差异:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 匿名namespace具有文件内部static的功效
namespace {
// 主要的碰撞处理函数
void shipAsteroid(GameObject &spaceShip, GameObject &asteroid);
void shipStation(GameObject &spaceShip, GameObject &spaceStation);
void asteroidStation(GameObject &asteroid, GameObject &spaceStation);
...
// 次要的碰撞处理函数,只是为了实现对称性
// 对调参数位置,然后调用主要的碰撞处理函数
void asteroidShip(GameObject &asteroid, GameObject &spaceShip)
{ shipAsteroid(spaceShip, asteroid); }
void stationShip(GameObject &spaceStation, GameObject &spaceShip)
{ shipStation(spaceShip, spaceStation); }
void stationAsteroid(GameObject &spaceStation, GameObject &asetroid)
{ asteroidStation(asteroid, spaceSation); }
...
// types/functions
typedef void (*HitFunctionPtr)(GameObject&, GameObject&);
typedef map< pair<string, string>, HitFunctionPtr > HitMap;
// 以两个char*字面常量产生一个pair<string, string>对象
pair<string, string> makeStringPair(const char *s1, const char *s2)
{ return pair<string, string>(s1, s2); }
//
HitMap* initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)[makeStringPair("SpaceShip", "Asteroid")] = &shipAsteroid;
(*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
...
return phm;
}
// 必须修改,以便接纳pair<string, string>对象
HitFunctionPtr lookup(const string class1, const string &class2)
{
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
HitMap::iterator mapEntry = collisionMap->find(make_pair(class1, class2));
if (mapEntry == collisionMap->end()) return 0;
return (*mapEntry).second;
}
}
void processCollision(GameObject &object1, GameObject &object2)
{
HitFunctinoPtr phf = lookup(typeid(object1).name(), typeid(object2).name());
if (phf) phf(object1, object2);
else throw UnknownCollision(object1, object2);
}- HitFunctionPtr如今是一个指向non-member function的指针。
- exception class CollsionWithUnknownObject已经被重新命名为UnknownCollision并改为取得两个对象。
- lookup需要接收两个类型名称,并执行double-dispatch的完整两半。
由于makeStringPair,initializationCollisionMap,lookup都声明于匿名namespace内,因此它们必须实现于相同的namesapce中,使得链接器能够正确的将定义和声明关联起来。
通过将碰撞处理函数从类中分离,实现了即使新的GameObject被添加,原有的class也不需要重新编译,只需要在initializeCollisionMap中增加对应的键-值对,并在processCollision所在的匿名命名空间中申明一个新的碰撞处理函数即可。
”继承“ + ”自行仿真的虚函数表格“
- 目前所做的每一件事都可以有效运作——只要在调用碰撞处理函数时不发生inheritance-based类型转换。 如果MilitaryShip和一个Asteroid碰撞,希望调用的时:
1
2
3
4
5
6graph BT
A[SpaceStation] --> G
S(SpaceShip) --> G(GameObject)
B[Asteroid] --> G
C[CommercialShip] --> S
D[MilitaryShip] --> S然而事非如此,而是抛出一个UnknownCollision exception。虽然MilitaryShip对象可视为一个SpaceShip对象,但lookup并不知道。1
void shipAsteroid(GameObject &spaceShip, GameObject &asteroid);
如果想要实现double-dispatching而且需要支持inheritance-based参数转换,那么唯一可用的资源是”双虚函数调用“机制。