《more effective C++》Item 29-35

《more effective C++》Item 29-35

[TOC]
本文记录一下《more effective C++》Item 29-35 的学习笔记.
内容包括引用计数, 代理类, 多虚化, 抽象类的设计, C 与 C++ 如何结合等.

Item 29 Reference counting(引用计数)

本节通过抛出一个概念然后逐渐优化它增加各种功能的做法来较为全面地介绍了引用计数技术.

概念的引入

定义: Reference counting is a technique that allows multiple objects with the same value to share a single representation of that value.

引用计数的好处:

  1. 简化 heap objects 周边的薄记工作(garbage collection), 记录对象有没有在被用, 没有人用时自动析构.
  2. 节省内存, 在同一个对象值(一块内存)被多个实例使用时, 统一用指针指向它.

实现的效果如下:

转变为:

实现

  • 接口

we need one reference count per string value, not one reference count per string object. 因此需要把引用计数与特定的值绑定在一起, 如下面的 StringValue.

1
2
3
4
5
6
7
8
9
class String {
public:
...
// the usual String member functions go here
private:
struct StringValue { ... };
// holds a reference count and a string value
StringValue *value; // value of this String
};

String 类为面向用户的接口. struct StringValue 为实现引用计数的内置类.

  • StringValue 的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class String {
private:
struct StringValue {
size_t 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;
}

StringValue 的目的是提供一个地点将某特定值以及共享该值的个数关联起来. 对于计数的操作在 String 里进行.

  • String 的具体实现
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
class String {
public:
String(const char *initValue = "");
String(const String& rhs); //copy constructor
~String(); //destructor
String& operator=(const String& rhs); //assignment operator
const char& operator[](int index) const; // for const Strings
char& operator[](int index); // for non-const Strings, 此处涉及到修改共享值的问题.
...
};

String::String(const String& rhs): value(rhs.value)
{
++value->refCount;
}

String::~String()
{
if (--value->refCount == 0) delete value;
}

String& String::operator=(const String& rhs)
{
if (value == rhs.value) { // do nothing if the values are already the same;
return *this; // this subsumes the usual test of this against &rhs
}

if (--value->refCount == 0) { // destroy *this’s value if no one else is using it
delete value;
}

value = rhs.value; // have *this share rhs’s value

++value->refCount;
return *this;
}

如下的初始化会导致图中的问题, 具体的查找消除重复的部分, 作者留给了读者.

1
2
3
4
5
6
String::String(const char *initValue)
: value(new StringValue(initValue))
{
}
String s1("More Effective C++");
String s2("More Effective C++");

Copy-on-write(写时才复制)

operator [] 的实现会遇到问题.

operator [] 进行读的操作时, 不会影响到引用计数 但用 operator [] 进行写操作时会影响到引用计数, 因为我们需要创建一个新的值放修改后的值, 同时原值的计数需要减1.

1
2
3
4
String s;
...
cout << s[3]; // this is a read
s[5] = ’x’; // this is a write

本节不介绍如何自动识别是读还是写 operator [] 的功能(在 Item 30 中介绍 proxy class), 因此悲观地假设 non-const operator [] 都是被用于写操作的.

同时还有一个假设: To implement the non-const operator[] safely, we must ensure that no other String object shares the StringValue to be modified by the presumed write.

1
2
3
4
5
6
7
8
9
10
char& String::operator[](int index)
{
// if we’re sharing a value with other String objects,
// break off a separate copy of the value for ourselves
if (value->refCount > 1) {
--value->refCount; // decrement current value’s refCount, because we won’t be using that value any more
value = new StringValue(value->data); // make a copy of the value for ourselves
}
return value->data[index]; // return a reference to a character inside our unshared StringValue object
}

Copy-on-write 是一个共用的技术, 使用了 lazy evaluation 的原则节省资源.

共享值被多处修改的问题

1
2
3
4
String s1 = "Hello";
char *p = &s1[1];
String s2 = s1;
*p = ’x’; // modifies both s1 and s2!

三种思路处理该问题:

  1. 就当没看见.
  2. 在文档中介绍不用这么干.
  3. StringValue 对象加上一个 shareability flag, 标明 non-const 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
36
37
38
39
40
41
42
43
44
45
class String {
private:
struct StringValue {
size_t refCount;
bool shareable; // add this
char *data;
StringValue(const char *initValue);
~StringValue();
};
...
};

String::StringValue::StringValue(const char *initValue): refCount(1),shareable(true) // add this
{
data = new char[strlen(initValue) + 1];
strcpy(data, initValue);
}

String::StringValue::~StringValue()
{
delete [] data;
}

String::String(const String& rhs)
{
//所有的 member function 都应该这样检查是否可以共享
if (rhs.value->shareable) {
value = rhs.value;
++value->refCount;
}
else { //如果不能共享就 new 一个一模一样的值出来, 可以共享.
value = new StringValue(rhs.value->data);
}
}

char& String::operator[](int index)
{
if (value->refCount > 1) {
--value->refCount;
value = new StringValue(value->data);
}
// non-const operator [] 是唯一把 flag 置为 false 的函数
value->shareable = false; // add this
return value->data[index];
}

一个引用计数的基类

Rewriting a class to take advantage of reference counting can be a lot of work. 因此考虑更好的方法: write (and test and document) the reference counting code in a context-independent manner, then just graft it onto classes when needed.

RCObject 的基类:

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
class 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:
size_t refCount;
bool shareable;
};

//下面是实现
RCObject::RCObject(): refCount(0), shareable(true) {} //refCount 为啥是 0? 后面讲解

RCObject::RCObject(const RCObject&): refCount(0), shareable(true) {} //refCount 为啥设置为 0? 后面讲解

RCObject& RCObject::operator=(const RCObject&) { return *this; } //后面讲解

RCObject::~RCObject() {} // virtual dtors must always be implemented, even if
// they are pure virtual and do nothing

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; }

