C++ 工程实践(8):值语义 (二)

2014-11-24 12:52:37 · 作者: · 浏览: 2
arent(myParent_)
{
}

private:
boost::weak_ptr myParent;
};
typedef boost::shared_ptr ChildPtr;


class Parent : public boost::enable_shared_from_this,
private boost::noncopyable
{
public:
Parent()
{
}

void addChild()
{
myChild.reset(new Child(shared_from_this()));
}

private:
ChildPtr myChild;
};

int main()
{
ParentPtr p(new Parent);
p->addChild();
}

上面这个 shared_ptr+weak_ptr 的做法似乎有点小题大做。

考虑一个稍微复杂一点的对象模型:a Child has parents: mom and dad; a Parent has one or more Child(ren); a Parent knows his/her spouser. 这个对象模型用 Java 表述一点都不复杂,垃圾收集会帮我们搞定对象生命期。

public class Parent
{
private Parent mySpouser;
private ArrayList myChildren;
}

public class Child
{
private Parent myMom;
private Parent myDad;
}
如果用 C++ 来实现,如何才能避免出现空悬指针,同时避免出现内存泄漏呢?借助 shared_ptr 把裸指针转换为值语义,我们就不用担心这两个问题了:

class Parent;
typedef boost::shared_ptr ParentPtr;

class Child : boost::noncopyable
{
public:
explicit Child(const ParentPtr& myMom_,
const ParentPtr& myDad_)
: myMom(myMom_),
myDad(myDad_)
{
}

private:
boost::weak_ptr myMom;
boost::weak_ptr myDad;
};
typedef boost::shared_ptr ChildPtr;

class Parent : boost::noncopyable
{
public:
Parent()
{
}

void setSpouser(const ParentPtr& spouser)
{
mySpouser = spouser;
}

void addChild(const ChildPtr& child)
{
myChildren.push_back(child);
}

private:
boost::weak_ptr mySpouser;
std::vector myChildren;
};

int main()
{
ParentPtr mom(new Parent);
ParentPtr dad(new Parent);
mom->setSpouser(dad);
dad->setSpouser(mom);
{
ChildPtr child(new Child(mom, dad));
mom->addChild(child);
dad->addChild(child);
}
{
ChildPtr child(new Child(mom, dad));
mom->addChild(child);
dad->addChild(child);
}
}

如果不使用 smart pointer,用 C++ 做面向对象编程将会困难重重。

值语义与标准库
C++ 要求凡是能放入标准容器的类型必须具有值语义。准确地说:type 必须是 SGIAssignable concept 的 model。但是,由 于C++ 编译器会为 class 默认提供 copy constructor 和 assignment operator,因此除非明确禁止,否则 class 总是可以作为标准库的元素类型——尽管程序可以编译通过,但是隐藏了资源管理方面的 bug。

因此,在写一个 class 的时候,先让它继承 boost::noncopyable,几乎总是正确的。

在现代 C++ 中,一般不需要自己编写 copy constructor 或 assignment operator,因为只要每个数据成员都具有值语义的话,编译器自动生成的 member-wise copying&assigning 就能正常工作;如果以 smart ptr 为成员来持有其他对象,那么就能自动启用或禁用 copying&assigning。例外:编写 HashMap 这类底层库时还是需要自己实现 copy control。

值语义与C++语言
C++ 的 class 本质上是值语义的,这才会出现 object slicing 这种语言独有的问题,也才会需要程序员注意 pass-by-value 和 pass-by-const-reference 的取舍。在其他面向对象编程语言中,这都不需要费脑筋。

值语义是C++语言的三大约束之一,C++ 的设计初衷是让用户定义的类型(class)能像内置类型(int)一样工作,具有同等的地位。为此C++做了以下设计(妥协):

class 的 layout 与 C struct 一样,没有额外的开销。定义一个“只包含一个 int 成员的 class ”的对象开销和定义一个 int 一样。
甚至 class data member 都默认是 uninitialized,因为函数局部的 int 是 uninitialized。
class 可以在 stack 上创建,也可以在 heap 上创建。因为 int 可以是 stack variable。
class 的数组就是一个个 class 对象挨着,没有额外的 indirection。因为 int 数组就是这样。
编译器会为 class 默认生成 copy constructor 和 assignment operator。其他语言没有 copy constructor 一说,也不允许重载 assignment operator。C++ 的对象默认是可以拷贝的,这是一个尴尬的特性。
当 class type 传入函数时,默认是 make a copy (除非参数声明为 reference)。因为把 int 传入函数时是 make a copy。
当函数返回一个 class type 时,只能通过 make a copy(C++ 不得不定义 RVO 来解决性能问题)。因为函数返回 int 时是 make a copy。
以 class type 为成员时,数据成员是嵌入的。例如 pair, size_t> 的 layout 就是 complex 挨着 size_t。
这些设计带来了性能上的好处,原因是 memory locality。比方说我们在 C++ 里定义 complex class,array of complex, vector >,它们的 layout 分别是:(re 和 im 分别是复数的实部和虚部。)

value1

而如果我们在 Java 里干同样的事情,layout 大不一样,memory locality 也差很多:

value2

Java 里边每个 object 都有 header,至少有两个 wo