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 可以满足这种需求。