上面代码的解释:

  1. constructors 里将 refCount 设为 0 而不是 1. It simplifies things for the creators of RCObjects to set refCount to 1 themselves.
  2. copy constructor always sets refCount to 0. That’s because we’re creating a new object representing a value, and new values are always unshared and referenced only by their creator.
  3. assignment operator looks downright subversive(颠覆性的): 这么做的原因: In our case, we don’t expect StringValue objects to be assigned to one another, we expect only String objects to be involved in assignments. In such assignments, no change is made to the value of a StringValue — only the StringValue reference count is modified. 这里我表述一下我的理解, 真实的字符串数值, 是在一个”字符串池”里产生的, String 里有的只是其指针以及统计其引用计数的 StringValue, 而管理计数的部分被抽象为 RCObject. 只是涉及到对 StringValue 以及 RCObject 的复制并不会影响计数, 因为计数这一现象是从 String 层级(较高的用户层级)产生的. 这样才可以理解下面的话:

Given StringValue objects sv1 and sv2, what should happen to sv1’s and sv2’s reference counts in an assignment? sv1 = sv2; Before the assignment, some number of String objects are pointing to sv1. That number is unchanged by the assignment, because only sv1’s value changes(其实相当于进行换壳, 而壳是 String 类, 至于为什么可以换壳, 是因为 String 里有的只是对真实字符串数值的指针). Similarly, some number of String objects are pointing to sv2 prior to the assignment, and after the assignment, exactly the same String objects point to sv2. sv2’s reference count is also unchanged. When RCObjects are involved in an assignment, then, the number of objects pointing to those objects is unaffected, hence RCObject::operator= should change no reference counts.

  1. removeReference 函数中当引用计数为 0 时, 析构自己使用了 delete this 的操作, 这个操作只有在 *this 是个 heap 对象时才安全. 需要保证诞生于 heap 内(Item 27 的内容).
  2. destructor is a pure virtual function, a sure sign this class is designed to be used only as a base class.

StringValue 继承 RCObject:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class 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;
}

使用智能指针自动操作引用计数

StringrefCount 的操作能借助什么自动执行呢? 答案是使用智能指针.

思路: If we could somehow make the pointer itself detect these happenings and automatically perform the necessary manipulations of the refCount field, we’d be home free.

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
// template class for smart pointers-to-T objects. T must
// support the RCObject interface, typically by inheriting from RCObject
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; // dumb pointer
void init(); // common initialization, 减少代码重复
};

//下面是实现
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>
void RCPtr<T>::init()
{
if (pointee == 0) { // if the dumb pointer is null, so is the smart one
return;
}
if (pointee->isShareable() == false) { // if the value isn’t shareable, copy it
pointee = new T(*pointee);//浅拷贝
}
pointee->addReference(); // note that there is now a new reference to the value
}

template<class T>
RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs)
{
if (pointee != rhs.pointee) { // skip assignments where the value doesn’t change
if(pointee){
Pointee->removeReference();
init(); //如果可能, 共享, 否则做一份属于自己的副本
}
return *this;
}

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; }

注意开头的注释模板里的 T 必须是继承 RCObject 的类型 or at least that T provide all the functionality that RCObject does.

pointee = new T(*pointee); 会导致浅拷贝, 也就是只拷贝指针本身, 深层次的数据内容没有被拷贝过去(automatically generated copy constructors in C++, copy only StringValue’s data pointer; it will not copy the char* string data points to). 我们需要在继承的具体类 StringValue 中实现深拷贝的操作.

You should get into the habit of writing a copy constructor (and an assignment operator) for all your classes that contain pointers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class String {
private:
struct StringValue: public RCObject {
StringValue(const StringValue& rhs);
...
};
...
};

// 不使用默认的 copy constructor 而是构造自己的深拷贝 constructor
String::StringValue::StringValue(const StringValue& rhs)
{
data = new char[strlen(rhs.data) + 1];
strcpy(data, rhs.data);
}

另一个问题 pointee 的类型为 T*, 它可能指向 T 的 derived class. pointee = new T(*pointee); 会去调用 SpecialStringValue 的 copy constructor 而不是 StringValue 的. 解决办法是使用 virtual copy constructor.

1
2
3
4
5
6
class String {
private:
struct StringValue: public RCObject { ... };
struct SpecialStringValue: public StringValue { ... };
...
};

放在一起的完整版

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
template<class T> // template class for smart pointers-to-T objects; T must inherit from RCObject
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();
};

class RCObject { // base class for reference-counted objects
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:
size_t refCount;
bool shareable;
};

class String { // class to be used by application developers
public:
String(const char *value = "");
const char& operator[](int index) const;
char& operator[](int index);
private:
// class representing string values
struct StringValue: public RCObject {
char *data;
StringValue(const char *initValue);
StringValue(const StringValue& rhs);
void init(const char *initValue);
~StringValue();
};
RCPtr<StringValue> value;
};

String 类里没有 copy constructor, assignment operator, destructor, 这些是通过智能指针来完成的.

实现代码如下:

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
//RCObject
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; }

//RCPtr
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>
RCPtr<T>& RCPtr<T>::operator=(const RCPtr& rhs)
{
if (pointee != rhs.pointee) {
if(pointee){
Pointee->removeReference();
init();
}
return *this;
}

template<class T>
T* RCPtr<T>::operator->() const { return pointee; }

template<class T>
T& RCPtr<T>::operator*() const { return *pointee; }

//String::StringValue
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::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];
}

与不使用智能指针的 String 相比, the only changes are in operator[], where we call isShared instead of checking the value of refCount directly and where our use of the smart RCPtr object eliminates the need to manually manipulate the reference count during a copy-on-write.

加入智能指针最关键的是 The String interface has not changed. not one line of client code needs to be changed.

