智能指针实践,C++真不需要垃圾回收

来源:互联网 发布:灯光编程教学视频 编辑:程序博客网 时间:2024/05/01 11:17

说来也许不能相信,自己之前并没有在软件开发中大规模引入智能指针,担心效率啊,在vector里面塞满了shared_ptr,一旦遍历,想想都心痛。


如果给有10万元素的vector和list中,随机删除和插入 4 byte大小的node,哪个效率更高?不要被臆想欺骗, vector 效率更高, 你信么?Bjarne 在他的 C++11 Style – A Touch of Class 中提到过这个问题,也是我第一次看到,各种震惊。cpp-benchmark-vector-list-deque做了更详细评测。对于软件效率,想象的未必是真实的。

我对智能指针的担心只是臆测。实践之后,效率问题?Performance analysis之后,我没有发现是在智能指针这里,绝对没有。不要担心我的shared_ptr的vector不够大,遍历的不够多。那个软件是做log分析的,几千万行的log。

效率问题不用担心,事情并没有万事大吉,细节总是在实际应用中浮出水面。相比较而言,效率问题,早就置之脑后了。


std::enable_shared_from_this这个class,熟悉么,它必要存在么?如果你在努力回忆在哪里仿佛看到过,那么你一定没有实践过用智能指针管理一切资源。如果你很熟悉,大量使用过,那么我知道的那点,也许你都早已熟悉了。


如果使用智能指针管理内存,那么就不要出现raw指针。怎么检查,假设某个类为Node,搜一下代码中有没有显示的new和delete,再搜一下有没有Node*这样的变量。这难道很极端吗?


智能指针的创建使用std::make_shared<Node>,因为shared_ptr里面有计数的成员需要占用内存,如果能把计数成员的内存和node本身内存分配成连续的,提高cache的命中率。这就是make_shared为我们做的,用连续的内存存放node本身和shared_ptr里面的成员。有人测试过对比分别创建node和shared_ptr,觉得效率区别不大。我以为是测试代码相对简单,尽管分别创建,但实际上他们内存连续。如果node和shared_ptr创建间距比较长,程序小对象又很多的情况下,差别应该有。我刚说完不要猜测,现在又在猜测了,眼高手低,通病啊。

 

继续make_shared<Node>,vc2012的实现中Node构造函数的参数个数最多为5个。大概是坐等variadic templates实现后重写此类变参数的函数,vs2012竟然懒到给定变参数个数为5, 如果实际使用1个参数,那么就填充其余4个参数。5个参数够不够,大多数情况够用了,不过我中招了,我本来想加第6个参数的,没有加成,才发现这个问题。


假设我想在Node中建立Visitor机制。

class Visitor{public:    virtual void visit(std::shared_ptr<Node> node) = 0;};

Node对应有accept方法

void Node::accept(Visitor & visitor){    visitor.visit(/*麻烦来了,怎么从创建一个shared_ptr,并且value是this指针*/)}

new一个shared_ptr, 把this指针穿进去肯定是不对的,这样推出visit调用之后,this就被销毁了。因为新创建的shared_ptr和this代表的Node实例没有共享引用计数,如果新创建的shared_ptr能够使用this对象已有的计数对象,并且应用计数加1,那么一切都能完美工作。思路正确了,enable_shared_from_this就是在做这件事情。

class Node : public std::enable_shared_from_this<Node>
{
public:
    virtual void accept(Visitor & visitor);
};

enable_shared_from_this中含有引用计数的成员,更准确的说是一个weak_ptr。Node从enable_shared_from_this继承,之后,所有基于Node实例创建的shared_ptr对象都共享同一个引用计数成员,而且这个成员就是隶属于基类enable_shared_from_this。这样accept方法就好写了

void Node::accept(Visitor & visitor){    visitor.visit(this->shared_from_this());}

事情还没有结束

