2

shared_ptr

很多编程语言都有GC的机制,可以自动管理内存资源,然后GC机制带来的是资源释放的不确定性,c++原始的手工管理内存资源的方式虽然具有释放的确定性,但是人工管理非常容易出错;如何既能自动释放内存又能保证确定性呢,modern c++给出的方案是shared_ptr。

从c++11开始引入的shared_ptr,用来表示指针对指向对象的“共享所有权”;一个对象可以被多个shared_ptr指向和访问,这些shared_ptr类型的指针共同享有该对象的所有权,当最后一个指向该对象的shared_ptr生命周期结束的时候,对象被销毁。

shared_ptr基于引用计数实现,shared_ptr的构造将引用计数加1,销毁的时候引用计数减1,而赋值则将源指针引用计数加1,目标指针引用计数减1,例如P1=P2,P1指向对象的引用计数减1,P2指向对象的引用计数加1。当引用计数减1之后为0的时候,shared_ptr将会销毁指向的对象。

存储引用计数的空间是动态分配的。此外,为了线程安全,引用计数的加减都必须是原子操作,原子操作的实现带来了性能上的损耗;上面提到,shared_ptr的构造函数函数会增加引用计数,但是移动构造除外,因为移动构造并没有增加指向对象的引用计数,所以不需要改变引用计数;

与unique_ptr类似,shared_ptr同样也支持自定义销毁方法(默认是直接调用delete),与unique_ptr不同的是,销毁方式是unique_ptr类型的一部分,而shared_ptr的销毁方式却不是。

auto loggingDel = [](Widget *pw)
                  {
                      makeLogEntry(pw);
                      delete pw;
                  };
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
std::shared_ptr<Widget> spw(new Widget, loggingDel);

不把销毁方式作为shared_ptr类型的一部分可以带来更大的灵活性,因为这里不同的shared_ptr<Widget>指针对象可能需要不同的销毁方式

auto customDeleter1 = [](Widget *pw) { … }; // custom deleters,
auto customDeleter2 = [](Widget *pw) { … }; // each with adifferent type
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

另一个与unique_ptr不同的是,自定义销毁方式并不会改变shared_ptr的size,shared_ptr的size始终是两倍的裸指针size,其内存布局是如下图所示:

clipboard.png

由图中可以看到,实际上引用计数、自定义销毁等都不是直接存储在shared_ptr中,而是通过一个指针指向的一个控制块存储的,控制块是动态分配的内存,对shared_ptr进行不同的操作时,需要判断需要不要分配新的控制块,控制块的分配主要有以下几种情况:

  1. 使用std::make_shared的时候总是分配控制块

  2. shared_ptr由unique_ptr或裸指针构建时分配控制块

  3. shared_ptr由其他shared_ptr或weak_ptr构建时不分配新的控制块,而是沿用既有智能指针的控制块

由上面的2引出的一个问题,当我们用一个裸指针构建多个shared_ptr时,会分配多个控制块,这就导致一个问题,同一个对象确有多个引用计数(控制块),这就很容易导致一个对象被销毁多次,下面的代码描述了这种情况:

auto pw = new Widget; // pw is raw ptr
//…
std::shared_ptr<Widget> spw1(pw, loggingDel); // create control block for *pw
std::shared_ptr<Widget> spw2(pw, loggingDel); // create 2nd control block

有两个方法可以避免上面的问题发生,
1 尽可能避免使用裸指针来构建shared_ptr,使用make_shared
2 必须使用裸指针的话,new出对象后直接传入,而不是将指针传入

std::shared_ptr<Widget> spw1(new Widget, loggingDel);

另外一种情况是涉及到this指针的问题,假如在某个类的实现代码中,用this指针构建了一个shared_ptr的时候:

class Widget {
public:
…
void process(){
    …
    processedWidgets.emplace_back(this);    //add it to the list of processed widgets
}
…
private:
    std::vector<std::shared_ptr<Widget>> processedWidgets;
};

上面这段代码是可以正常编译的,这里使用this指针构建了一个shared_ptr并保存到了一个vector中,这就给当前对象生成了一个控制块,但是实际上,由于外部使用shared_ptr管理这个对象,该对象已经存在一个控制块了;

std::shared_ptr<Widget> spw1(new Widget);//产生一个控制块

为了解决这个问题,标准库引入了一个新的模板类enable_shared_from_this,自定义的类可以通过继承这个模板类来规避由this指针构建shared_ptr的问题,上面的代码修改之后如下面所示:

class Widget: public std::enable_shared_from_this<Widget> {
public:
    …
    void process()
    {
        // as before, process the Widget
        …
        // add std::shared_ptr to current object to processedWidgets
        processedWidgets.emplace_back(shared_from_this());
    }
    …
};

通过调用enable_shared_from_this类的shared_from_this函数来获取对象本身的shared_ptr指针,就不会再创建一个新的控制块了。shared_from_this会自动去查找关联了当前对象的控制块,并创建一个shared_ptr指针引用已有的控制块,实际的情况中,对象函数被调用必然是在对象已经存在的前提下,所以当前对象关联的控制块总是存在的,如果shared_from_this未查找到当前对象关联的控制块,就会导致未定义行为,通常会抛出一个异常。

为了避免上面的情况,可以把类的构造函数设为私有,再通过一个工厂方法函数返回shared_ptr来确保客户端在调用的时候对象一定关联了控制块

class Widget: public std::enable_shared_from_this<Widget> {
public:
    // factory function that perfect-forwards args
    // to a private ctor
    template<typename... Ts>
    static std::shared_ptr<Widget> create(Ts&&... params);
    …
    void process(); // as before
    …
private:
    … // ctors
};

关于shared_ptr性能的讨论
shared_ptr的控制块是动态生成的,尽管占用的空间并不大,但是控制块的实际实现比想象的要复杂,实现控制块使用到了继承和虚函数,同时引用计数的增减是原子操作也增加了性能上的代价,这些都导致了shared_ptr并不是管理所有动态资源的最好方案,使用shared_ptr解引用获取对象时会比直接使用裸指针的代价更高;

然而,尽管shared_ptr有在性能上付出了一定的代价,其带来的收益是非常显著的,shared_ptr解决了动态分配资源的生命周期自动管理,大多数时候,在“共享所有权”的语义下,使用shared_ptr管理动态资源都是值得推荐的;而没有“共享所有权”语义的其他情况下,例如“独占所有权”,则可以使用unique_ptr来代替;

另一个shared_ptr不能做的事情是管理数组,不能吃std::shared_ptr<T[]>这样的类型,然而,c++ 11之后标准库已经引入了std::array,shared_ptr管理一个std::array类型的对象是可行的。

shared_ptr的注意点

  • shared_ptr可以用来管理具有“共享所有权”语义的动态资源,可以自动管理对象的生命周期和GC

  • 由于control_block的存在,shared_ptr的size通常是2倍裸指针或unique_ptr的大小,此外,shared_ptr的引用计数增减是原子操作

  • shared_ptr同样支持自定义销毁方式,且自定义销毁方式与shared_ptr类型无关

  • 避免使用裸指针构建shared_ptr,在有通过this指针构建shared_ptr的情况下要继承std::enable_shared_from_this


Keybord_dancer
387 声望6 粉丝