把引用计数加到既有的 classes 上

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
template<class T>
class RCIPtr {
public:
RCIPtr(T* realPtr = 0);
RCIPtr(const RCIPtr& rhs);
~RCIPtr();
RCIPtr& operator=(const RCIPtr& rhs);
T* operator->() const;
T& operator*() const;
RCObject& getRCObject()// give clients access to isShared, etc.
{ return *counter; }
private:
struct CountHolder: public RCObject
{
~CountHolder() { delete pointee; }
T *pointee;
};
CountHolder *counter;
void init();
};

template<class T>
void RCIPtr<T>::init()
{
if (counter->isShareable() == false) {
T *oldValue = counter->pointee;
counter = new CountHolder;
counter->pointee = oldValue ? new T(*oldValue) : 0;
}
counter->addReference();
}

template<class T>
RCIPtr<T>::RCIPtr(T* realPtr): counter(new CountHolder)
{
counter->pointee = realPtr;
init();
}

template<class T>
RCIPtr<T>::RCIPtr(const RCIPtr& rhs): counter(rhs.counter)
{ init(); }

template<class T>
RCIPtr<T>::~RCIPtr()
{ counter->removeReference(); }

template<class T>
RCIPtr<T>& RCIPtr<T>::operator=(const RCIPtr& rhs)
{
if (counter != rhs.counter) {
counter->removeReference();
counter = rhs.counter;
init();
}
return *this;
}

template<class T>
T* RCIPtr<T>::operator->() const
{ return counter->pointee; }

template<class T>
T& RCIPtr<T>::operator*() const
{ return *(counter->pointee); }

RCIPtrRCPtr 不同点: RCPtr 对象直接指向对象, RCIPtr 通过 CountHolder 指向对象.

if Widget looks like this:

1
2
3
4
5
6
7
8
9
10
class Widget
{
public:
Widget(int size);
Widget(const Widget &rhs);
~Widget();
Widget &operator=(const Widget &rhs);
void doThis();
int showThat() const;
};

RCWidget will be defined this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RCWidget
{
public:
RCWidget(int size) : value(new Widget(size)) {}
void doThis()
{
if (value.getRCObject().isShared())
{
value = new Widget(*value);// do COW if Widget is shared
}
value->doThis();
}
int showThat() const { return value->showThat(); }

private:
RCIPtr<Widget> value;
};

评估

究竟什么时候应该使用引用计数?

  • 相对较多的对象共享相对少量的实值.
    The higher the objects/values ratio, the better the case for reference counting.
  • 对象实值的产生或者销毁成本很高, 或者它们使用许多内存.

使用引用计数可能带来问题的场景:

  • Some data structures (e.g.directed graphs) lead to self-referential or circular dependency structures.

补充使用时的注意点: RCObjects 只能通过 heap 产生这一点需要客户保证. StringValue we limit its use by making it private in String. Only String can create StringValue objects, so it is up to the author of the String class to ensure that all such objects are allocated via new.

Item 30: Proxy classes.

二维数值对象实现

  • 不使用 [][] 而是使用 ()() 进行索引的简单方法:
1
2
3
4
5
6
template<class T>
class Array2D {
public:
Array2D(int dim1, int dim2);
...
};

使用:

1
2
3
4
5
6
7
Array2D<int> data(10, 20); // fine
Array2D<float> *data = new Array2D<float>(10, 20); // fine
void processInput(int dim1, int dim2)
{
Array2D<int> data(dim1, dim2); // fine
...
}
  • 类似地使用 () 进行索引:
1
2
3
4
5
6
7
8
9
10
template<class T>
class Array2D {
public:
// declarations that will compile
T& operator()(int index1, int index2);
const T& operator()(int index1, int index2) const;
...
};
//使用:
cout << data(3, 6);//don’t look like built-in arrays any more. like a function call.
  • overloading operator[] to return an object of a new class, Array1D. 执着于 [][] 索引.
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T>
class Array2D {
public:
class Array1D {
public:
T& operator[](int index);
const T& operator[](int index) const;
...
};
Array1D operator[](int index);
const Array1D operator[](int index) const;
...
};

使用:

1
2
3
Array2D<float> data(10, 20);
...
cout << data[3][6]; // fine

Array1D 类在用户眼里实际上并不存在, 用户接触到的整体行为还是二维数组. Array1D 用来代表(象征)其他对象, 这种对象被称为 proxy objects.
objects of such classes are also sometimes known as surrogates.

区分 operator [] 的读写操作

读取是右值运用(rvalue usages), 写是左值运用(lvalue usages). 区分读写即是区分左/右值的运用.

  • 最直接的想法是通过 const 性的重载来实现区分:
1
2
3
4
5
6
class String {
public:
const char& operator[](int index) const; // for reads
char& operator[](int index); // for writes
...
};

但是无法实现, 重载的区分点在调用函数的对象是否是 const 为基准, 而目的. Compilers choose between const and non-const member functions by looking only at whether the object invoking a function is const. No consideration is given to the context in which a call is made.

1
2
3
4
5
String s1, s2;
...
cout << s1[5];// calls non-const operator[], because s1 isn’t const
s2[5] = ’x’;// also calls non-const operator[]: s2 isn’t const
s1[3] = s2[8];// both calls are to non-const operator[], because both s1 and s2 are non-const objects
  • 使用 proxy

思路: [] 返回一个 proxy 中间对象, 后者再根据后续的场景(左值/右值)来判断到底是读还是写, 也就是 lazy evaluation.

