《Effective Modern C++》学习笔记 - Item 18: 使用 std::unique_ptr 管理独占性资源(附MSVC源码解析)

  • std::unique_ptr 占用空间与裸指针相同,大多数操作(包括解引用)的方法也相同。它表现的是 独占性拥有(exclusive ownership) 的语义,这意味着它不能被拷贝,是 move-only 类型;当其自身被销毁时,总会销毁其拥有的资源。而对象的销毁在作用域结束(即右大括号出现)时自动通过调用析构函数进行,此时 std::unique_ptr 默认对内部的裸指针进行 delete,因此你无需像裸指针一样操心什么时候要手动销毁。
  • (注:笔者尽己所能对MSVC的 std::unique_ptr 源码进行了一些解析,为了不影响整体观感,放在全文最后)
  • std::unique_ptr 可以作为工厂函数的返回类型。假设有一个基类 Investment(投资)以及若干派生类 Stock(股票),Bond(债券),RealEstate(不动产)。工厂函数 makeInvestment 根据参数 params 生成对象,并以 std::unique_ptr<Investment> 类型返回:
template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);
  • std::unique_ptr 的析构行为默认是 delete,可以在构造时传入自定义的删除器(deleter):一个任意的函数(或仿函数对象,包括 lambda 表达式)。
template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14,自动推导函数返回值类型
{
    auto delInvmnt = [](Investment* pInvestment)
    {
        makeLogEntry(pInvestment);  // 析构前先做某种日志记录
        delete pInvestment;         // 任何派生类都会通过调用Investment的析构函数销毁
                                    // 因此Investment的析构函数必须是virtual!
    };
    std::unique_ptr<Investment, decltype(delInvmnt)> pInv(nullptr, delInvmet);

    if ( /* 应该创建Stock对象 */)
    {
        pInv.reset(new Stock());    // unique_ptr通过reset()函数重新设置管理的资源
    }
    else if ( /* 应该创建Bond对象 */)
    {
        pInv.reset(new Bond());
    }
    else if ( /* 应该创建RealEstate对象 */)
    {
        pInv.reset(new RealEstate());
    }
    return pInv;
}
  • 使用默认删除时,std::unique_ptr 和裸指针占用相同的大小。需要自定义 deleter时,尽可能使用 lambda 表达式而非普通函数,使用前者不会带来占用空间的增加,而使用后者会多占用至少一个字长的空间
    在这里插入图片描述

  • std::unique_ptr 在 Pimpl 范式中的应用更加流行,详见Item 22。

  • std::unique_ptr 有两个版本,一个针对单独对象 std::unique_ptr<T>,一个针对数组 std::unique_ptr<T[]>。二者之间区分明确,例如前者没有下标运算符(operator []),而后者没有解引用运算符(operator *operator ->)。(笔者注:查看源码可以发现,二者实际是两个分开定义的类)不过后者的应用场景很少,主要可以用于与返回数组指针的C风格API对接。

  • std::unique_ptr 的一个出色特性是可以轻易且高效地转成std::shared_ptr。由此,工厂函数无需知道调用者是否一定会将其独占使用——他们只需返回最高效的智能指针,至于调用者是将其作为 std::unique_ptr 还是 std::shared_ptr 使用都与他们无关。对于调用者而言的使用体验也是很愉快的。

// converts std::unique_ptr to std::shared_ptr
std::shared_ptr<Investment> sp = makeInvestment( arguments );

总结

  1. std::unique_ptr 是一个小巧、高效、move-only 的智能指针,用于管理独占性资源。
  2. 资源的销毁默认通过 delete 进行,但是可以声明自定义的 deleter。有状态的 deleter 和函数指针作为 deleter 会增加 std::unique_ptr 对象的大小。
  3. std::unique_ptr 转换为 std::shared_ptr 是很轻松的。

附:对MSVC的STL中 std::unique_ptr 实现的一些分析
此部分网络上基本没有参考资料,主要基于笔者阅读源码的推测,可能有错误,欢迎各位大神指出。
在这里插入图片描述

在这里插入图片描述

  • 首先如前文所述,std::unique_ptr 分单个对象和数组两个版本,相应的也有两个 make_unique 版本函数。以下主要针对单对象版本说明。模板参数将数据类型和 deleter 类型分别称为 _Ty_Dx
  • 从声明中可以看出,默认的 deleter 使用了一个结构体对象 default_delete,它是重载了 operator () 的仿函数。其行为自然是 delete 掉一个 _Ty类型的指针。
    在这里插入图片描述
  • 回到 unique_ptr,首先用 alias declaration 定义了几个便于阅读的类型名称 pointerelement_typedeleter_type。其中 pointer 是 deleter 使用的指针类型,默认为 _Ty*
    在这里插入图片描述
  • 先忽略下面的众多函数,跳到 private 部分,发现 MSVC的 std::unique_ptr 内部对 deleter 和数据指针实际以类似 pair 的形式存储:
    在这里插入图片描述
  • unique_ptr 在使用默认 deleter 时能做到大小和裸指针一样的秘诀也在这个存储方式 compressed pair 中,推荐一篇博客:实现 compressed pair

在这里插入图片描述
在这里插入图片描述

  • 构造函数:
    • _Zero_then_variadic_args_t_One_then_variadic_args_t 起标识作用,指示 pair 的第一项(deleter)是用默认构造函数还是用以第一个参数做参数的构造函数(通过 std::forward 转发)。
    • 仅传入 _Ptr 的构造函数:deleter 用默认构造函数,pointer 用 _Ptr 构造;
    • 传入 _Ptr 和 const 引用 _Dt 的构造函数:deleter 用 _Dt 构造,pointer 用 _Ptr 构造;
    • 传入 _Ptr 和右值 _Dt 的构造函数:deleter 用 std::move(_Dt) 构造,pointer 用 _Ptr 构造;
    • 传入 _Right 的移动构造函数:deleter 用 _Right.get_deleter() 构造,pointer 用 _Right.release() (见下面)构造;
    • 复制构造函数(和复制赋值运算符)被屏蔽(=delete)。

在这里插入图片描述

  • getter 函数:get()operator->()operator*()get_deleter() ,直接返回 _Mypair 中的项。
  • operator bool():转化为 bool 值,只能显式调用。

在这里插入图片描述
在这里插入图片描述

  • 三个释放函数:
    • release():将自身内含指针设为nullptr,不销毁并返回原指针。通过 std::exchange() 实现。
    • reset():将自身内含指针销毁,设置为新的指针。先通过 std::exchange_Old 指针换出来,如果 _Old 非空,通过 _Mypair._Get_first() 拿到 deleter,然后以 _Old 为参数调用 deleter。
    • 析构函数:释放逻辑与 reset() 后半部分相同。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值