高度なC++:スマートポインターのshared_ptr

創造を続け、成長を加速させましょう!「ナゲッツデイリーニュープラン・6月アップデートチャレンジ」に参加した初日です。クリックしてイベントの詳細をご覧ください。

std::shared_ptrコンセプト

その制限(排他的所有権)のため、unique_ptrがマルチスレッド操作に使用されることはめったにありません。マルチスレッド操作では、リソースを共有したり、リソースを自動的に解放したりできるため、shared_ptrが導入されます。

クロススレッドアクセスをサポートするために、shared_ptrには、現在リソースを使用しているshared_ptrsの数を記録するために使用される参照カウント(スレッドセーフ)があります。関連するリソースを自動的に解放します。

機能unique_ptrの排他的所有権とは対照的に、shared_ptrは所有権を共有できます。その中には、リソースを共有するshared_ptrの数を記録するために使用される参照カウントがあります。共有カウントが0の場合、関連するリソースは自動的に解放されます。

unique_ptrと比較すると、shared_ptrは配列をサポートしていないため、shared_ptrを使用して配列を指す場合は、次に示すように、自分で削除機能を手動で実装する必要があります。

std::shared_ptr<int> p(new int[8], [](int *ptr){delete []ptr;});
复制代码

shared_ptrテンプレートクラス

template<class T> class shared_ptr {
  public:
    using element_type = remove_extent_t<T>;
    using weak_type    = weak_ptr<T>;
 
    // 构造函数
    constexpr shared_ptr() noexcept;
    constexpr shared_ptr(nullptr_t) noexcept : shared_ptr() { }
    template<class Y> explicit shared_ptr(Y* p);
    template<class Y, class D> shared_ptr(Y* p, D d);
    template<class Y, class D, class A> shared_ptr(Y* p, D d, A a);
    template<class D> shared_ptr(nullptr_t p, D d);
    template<class D, class A> shared_ptr(nullptr_t p, D d, A a);
    template<class Y>
    shared_ptr(const shared_ptr<Y>& r, element_type* p) noexcept;
    template<class Y>
    shared_ptr(shared_ptr<Y>&& r, element_type* p) noexcept;
    shared_ptr(const shared_ptr& r) noexcept;
    template<class Y> shared_ptr(const shared_ptr<Y>& r) noexcept;
    shared_ptr(shared_ptr&& r) noexcept;
    template<class Y> shared_ptr(shared_ptr<Y>&& r) noexcept;
    template<class Y> explicit shared_ptr(const weak_ptr<Y>& r);
    template<class Y, class D> shared_ptr(unique_ptr<Y, D>&& r);
 
    // 析构函数
    ~shared_ptr();
 
    // 赋值
    shared_ptr& operator=(const shared_ptr& r) noexcept;
    template<class Y>
    shared_ptr& operator=(const shared_ptr<Y>& r) noexcept;
    shared_ptr& operator=(shared_ptr&& r) noexcept;
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y>&& r) noexcept;
    template<class Y, class D>
    shared_ptr& operator=(unique_ptr<Y, D>&& r);
 
    // 修改函数
    void swap(shared_ptr& r) noexcept;
    void reset() noexcept;
    template<class Y> void reset(Y* p);
    template<class Y, class D> void reset(Y* p, D d);
    template<class Y, class D, class A> void reset(Y* p, D d, A a);
 
    // 探察函数
    element_type* get() const noexcept;
    T& operator*() const noexcept;
    T* operator->() const noexcept;
    element_type& operator[](ptrdiff_t i) const;
    long use_count() const noexcept;
    explicit operator bool() const noexcept;
    template<class U>
    bool owner_before(const shared_ptr<U>& b) const noexcept;
    template<class U>
    bool owner_before(const weak_ptr<U>& b) const noexcept;
  };
复制代码

shared_ptr同じオブジェクトへの複数のポインター。shared_ptrは参照カウントを使用し、shared_ptrの各コピーは同じメモリを指します。使用するたびに、内部参照カウントが1ずつ増加し、破棄されるたびに、内部参照カウントが1ずつ減少します。0に減少すると、ポイントされたヒープメモリは自動的に削除されます。shared_ptr内の参照カウントはスレッドセーフですが、オブジェクトの読み取りにはロックが必要です。

  • 初期化。スマートポインターは、型を指定できるテンプレートクラスであり、着信ポインターはコンストラクターを介して初期化されます。make_shared関数を使用して初期化することもできます。1つはクラス用、もう1つはポインター用のスマートポインターに直接ポインターを割り当てることはできません。たとえばstd::shared_ptr<int> p4 = new int(1);、スペルが間違っているため、暗黙的に変換することはできません。
  • コピーと割り当て。コピーするとオブジェクトの参照カウントが1増加し、割り当てると元のオブジェクトの参照カウントが1減少します。カウントが0の場合、メモリは自動的に解放されます。後で指すオブジェクトの参照カウントは1ずつ増加し、後のオブジェクトを指します。
  • get関数は生のポインタを取得します。
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用我们在后面的weak_ptr中介绍。

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象:

std::shared_ptr<int> pi;
int* p_reg = new int;
//pi = p_reg;  // not allowed(implicit conversion)
pi = std::shared_ptr<int>(p_reg);  // allowed(explicit conversion)
//std::shared_ptr<int> pshared = p_reg;  // not allowed(implicit conversion)
//std::shared_ptr<int> pshared(g_reg);  // allowed(explicit conversion)
复制代码

下面我们看一个简单的例子:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    std::shared_ptr<int> sp = std::make_shared<int>(10);
    cout << sp.use_count() << endl;//1
    std::shared_ptr<int> sp1(sp);//再次被引用则计数+1
    cout << sp1.use_count() << endl;//2
}
复制代码

从上面可以看到,多次被引用则会增加计数,我们可以通过使用use_count方法打印具体的计数。

shared_ptr的构造和析构

#include <iostream>
#include <memory>

struct C {int* data;};

int main () {
 auto deleter = [](int* ptr){
    std::cout << "custom deleter called\n";
    delete ptr;
  };//Labmbda表达式

  //默认构造,没有获取任何指针的所有权,引用计数为0
  std::shared_ptr<int> sp1;
  std::shared_ptr<int> sp2 (nullptr);//同1
  //拥有指向int的指针所有权,引用计数为1
  std::shared_ptr<int> sp3 (new int);
  //同3,但是拥有自己的析构方法,如果指针所指向对象为复杂结构C
  //结构C里有指针,默认析构函数不会将结构C里的指针data所指向的内存释放,
  //这时需要自己使用自己的析构函数(删除器)
  std::shared_ptr<int> sp4 (new int, deleter);
  //同4,但拥有自己的分配器(构造函数),
  //如成员中有指针,可以为指针分配内存,原理跟浅拷贝和深拷贝类似                         
  std::shared_ptr<int> sp5 (new int, [](int* p){delete p;}, std::allocator<int>());
  //如果p5引用计数不为0,则引用计数加1,否则同样为0, p6为0
  std::shared_ptr<int> sp6 (sp5);
  //p6的所有权全部移交给p7,p6引用计数变为为0
  std::shared_ptr<int> sp7 (std::move(sp6));
  //p8获取所有权,引用计数设置为1
  std::shared_ptr<int> sp8 (std::unique_ptr<int>(new int));
  std::shared_ptr<C> obj (new C);
  //同6一样,只不过拥有自己的删除器与4一样
  std::shared_ptr<int> sp9 (obj, obj->data);

  std::cout << "use_count:\n";
  std::cout << "p1: " << sp1.use_count() << '\n'; //0
  std::cout << "p2: " << sp2.use_count() << '\n'; //0
  std::cout << "p3: " << sp3.use_count() << '\n'; //1
  std::cout << "p4: " << sp4.use_count() << '\n'; //1
  std::cout << "p5: " << sp5.use_count() << '\n'; //2
  std::cout << "p6: " << sp6.use_count() << '\n'; //0
  std::cout << "p7: " << sp7.use_count() << '\n'; //2
  std::cout << "p8: " << sp8.use_count() << '\n'; //1
  std::cout << "p9: " << sp9.use_count() << '\n'; //2
  return 0;
}
复制代码

shared_ptr赋值

给shared_ptr赋值有三种方式,如下

#include <iostream>
#include <memory>

int main () {
  std::shared_ptr<int> foo;
  std::shared_ptr<int> bar (new int(10));
  //右边是左值,拷贝赋值,引用计数加1
  foo = bar; 
  //右边是右值,所以是移动赋值
  bar = std::make_shared<int> (20); 
  //unique_ptr 不共享它的指针。它无法复制到其他 unique_ptr,
  //无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动unique_ptr
  std::unique_ptr<int> unique (new int(30));
  // move from unique_ptr,引用计数转移
  foo = std::move(unique); 

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';

  return 0;
}
复制代码