There are only three things you can do with a proxy:

  1. 创建, Create it.
  2. 左值, Use it as the target of an assignment, in which case you are really making an assignment to the string character it stands for. When used in this way, a proxy represents an lvalue use of the string on which operator[] was invoked.
  3. 右值, Use it in any other way. When used like this, a proxy represents an rvalue use of the string on which operator[] was invoked.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class String { // reference-counted strings;
public:
class CharProxy { // proxies for string chars
public:
CharProxy(String& str, int index); // creation
CharProxy& operator=(const CharProxy& rhs); // lvalue uses
CharProxy& operator=(char c); // lvalue uses
operator char() const; // rvalue use
private:
String& theString; // 这个proxy 所附属的字符串
int charIndex;
};

//Because CharProxy::operator= isn’t a const member function,
//such proxies can’t be used as the target of assignments.
//实现了 const 重载的本意(防止了 CharProxy 类偷走一个 char 然后修改之)
const CharProxy operator[](int index) const; // for const Strings,return CharProxy objects.

CharProxy operator[](int index); // for non-const Strings,return CharProxy objects.
...
friend class CharProxy;
private:
RCPtr<StringValue> value;
};

其中延迟后的 proxy 的判断如下:

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
const String::CharProxy String::operator[](int index) const
{
//const_cast is necessary to satisfy the constraints of the CharProxy constructor,
//which accepts only a non-const String.
return CharProxy(const_cast<String&>(*this), index);
// CharProxy object returned by operator[] is itself const,
//so there is no risk the String containing the character
//to which the proxy refers will be modified.
}

String::CharProxy String::operator[](int index)
{
return CharProxy(*this, index);
}

String::CharProxy::CharProxy(String& str, int index): theString(str), charIndex(index) {}

//Conversion of a proxy to an rvalue
//return a copy of the character represented by the proxy:
String::CharProxy::operator char() const
{
return theString.value->data[charIndex];
}

//Conversion of a proxy to an lvalue
String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs)
{
if (theString.value->isShared()) { //检查可共享性 COW
theString.value = new StringValue(theString.value->data);
}
//创建新的 string data 然后进行赋值
theString.value->data[charIndex] =
rhs.theString.value->data[rhs.charIndex];
return *this;
}

String::CharProxy& String::CharProxy::operator=(char c)
{
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->data[charIndex] = c;
return *this;
}

限制

  1. operator 重载后返回的类型

In general, taking the address of a proxy yields a different type of pointer than does taking the address of a real object.

Stringoperator [] 返回值都是 CharProxy 类 而不是 char&.

1
2
String s1 = "Hello";
char *p = &s1[1]; // error!

因此需要 overload the address-of operators for the CharProxy 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
class String {
public:
class CharProxy {
public:
...
char * operator&();
const char * operator&() const;
...
};
...
};

const char * String::CharProxy::operator&() const
{
return &(theString.value->data[charIndex]);
}

char * String::CharProxy::operator&()
{
if (theString.value->isShared()) {
theString.value = new StringValue(theString.value->data);
}
theString.value->markUnshareable();
return &(theString.value->data[charIndex]);
}

但是还有其他运算符也可能遇到类似的问题, 例如其他需要左值的操作符 ++, += 等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class T> // reference-counted array using proxies
class Array {
public:
class Proxy {
public:
Proxy(Array<T>& array, int index);
Proxy& operator=(const T& rhs);
operator T() const;
...
};
const Proxy operator[](int index) const;
Proxy operator[](int index);
...
};

Array<int> intArray;
...
intArray[5] = 22; // fine
intArray[5] += 5; // error!
++intArray[5]; // error!

原因是 proxy 毕竟不是原类本身, 需要原类本身的性质的时候, 只能通过增加逻辑的方式, 将 proxy 模拟成原类.

  1. 调用原类的 member functions.
1
2
3
4
5
6
7
8
9
10
11
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const;
int denominator() const;
...
};

Array<Rational> array;
cout << array[4].numerator(); // error!
int denom = array[22].denominator(); // error!

To make proxies behave like the objects they stand for, you must overload each function applicable to the real objects so it applies to proxies, too.

  1. proxies fail to replace real objects is when being passed to functions that take references to non-const objects
1
2
3
void swap(char &a, char &b);// swaps the value of a and b
String s = "+C+";// oops, should be "C++"
swap(s[0], s[1]);// this should fix the problem, but it won’t compile

the char to which it may be converted can’t be bound to swap’s char& parameters, because that char is a temporary object (it’s operator char’s return value) and, as Item 19 explains, there are good reasons for refusing to bind temporary objects to non-const reference parameters.

  1. implicit type conversions

只能对 user-defined 的 implicit type conversion 进行一层, 存在限制.

As Item 5 explains, compilers may use only one user-defined conversion function when converting a parameter at a call site into the type needed by the corresponding function parameter.

但如上有时候反而可以利用这一点实现压抑隐式转换的功能.

评估

proxy class 的三个应用举例:

  • 多维数组
  • 左右值区分
  • 压抑隐式转换

disadvantages:

  • As function return values, proxy objects are temporaries (see Item 19), so they must be created and destroyed. That’s not free.
  • increases the complexity of software systems.
  • shifting from a class that works with real objects to a class that works with proxies often changes the semantics of the class.

Item 31 让函数根据一个以上的对象类型来决定如何虚化

问题引入

开发游戏中存在如下继承关系, 并要计算不同类对象之间碰撞(不同类型不同函数).

1
2
3
4
5
6
7
8
9
void checkForCollision(GameObject& object1, GameObject& object2)
{
if (theyJustCollided(object1, object2)) {
processCollision(object1, object2);
}
else {
...
}
}

What you need is a kind of function whose behavior is somehow virtual on the types of more than one object. C++ offers no such function.

其他语言里此特性: You could turn to CLOS, for example, the Common Lisp Object System. CLOS supports what is possibly the most general object-oriented function-invocation mechanism one can imagine: multi-methods.

专业术语, 把”虚函数调用动作”成为一个 message dispatch, 上面的这种根据两个参数而虚化被称为 double dispatch, 更多的被称为 multiple dispatch.

常规思维 虚函数+RTTI

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
class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
...
};

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);
process a SpaceShip-Asteroid collision;
}
else {
throw CollisionWithUnknownObject(otherObject);
}
}