class SubNode : public Node{public:    virtual void accept(Visitor & visitor);};
class Visitor{public:    virtual void visit(std::shared_ptr<Node> node) = 0;    virtual void visit(std::shared_ptr<SubNode> node) = 0;};
void SubNode::accept(Visitor & visitor){    visitor.visit(/*this->shared_from_this()返回的是std::shared_ptr<Node>,我们期望的是std::shared_ptr<SubNode>*/);}

this实际上是SubNode,只不过shared_from_this返回的是一个Node类表示的SubNode shared_ptr对象。如果不考虑shared_ptr对象,我们可以用dynamic_cast把Node*转换成SubNode*,甚至用static_cast也未尝不可。与之类似的, stl提供了 dynamic_pointer_cast,static_pointer_cast, const_pointer_cast。所以任务解决了:

void SubNode::accept(Visitor & visitor){    visitor.visit(std::dynamic_cast<SubNode>(this->shared_from_this()));}


之前提到过weak_ptr,就是shared_from_this中包含的,持有引用计数的成员。weak_ptr能知道对象的引用计数情况,但是自己不对计数进行修改。weak_ptr可以从shared_ptr中转化而来,也可以从weak_ptr中创建一个新的shared_ptr。转来转去,核心还是在共享引用计数。一个对象实例,只有一份引用计数。weak_ptr是要被经常使用到的。

考虑两个class相互引用的情况

struct NodeParent{    std::shared_ptr<NodeSub> mSub;};struct NodeSub{    std::shared_ptr<NodeParent> mParent;};int main(){    auto parent = std::make_shared<NodeParent>();    auto sub    = std::make_shared<NodeSub>();    parent.mSub = sub;    sub.mParent = parent;}

程序结束时,a和b都不会被释放掉,内存泄露。解决方法,就是对互相引用的情况,要分主次,一个是shared_ptr引用,一个是weak_ptr引用。

struct NodeParent{    std::shared_ptr<NodeSub> mSub;};struct NodeSub{    std::weak_ptr<NodeParent> mParent;};
全部使用智能指针后,还有内存泄露,那么基本就是这种情况。 实际中,也许类之间的从属关系没那么清楚,容易犯错误。我自己也犯这个错误。不过日子比以前好多了,不会有野指针的crash,不会double delete,使用内存工具检查下,很快就能把所有leak改掉。 

NodeSub如果要访问mParent的方法,首先要检查mParent是否依然正确,因为mParent这个weak_ptr可以看到parent对象的引用计数,它知道parent对象是否计数为0,已经被释放。 mParent->expired()就是检查对象是否依然有效的方法。如果没有过期,使用mParent->lock()返回一个shared_ptr<NodeParent>对象。

到此为止,关于智能指针的用法就这么多了。 在加上RAII机制,unique_ptr,自定义delete function,垃圾回收机制除了语法上更简单点之外, 没有提供更多的便利。


智能指针用法介绍结束,实践仍然继续。

假设程序打开了一个文件,创建了对象模型,他们都出于智能指针管理之下,假设根节点是mRoot,然后把对象模型数据显示在了GUI之上。

紧接着,需要打开另外一个文件,需要重新创建对象模型。mRoot = newRoot。 这时,mRoot所代表的对象模型不一定就被删除了,因为GUI或者别的地方依然还有人在引用它。 如果两个文件都很大,那么在这个时间里,程序占用了2倍的内存, 导致软件的处理大文件能力显著下降。

我想表达的是,虽然我们使用了智能指针,不会有泄露,对象最后一定会被释放,但是我们依然需要设计,比如在切换文件之前,要通知所以模块,清空对mRoot极其子对象的引用。 

这个例子不是我杜撰的,是我遇到的真实的例子。软件需要设计,需要细心的控制对象生存周期。垃圾回收所倡导的方法并不引导向这个方向努力。


继续吐槽下boost

因为依赖于boost::serialization库,它竟然对C++标准里的shared_ptr不支持,只支持boost::shared_ptr。 我就被迫一部分对象使用boost库的智能指针,一部分使用stl的。 他们工作的很和谐!










原创粉丝点击