make_shared

看下面make_shared的用法:

#include <iostream>
#include <memory>

int main () {

  std::shared_ptr<int> foo = std::make_shared<int> (10);
  // same as:
  std::shared_ptr<int> foo2 (new int(10));
  //创建内存,并返回共享指针,只创建一次内存
  auto bar = std::make_shared<int> (20);

  auto baz = std::make_shared<std::pair<int,int>> (30,40);

  std::cout << "*foo: " << *foo << '\n';
  std::cout << "*bar: " << *bar << '\n';
  std::cout << "*baz: " << baz->first << ' ' << baz->second << '\n';

  return 0;
}
复制代码

效率提升 std::make_shared(比起直接使用new)的一个特性是能提升效率。使用std::make_shared允许编译器产生更小,更快的代码,产生的代码使用更简洁的数据结构。考虑下面直接使用new的代码:

std::shared_ptr<Test> sp(new Test);
复制代码

很明显这段代码需要分配内存,但是它实际上要分配两次。每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块。如果使用std::make_shared来替换:

auto sp = std::make_shared<Test>();
复制代码

一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

对std::make_shared的效率分析可以同样地应用在std::allocate_shared上,所以std::make_shared的性能优点也可以扩展到这个函数上。

异常安全

另外一个std::make_shared的好处是异常安全,我们看下面一句简单的代码:

callTest(std::shared_ptr<Test>(new Test), secondFun());
复制代码

简单说,上面这个代码可能会发生内存泄漏,我们先来看下上面这个调用中几个语句的执行顺序,可能是顺序如下:

new Test()
secondFun()
std::shared_ptr<Test>()
复制代码

如果真是按照上面这样的代码顺序执行,那么在运行期,如果secondFun()中产生了一个异常,程序就会直接返回了,则第一步new Test分配的内存就泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。但是如果使用std::make_shared则可以避免这样的问题。调用代码将看起来像这样:

callTest(std::make_shared<Test>(), secondFun());           
复制代码

在运行期,不管std::make_shared或secondFun哪一个先被调用。如果std::make_shared先被调用,则在secondFun调用前,指向动态分配出来的Test的原始指针能安全地被存放到std::shared_ptr中。如果secondFun之后产生一个异常,std::shared_ptr的析构函数将发现它持有的Test需要被销毁。并且如果secondFun先被调用并产生一个异常,std::make_shared就不会被调用,因此这里就不需要考虑动态分配的Test了。

计数线程安全?

我们上面一直说shared_ptr中的计数是线程安全的,其实shared_ptr中的计数是使用了我们前面文章介绍的std::atomic特性,引用计数加一减一操作是原子性的,所以线程安全的。引用计数器的使用等价于用 std::memory_order_relaxedstd::atomic::fetch_add 自增(自减要求更强的顺序,以安全销毁控制块)。

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
 
struct Test
{
    Test() { std::cout << " Test::Test()\n"; }
    ~Test() { std::cout << " Test::~Test()\n"; }
};
 