固定第一个虚化的对象(*this), 穷举第二个虚化的可能性, 通过 typeid 判断类型然后用 static_cast 转换匹配类型, 最终执行具体的逻辑.

问题:

  1. 没有封装性可言(because each collide function must be aware of each of its sibling classes).
  2. if-else 链容易失误, 难以维护.

只使用虚函数

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
class SpaceShip; // forward declarations
class SpaceStation;
class Asteroid;

class GameObject {
public:
virtual void collide(GameObject& 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(GameObject& otherObject);
virtual void collide(SpaceShip& otherObject);
virtual void collide(SpaceStation& otherObject);
virtual void collide(Asteroid& otherobject);
...
};

//实现的一个例子, SpaceShip 为例
void SpaceShip::collide(GameObject& otherObject)
{
otherObject.collide(*this);
}

void SpaceShip::collide(SpaceShip& otherObject)
{
process a SpaceShip-SpaceShip collision;
}

void SpaceShip::collide(SpaceStation& otherObject)
{
process a SpaceShip-SpaceStation collision;
}

void SpaceShip::collide(Asteroid& otherObject)
{
process a SpaceShip-Asteroid collision;
}

思路: *this 为静态类型, 为调用的实例本身的类型, 这里是 SpaceShip, 然后根据入参的 GameObject 进行 RTTI 动态类型判断. 然后匹配到正确的函数重载.

问题:

  • each class must know about its siblings.
  • As new classes are added, the code must be updated. 一旦有新加入的同级类, base 类以及所有 derived 类都要修改, 全部需要重新编译.

自行仿真虚函数表格(virtual function tables)

放弃重载, 换成使用关系型数组的方式, 在运行时判断类型然后在关系数组中寻找匹配相应类型的函数指针. create an associative array that, given a class name, yields the appropriate member function pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
class GameObject {
public:
virtual void collide(GameObject& otherObject) = 0;
...
};
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
virtual void hitSpaceShip(SpaceShip& otherObject);
virtual void hitSpaceStation(SpaceStation& otherObject);
virtual void hitAsteroid(Asteroid& otherobject);
...
};

寻找的过程为 lookup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SpaceShip: public GameObject {
private:
//对 collide 这一类函数起别名
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
//声明 lookup 函数
static HitFunctionPtr lookup(const GameObject& whatWeHit);
...
};

//collide 调用 lookup 的过程
void SpaceShip::collide(GameObject& otherObject)
{
HitFunctionPtr hfp = lookup(otherObject); // find the function to call
if (hfp) { // if a function was found call it
(this->*hfp)(otherObject);
}
else {
throw CollisionWithUnknownObject(otherObject);
}
}

采用 STL 的 std::map 来实现关系型数组. 并且要保证其在第一次被调用时产生, main 结束后被销毁, 因此声明为 static 性的.

1
2
3
4
5
6
7
8
9
10
11
12
class SpaceShip: public GameObject {
private:
typedef void (SpaceShip::*HitFunctionPtr)(GameObject&);
typedef map<string, HitFunctionPtr> HitMap;
...
};

SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
...
}

利用 std::maplookup 的实现如下

1
2
3
4
5
6
7
SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
static HitMap collisionMap;
HitMap::iterator mapEntry= collisionMap.find(typeid(whatWeHit).name());
if (mapEntry == collisionMap.end()) return 0;
return (*mapEntry).second;
}

初始化 virtual function tables

使用一个 private static member function 实现对 std::map 的初始化, 这样可以实现 std::map 生成时把所有的条目放进去, 并且放进去的操作只有这一次. 注意是在子类里定义的 std::map 每个类自己查找自己的容器.

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
class SpaceShip: public GameObject {
private:
static HitMap * initializeCollisionMap();
...
};

SpaceShip::HitFunctionPtr SpaceShip::lookup(const GameObject& whatWeHit)
{
//使用智能指针管理 map 的资源周期
static auto_ptr<HitMap> collisionMap(initializeCollisionMap());
...
}

// 具体的初始化过程
// 所有的 hit* member function 的参数类型是一样的, 基类 GameObject 的引用
class SpaceShip: public GameObject {
public:
virtual void collide(GameObject& otherObject);
// these functions now all take a GameObject parameter
virtual void hitSpaceShip(GameObject& spaceShip);
virtual void hitSpaceStation(GameObject& spaceStation);
virtual void hitAsteroid(GameObject& asteroid);
...
};

SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = &hitSpaceShip;
(*phm)["SpaceStation"] = &hitSpaceStation;
(*phm)["Asteroid"] = &hitAsteroid;
return phm;
}

同时为了保证 map 中的 value 也就是函数指针的类型相同(function signature) 每个 hit* member function 的参数类型是一样的–>基类 GameObject 的引用.
下面的做法是不可取的, 即根据 std::map 的 key 不同, 对函数指针的参数类型进行异化.

1
2
3
4
5
6
7
8
SpaceShip::HitMap * SpaceShip::initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)["SpaceShip"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceShip);
(*phm)["SpaceStation"] = reinterpret_cast<HitFunctionPtr>(&hitSpaceStation);
(*phm)["Asteroid"] = reinterpret_cast<HitFunctionPtr>(&hitAsteroid);
return phm;
}

也就是用 reinterpret_cast 类型转换来欺骗编译器. 如果这么干的话, 如果遇到多重继承/虚拟继承的话会导致无法正确动态识别类型.

下面是对具体执行函数的实现, 注意因为大家接受的入参类型都是 base 类, 需要用 dynamic_cast 转成各自需要的 derived 类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void SpaceShip::hitSpaceShip(GameObject& spaceShip)
{
SpaceShip& otherShip=dynamic_cast<SpaceShip&>(spaceShip);
process a SpaceShip-SpaceShip collision;
}

void SpaceShip::hitSpaceStation(GameObject& spaceStation)
{
SpaceStation& station=dynamic_cast<SpaceStation&>(spaceStation);
process a SpaceShip-SpaceStation collision;
}

