Item 20: Use std::weak_ptr for std::shared_ptr like pointers that can dangle.

Item 20: Use std::weak_ptr for std::shared_ptr like pointers that can dangle.

std::weak_ptr 的特点

std::weak_ptr 通常不会单独使用,一般是与 std::shared_ptr 搭配使用,可以将 std::weak_ptr 类型指针视为 std::shared_ptr 指针的一种辅助工具,借用 std::weak_ptr 类型指针, 可以获取 std::shared_ptr 指针的一些状态信息,例如有多少 std::shared_ptr 指针指向相同的资源、std::shared_ptr 指针指向的内存是否已经被释放等。

std::weak_ptr 常常是通过 std::shared_ptr 构造而来,它和 std::shard_ptr 指向的相同的位置。但是,std::weak_ptr 不会影响对象的引用计数,也就是说,std::weak_ptr 被创建时,引用计数不会增加,当它被释放时,引用计数也不会减少。

auto spw =                     // after spw is constructed, the pointed-to Widget's
  std::make_shared<Widget>();  // ref count (RC) is 1.
  …
  std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw. RC remains 1
  …
  spw = nullptr;  // RC goes to 0, and the Widget is destroyed. wpw now dangles

  if (wpw.expired())// if wpw doesn't point to an object…

std::weak_ptr 没有解引用操作,但可以将它转换为 std::shared_ptr,使用 lock 可以保证线程安全。

std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired, spw1 is null
auto spw2 = wpw.lock(); // same as above, but uses auto

std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_ptr

std::weak_ptr 的典型应用

下面介绍 std::weak_ptr 的两个典型应用,其实本质上就是利用了 std::weak_ptr 的特点:共享资源所有权,但又不增加其引用计数

循环引用

std::weak_ptr 的一个典型应用是解决 std::shared_ptr 的内存泄露问题----循环引用。看下面的代码:

 #include <iostream>
 #include <memory>
 using namespace std;
 
class B;
class A {
  public:
    A() { cout << "A constructor" << endl; }
    ~A() { cout << "A destructor" << endl; }
     
    shared_ptr<B> b;
};
 
class B {
  public:
    B() { cout << "B constructor" << endl; }
    ~B() { cout << "B's destructor" << endl; }
   
    shared_ptr<A> a; 
};
 
int main() {
  std::shared_ptr<A> aa = make_shared<A>(); // aa 引用计数为 1
  std::shared_ptr<B> bb = make_shared<B>(); // bb 引用计数为 1
 
  aa->b = bb;// bb 引用计数为 2
  bb->a = aa;// aa 引用计数为 2

  return 0;
}

// output
A constructor
B constructor

从运行结果可以看到 A 和 B 都调用了构造函数,却没有调用析构函数,导致了资源泄露。原因是 main 函数结束后,两个对象的引用计数都为 1 ,导致 std::shared_ptr 没有调用析构函数。解决办法是将 A 和 B 对象中 shared_ptr 换成 weak_ptr 即可。

带缓存的工厂方法

当调用工厂方法的代价比较高时,可以通过增加缓存来优化。但是把所有对象都缓存下来会造成效率问题,当对象不再使用时,可以销毁其缓存。

示例代码参考这里

#include <iostream>
#include <unordered_map>
#include <memory>


using namespace std;

/*!
 * \brief The Investment class 基类
 */
class Investment
{
public:
    virtual ~Investment() = default;

public:
    virtual void doWork() = 0;
};

/*!
 * \brief The Stock class 派生类
 */
class Stock : public Investment
{
public:
    ~Stock() {
        cout << "~Stock() called....\n";
    }
public:
    virtual void doWork() override {
        cout << "Stock doWork....\n";
    }
};

/*!
 * \brief The Stock class 派生类
 */
class Bond : public Investment
{
public:
    ~Bond() {
        cout << "~Bond() called....\n";
    }

public:
    virtual void doWork() override {
        cout << "Bond doWork....\n";
    }
};

enum class InvestType {
    INVEST_TYPE_STOCK,
    INVEST_TYPE_BOND,
};

// 工厂函数
auto makeInvestment(InvestType type)
{
    // 自定义析构器, 这里以lambda表达式的形式给出
    auto delInvmt = [](Investment *pInvestment) {
        cout << "custom delInvmt called...." << pInvestment << endl;

        // 注意:pInvestment可能为空指针,比如默认为空,然后调用reset赋值时,会先调用一遍析构
        if (pInvestment)
        {
            // TODO 自定义析构时想干的事
            delete pInvestment;
        }
    };

    // 待返回的指针, 初始化为空指针
    shared_ptr<Investment> pInv(nullptr);

    // 注意这里用reset来指定pInv获取从new产生的对象的所有权, 不能用=赋值
    switch (type)
    {
    case InvestType::INVEST_TYPE_STOCK:
        // 注意:自定义析构器是随对象一起指定的,这里区别于unique_ptr
        pInv.reset(new Stock, delInvmt);
        break;
    case InvestType::INVEST_TYPE_BOND:
        // 如果不指定自定义析构器的话,则不会调用
        pInv.reset(new Bond);
        break;
    }

    // 返回智能指针
    return pInv;
}

// 带缓存的工厂函数
// 使用场景:当调用工厂函数makeInvestment成本高昂(e.g. 会执行一些文件或数据块的I/O操作), 并且type会频繁的重复调用
shared_ptr<Investment> fastLoadInvestment(InvestType type)
{
    // 定义一个做缓存的容器,注意这里存的内容是weak_ptr
    // 使用weak_ptr的好处是,它不会影响所指涉对象的引用计数
    // 如果这里改为shared_ptr的话,则函数外边永远不会析构掉这个对象, 因为缓存中至少保证其引用计数为1。这就背离的我们的设计
    static unordered_map<InvestType, weak_ptr<Investment>> s_cache;
    

    // 将weak_ptr生成shared_ptr
    auto pInv = s_cache[type].lock();

    // 如果缓存中没有的话,则调用工厂函数创建一个新对象,并且加入到缓存中去
    if (!pInv)
    {
        cout << "create new investment..\n";
        pInv = makeInvestment(type);
        s_cache[type] = pInv;
    }

    return pInv;
}

int main () {
    {
        auto pInv = fastLoadInvestment(InvestType::INVEST_TYPE_BOND);
        pInv->doWork();
    }

    cout << "-------------------------------\n";

    {
        auto pInv = fastLoadInvestment(InvestType::INVEST_TYPE_BOND);
        pInv->doWork();
    }

    cout << "-------------------------------\n";

    {
        auto pInv = fastLoadInvestment(InvestType::INVEST_TYPE_STOCK);
        pInv->doWork();
        auto pInv2 = fastLoadInvestment(InvestType::INVEST_TYPE_STOCK);
        pInv2->doWork();
    }
    
    return 0;
}

// output
create new investment..
Bond doWork....
~Bond() called....
-------------------------------
create new investment..
Bond doWork....
~Bond() called....
-------------------------------
create new investment..
Stock doWork....
Stock doWork....
custom delInvmt called....0x1258cd0
~Stock() called....

对象的缓存管理器需要一个类似 std::shared_ptr 的指针,但又想这些对象的生存期可以由调用者来管理来管理,因而使用 std::weak_ptr 可以满足这种需求。

  • 9
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值