//线程函数
void thr(std::shared_ptr<Test> p)
{
    //线程暂停1s
    std::this_thread::sleep_for(std::chrono::seconds(1));

    //赋值操作, shared_ptr引用计数use_cont加1(c++11中是原子操作)
    std::shared_ptr<Test> lp = p;
    {
        //static变量(单例模式),多线程同步用
        static std::mutex io_mutex;

        //std::lock_guard加锁
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
            << " lp.get() = " << lp.get()
            << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}
 
int main()
{
    //使用make_shared一次分配好需要内存
    std::shared_ptr<Test> p = std::make_shared<Test>();
    //std::shared_ptr<Test> p(new Test);

    std::cout << "Created a shared Test\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';

    //创建三个线程,t1,t2,t3
    //形参作为拷贝, 引用计数也会加1
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    std::cout << "Shared ownership between 3 threads and released\n"
        << "ownership from main:\n"
        << " p.get() = " << p.get()
        << ", p.use_count() = " << p.use_count() << '\n';
    //等待结束
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted\n";

    return 0;
}
复制代码

输出:

Test::Test()
Created a shared Test
 p.get() = 0xa7cec0, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
 p.get() = 0xa7cec0, p.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 5
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 4
local pointer in a thread:
 lp.get() = 0xa7cec0, lp.use_count() = 3
All threads completed, the last one deleted
 Test::~Test()
复制代码

enable_shared_from_this

在某些场合下,会遇到一种情况,如何安全的获取对象的this指针,一般来说我们不建议直接返回this指针,可以想象下有这么一种情况,返回的this指针保存在外部一个局部或全局变量,当对象已经被析构了,但是外部变量并不知道指针指向的对象已经被析构了,如果此时外部继续使用了这个指针就会发生程序奔溃。既要像指针操作对象一样,又能安全的析构对象,很自然就想到,智能指针就很合适!我们来看下面这段程序:

#include <iostream>
#include <memory>

class Test{
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        return std::shared_ptr<Test>(this);
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}
复制代码

编译运行后程序输出如下:

free(): double free detected in tcache 2
Test::Test()
1
1
Test::~Test()
Test::~Test()
复制代码

从上面的输出可以看到,构造函数调用了一次,析构函数却调用了两次,很明显这是不正确的。而std::enable_shared_from_this正是为了解决这个问题而存在。

std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权(这个是关键,直接使用this无法达到该效果)。

std::enable_shared_from_this是模板类,内部有个_Tp类型weak_ptr指针,std::enable_shared_from_this的构造函数都是protected,因此不能直接创建std::enable_from_shared_from_this类的实例变量,只能作为基类使用,通过调用shared_from_this成员函数,将会返回一个新的 std::shared_ptr<T> 对象,它与 pt 共享 t 的所有权。因此使用方法如下代码所示:

#include <iostream>
#include <memory>

// 这里必须要 public继承,除非用struct
class Test : public std::enable_shared_from_this<Test> {
public:
    Test(){
        std::cout << "Test::Test()" << std::endl;
    }
    ~Test(){
        std::cout << "Test::~Test()" << std::endl;
    }

    std::shared_ptr<Test> GetThis(){
        std::cout << "shared_from_this()" << std::endl;
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Test> p(new Test());
    std::shared_ptr<Test> p_this = p->GetThis();

    std::cout << p.use_count() << std::endl;
    std::cout << p_this.use_count() << std::endl;

    return 0;
}
复制代码

クラス内でenable_shared_from_this定義され使用しshared_from_this()shared_ptr<Test>オブジェクトを作成します。この関数は、Testオブジェクトを他のshared_ptrと共有できます。通常、非同期スレッドで使用します。非同期呼び出しでは、キープアライブメカニズムがあります。非同期関数が実行される時刻を特定することはできません。ただし、非同期関数は、非同期呼び出しの前に存在していた変数を使用する場合があります。非同期関数の実行中に変数が常に有効であることを保証するために、それ自体を指すshare_ptrを非同期関数に渡して、share_ptrによって管理されるオブジェクトが非同期関数の実行中に破棄されないようにすることができます。使用される変数は常に有効です(存続します)。

shared_ptrの使用に関する注意:

  • 複数のshared_ptrへのネイティブポインターを管理しないでください。管理されたrawポインターをアクティブに削除shared_ptrし。

    BigObj *p = new BigObj();
    std::shared_ptr<BigObj> sp(p);
    std::shared_ptr<BigObj> sp1(p);
    delete p;
    复制代码
  • このポインタをshared_ptrに指定せず、上記のようにenable_shared_from_thisを使用します。

  • メモリリークを防ぐことを考えずにポインタをshared_ptrに置き換えないでください。shared_ptrは万能薬ではなく、それらを使用するには一定のオーバーヘッドも必要です。

  • 共有所有権を持つオブジェクトは、通常、スコープオブジェクトよりも長持ちするため、平均リソース使用時間が長くなります。

  • マルチスレッド環境で共有ポインターを使用すると、参照カウントでのデータ競合を回避する必要があるため、コストがかかります。

  • スマートポインタを使用して、新しく割り当てられたメモリ以外のリソースを管理する場合は、それを削除者に渡すことを忘れないでください。

要約する

スマートポインタは、ポインタではなくテンプレートクラスです。スマートポインタを作成するときは、ポインタが指すことができるタイプ、、、...などである必要があり<int>ます<string>スマートポインタの本質は、->AND*演算子をオーバーロードするクラスです。このクラスは、例外が発生した場合でも、スマートポインタクラスのデストラクタを介してメモリを解放できるようにメモリを管理します。具体的には、参照カウント技術とC ++のRAII(リソース取得は初期化)機能を利用します。さて、次の記事でweak_ptrを学びましょう。

おすすめ

転載: juejin.im/post/7102684262737903653