void SpaceShip::hitAsteroid(GameObject& asteroid)
{
Asteroid& theAsteroid = dynamic_cast<Asteroid&>(asteroid);
process a SpaceShip-Asteroid collision;
}

用 non-member function 实现执行函数

有 2 个动机使用 non-member function 指针替代 member function 指针作为 std::map 的元素.

  1. 新增一个元素会导致 recompilation, 即便那些不关心新加元素的用户.
  2. 哪里放入管理这些函数? 上面是放在一个子类中, 那问题是也可以放在另一个子类中. in which class should collisions between objects of different types be handled? 放在一个中立的位置?
    Wouldn’t it be better to design things so that collisions between objects of types A and B are handled by neither A nor B but instead in some neutral location outside both classes?

使用 non-member function, give clients header files that contain class definitions without any hit or collide 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
#include "SpaceShip.h"
#include "SpaceStation.h"
#include "Asteroid.h"
namespace
{ // unnamed namespace — see below primary collision-processing functions
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 &asteroid)
{
asteroidStation(asteroid, spaceStation);
}
...
// see below for a description of these types/functions
typedef void (*HitFunctionPtr)(GameObject &, GameObject &);
typedef map<pair<string, string>, HitFunctionPtr> HitMap;
pair<string, string> makeStringPair(const char *s1, const char *s2);
HitMap *initializeCollisionMap();
HitFunctionPtr lookup(const string &class1, const string &class2);
} // end namespace

void processCollision(GameObject &object1, GameObject &object2)
{
HitFunctionPtr phf = lookup(typeid(object1).name(),
typeid(object2).name());
if (phf)
phf(object1, object2);
else
throw UnknownCollision(object1, object2);
}

有几点需要注意:

  • 匿名的 namespace 的作用: 将声明与实现放在同一个 namespace 下面实现 internal linkage.
  • 碰撞的执行函数类型为两个, 因此顺序也有影响, 所以对称地制作了 $A_3^2$ 个函数, 其中一半互为对称.

实现如下

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
namespace {
pair<string,string> makeStringPair(const char *s1, const char *s2)
{ return pair<string,string>(s1, s2); }
} // end namespace

namespace {
HitMap * initializeCollisionMap()
{
HitMap *phm = new HitMap;
(*phm)[makeStringPair("SpaceShip","Asteroid")] = &shipAsteroid;
(*phm)[makeStringPair("SpaceShip", "SpaceStation")] = &shipStation;
...
return phm;
}
} // end namespace

namespace {
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;
}
} // end namespace

inheritance-based 情形

当需求说要增加如下继承关系时, 会变成什么情况?

  • your only practical recourse is to fall back on the double-virtual-function-call mechanism.
  • That implies you’ll also have to put up with everybody recompiling when you add to your inheritance hierarchy.

Initializing Emulated Virtual Function Tables (Reprise)

std::map 是静态的, Once we’ve registered a function for processing collisions between two types of objects, that’s it; we’re stuck with that function forever. What if we’d like to add, remove, or change collision-processing functions as the game proceeds? There’s no way to do it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CollisionMap
{
public:
typedef void (*HitFunctionPtr)(GameObject &, GameObject &);
void addEntry(const string &type1, const string &type2,
HitFunctionPtr collisionFunction, bool symmetric = true);//支持参数的顺序是否重要
void removeEntry(const string &type1, const string &type2);
HitFunctionPtr lookup(const string &type1, const string &type2);
static CollisionMap &theCollisionMap();

private:
// private 函数防止产生多个 map
CollisionMap();
CollisionMap(const CollisionMap &);
};

使用示例

1
2
3
4
5
6
7
8
9
void shipAsteroid(GameObject& spaceShip,GameObject& asteroid);
CollisionMap::theCollisionMap().addEntry("SpaceShip","Asteroid",&shipAsteroid);

void shipStation(GameObject& spaceShip,GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("SpaceShip","SpaceStation",&shipStation);

void asteroidStation(GameObject& asteroid,GameObject& spaceStation);
CollisionMap::theCollisionMap().addEntry("Asteroid","SpaceStation",&asteroidStation);
...

这种做法需要检查调用相应函数前是否在 std::map 中, 并且如果有新的 entry 需要改类, 重新编译类.

更好的方法是把 std::map 的操作变成接口, 随时可以调用, 修改也不用修改类.

1
2
3
4
5
6
7
8
class RegisterCollisionFunction {
public:
RegisterCollisionFunction(const string& type1,const string& type2,
CollisionMap::HitFunctionPtr collisionFunction, bool symmetric = true)
{
CollisionMap::theCollisionMap().addEntry(type1, type2, collisionFunction,symmetric);
}
};

在 main 之前注册

1
2
3
4
5
6
7
8
RegisterCollisionFunction cf1("SpaceShip", "Asteroid",&shipAsteroid);
RegisterCollisionFunction cf2("SpaceShip", "SpaceStation",&shipStation);
RegisterCollisionFunction cf3("Asteroid", "SpaceStation",&asteroidStation);
...
int main(int argc, char * argv[])
{
...
}

新增 entry

1
2
3
4
5
6
7
class Satellite: public GameObject { ... };
void satelliteShip(GameObject& satellite,GameObject& spaceShip);
void satelliteAsteroid(GameObject& satellite,GameObject& asteroid);

//注册
RegisterCollisionFunction cf4("Satellite", "SpaceShip",&satelliteShip);
RegisterCollisionFunction cf5("Satellite", "Asteroid",&satelliteAsteroid);

Item 32 在未来时态下发展程序

一些原则摘抄

  • One way to do this is to express design constraints in C++ instead of (or in addition to) comments or other documentation. 其实就是 C++20 里面的 concept 的思想.

  • Handle assignment and copy construction in every class, even if “nobody ever does those things.” If these functions are difficult to implement, declare them private . That way no one will inadvertently call compiler-generated functions that do the wrong thing.

  • strive to provide classes whose operators and functions have a natural syntax and an intuitive semantics. Preserve consistency with the behavior of the built-in types: when in doubt, do as the ints do.

  • Recognize that anything somebody can do, they will do.

  • Strive for portable code. 除非 performance be significant enough to justify unportable constructs.

  • Design your code so that when changes are necessary, the impact is localized. Encapsulate as much as you can; make implementation details private.

Future-tense thinking

  • Provide complete classes, even if some parts aren’t currently used.

  • Design your interfaces to facilitate common operations and prevent common errors.

  • If there is no great penalty for generalizing your code, generalize it.

Item 33: Make non-leaf classes abstract

本小节推荐一种原则, 直接理解字面意思不够明显, 参见下图:

初始的想法是类 C2 继承非抽象的实体类 C1, 这样 C1 就成了在继承体系树下的非叶子类, 作者给出的建议是尽量改成右边的类, 即新建一个抽象类 A, 让 C2C1 之间的关系通过接口的形式实现.

这样做的理由是把本该是直接继承关系的 2 个实体类, 通过一个共同的抽象类连接在一起, 共同的接口部分即是抽象类的内容.

问题就来了

  • 为什么要这么做?
  • 怎么做?
  • 什么情况下要这么做?
  • 什么情况下不要这么做?
  • 实在无法按照此建议做的时候该怎么做?

本节回答了上面 5 个问题.

为什么要这么做?

例如如下类设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
public:
Animal& operator=(const Animal& rhs);
...
};

class Lizard: public Animal {
public:
Lizard& operator=(const Lizard& rhs);
...
};

class Chicken: public Animal {
public:
Chicken& operator=(const Chicken& rhs);
...
};

调用情况:

1
2
3
4
5
6
Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;

不做任何进一步的思考仅凭语法写出的结构有如下问题:

最后一行的 liz1Animal 的成分会被成与 liz2 一致, 但 Lizard 的成分不会被改变, 这就是**部分赋值(partial assignments)**问题.

尝试解决:

让 assignment 操作符成为虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
public:
virtual Animal& operator=(const Animal& rhs);
...
};

class Lizard: public Animal {
public:
virtual Lizard& operator=(const Animal& rhs);
...
};

class Chicken: public Animal {
public:
virtual Chicken& operator=(const Animal& rhs);
...
};

这样做出现异形赋值:

1
2
3
4
5
6
Lizard liz;
Chicken chick;
Animal *pAnimal1 = &liz;
Animal *pAnimal2 = &chick;
...
*pAnimal1 = *pAnimal2; // assign a chicken to a lizard!

为了解决异形赋值问题:

利用 dynamic_cast 强制类型匹配

1
2
3
4
5
6
Lizard& Lizard::operator=(const Animal& rhs)
{
// make sure rhs is really a lizard
const Lizard& rhs_liz = dynamic_cast<const Lizard&>(rhs);
proceed with a normal assignment of rhs_liz to *this;
}

如果不能通过 dynamic_cast 确定 rhsLizard 类的对象时, 会抛出 exception, 类型是 std::bad_cast.

这么做的问题:

needlessly complicated and expensive, 不加区分, 下面的情况下应该不需要经过 cast 直接赋值即可.

1
2
3
Lizard liz1, liz2;
...
liz1 = liz2;// no need to perform a dynamic_cast: this assignment must be valid

解决办法:

对 assignment 进行重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Lizard: public Animal {
public:
virtual Lizard& operator=(const Animal& rhs); // 利用下面的 = 重载可以简化此函数
{
return operator=(dynamic_cast<const Lizard&>(rhs));
}
Lizard& operator=(const Lizard& rhs); // add this
...
};

Lizard liz1, liz2;
...
liz1 = liz2; // calls operator= taking a const Lizard&

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2; // calls operator= taking a const Animal&

问题:

  1. dynamic_cast 不一定被所有平台支持, 得考虑移植性(这条 “modern C++” 可以忽略了).
  2. 没有避免 exception.
  3. 能不能不在运行期而是在编译期杜绝问题的存在呢? 也就是说出现了不符合设计的动作连编译都不认通过.

基类 operator = 设为 private 属性

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
class Animal {
private:
Animal& operator=(const Animal& rhs);// this is now private
...
};

class Lizard: public Animal {
public:
Lizard& operator=(const Lizard& rhs);
...
};

class Chicken: public Animal {
public:
Chicken& operator=(const Chicken& rhs);
...
};

Lizard liz1, liz2;
...
liz1 = liz2; // fine

Chicken chick1, chick2;
...
chick1 = chick2; // also fine

Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &chick1;
...
*pAnimal1 = *pAnimal2; // error! attempt to call private Animal::operator=

Animal animal1, animal2;
...
animal1 = animal2; // error! attempt to call private Animal::operator=

问题:

  1. Animal 类的对象之间无法进行赋值.
  2. 无法通过基类指针多态地实现派生类的赋值.
  3. 派生类需要调用基类的赋值操作符, 无法调用 private 导致错误.

第二个问题可以通过 Animal::operator= 申明为 protected 来解决.

上面的探索过程, 我们可以得出结论这种继承结构不够合理. 这回答了第一个问题.

怎么做?

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
class AbstractAnimal {
protected:
AbstractAnimal& operator=(const AbstractAnimal& rhs);
public:
virtual ~AbstractAnimal() = 0;
...
};

class Animal: public AbstractAnimal {
public:
Animal& operator=(const Animal& rhs);
...
};

class Lizard: public AbstractAnimal {
public:
Lizard& operator=(const Lizard& rhs);
...
};

class Chicken: public AbstractAnimal {
public:
Chicken& operator=(const Chicken& rhs);
...
};

怎么做的关键点在于抽象类 AbstractAnimal 该如何实现.

  1. 没有 member data.
  2. 设置一个纯虚函数, 如果没有可用的 member function, 传统方法是将析构函数申明为纯虚函数.
  3. 为了支持多态, 基类总需要虚析构函数, 将它再多设为纯虚的唯一麻烦就是必须在类的定义之外实现它. 实现一个纯虚函数需要注意两点:
    1. 当前类是抽象类.
    2. 任何从此类派生的实体类必须将此函数申明为一个”普通”的虚函数(也就是说, 不能带 = 0).

什么情况下要这么做?

It forces the introduction of a new abstract class only when an existing concrete class is about to be used as
a base class
, i.e., when the class is about to be (re)used in a new context. Such abstractions are useful.

什么情况下不要这么做?

因为为我们还不完全了解的原型设计优秀的类几乎是不可能的. 所以在我们还不能保证自己掌握足够的信息做抽象类时, 最好不要这么做.

It is unlikely you could design a satisfactory abstract packet class unless you were well versed in many different kinds of packets and in the varied contexts in which they are used. Given your limited experience in this case, my advice would be not to define an abstract class for packets, adding one later only if you find a need to inherit from the concrete packet class(though recompile).

实在无法按照此建议做的时候该怎么做?

无法应用此建议的场景的确存在, 例如, 第三方的 C++ 库里的实体类只有读权, 无法修改其继承自一个抽象类.

作者给出了偏重于各种角度的做法如下:

  • 从已存在的实体类派生出你的实体类, 自己多加小心部分赋值与异性赋值.
  • 试图在类库的继承树的更高处找到一个完成了你所需的大部分功能的抽象类, 从它进行继承. 缺点是重复造轮子, you may have to duplicate a lot of effort that has already been put into the implementation of the concrete class whose functionality you’d like to extend.
  • 以”你所希望继承的那个程序库类”来实现你的新类. 例如, 把库里的类的对象变成你的新类的 data member, 然后在新类中重新实现接口. 缺点是
    1. update your class each time the library vendor updates the class on which you’re dependent.
    2. forgo the ability to redefine virtual functions declared in the library class, because you can’t redefine virtual functions unless you inherit them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Window {
// this is the library class
public:
virtual void resize(int newWidth, int newHeight);
virtual void repaint() const;
int width() const;
int height() const;
};

class SpecialWindow {
// this is the class you wanted to have inherit
public:
...
// from Window pass-through implementations of nonvirtual functions
int width() const { return w.width(); }
int height() const { return w.height(); }
// new implementations of "inherited" virtual functions
virtual void resize(int newWidth, int newHeight);
virtual void repaint() const;
private:
Window w;
};
  • 手上有什么就用什么, 包括使用 non-member functions.

Item 34 如何在同一个程序中结合 C++ 和 C

确定 C++ 和 C 编译器产出兼容的目标文件

这是下面讨论的前提条件.

将双方都会使用的函数声明为 extern "C"

Name Mangling

In C you can’t overload function names ==> no name mangling.

linkers usually insist on all function names being unique. ==> C++ has name mangling.

因此, you need a way to tell your C++ compilers not to mangle certain function names. ==> extern “C” directive:

extern “C”的用法:

  • 声明 C 函数.
  • 声明 a function in assembler.
  • declare C++ functions, 方便 debug 出函数名字: your clients can use the natural and intuitive names you choose instead of the mangled names your compilers would otherwise generate.
  • 多个函数一起声明.
1
2
3
4
5
6
extern "C" {
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
}
  • 区分 C/C++ 编译器使用 preprocessor symbol __cplusplus
1
2
3
4
5
6
7
8
9
10
11
#ifdef __cplusplus
extern "C"
{
#endif
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
#ifdef __cplusplus
}
#endif

关于 name mangling 以及 extern "C"的用法可以参见博文.

Initialization of Statics

static initialization 与 static destruction: 在 main 函数之前, 编译器会执行一些函数, 执行构造 static class 对象, 全局对象, namespace 内的对象以及文件范围(file scope) 内的对象. 在 main 后执行析构这些 static 性质的对象.

static initialization: 例如 the constructors of static class objects and objects at global, namespace, and file scope are usually called before the body of main is executed.
同样地, objects that are created through static initialization must have their destructors called during static destruction; that process typically takes place after main has finished executing.

例如一些编译器的下面的做法(插入代码式的):

1
2
3
4
5
int main(int argc, char *argv[])
{
performStaticInitialization(); // generated by the implementation the statements you put in main go here;
performStaticDestruction(); // generated by the implementation
}

沿袭这种思想: Just rename the main you wrote in C to be realMain, then have the C++ version of main call realMain:

1
2
3
4
5
6
7
8
extern "C"
// implement this
int realMain(int argc, char *argv[]); // function in C

int main(int argc, char *argv[]) // write this in C++
{
return realMain(argc, argv);
}

Dynamic Memory Allocation

动态内存分配时, delete ~ new, malloc ~ free 配对

你没法判断给到的指针指向的对象到底是 new 出来的还是 malloc 出来的.

1
char * strdup(const char *ps); // return a copy of the string pointed to by ps

数据结构兼容性

C 能接受哪些 C++ 的数据类型?

  • 内建数据类型
  • 一般指针, functions in the two languages can safely exchange pointers to objects and pointers to non-member or static functions.
  • trivial struct(memory layout should not change, POD). 例如没有 non-virtual functions. 没有 base class.

Item 35 让自己习惯于标准 C++ 语言

标准的重要意义: The ISO/ANSI standard for C++ is what vendors will consult when implementing compilers, what authors will examine when preparing books, and what programmers will look to for definitive answers to questions about C++.

标准库区分如下大项:

  • 支持标准 C 运行库.
  • 支持 string 类型.
  • 支持国别(地域, 本土化, localization).
  • 支持 I/O 操作.
  • 支持数值应用, 例如复数.
  • 支持容器与算法(STL).
作者

cx

发布于

2022-04-14

更新于

2022-11-23

许可协议