条款18.使用std::unique_ptr管理具备专属所有权的资源

使用std::unique_ptr管理具备专属所有权的资源

每当你需要使用智能指针的时候,std::unique_ptr基本是最合适的。可以认为在默认情况下,std::unique_ptr等同于原始指针,而且对于大多数操作(包括解引用),它们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr几乎可以肯定也能满足你的要求。

std::unique_ptr体现了专有所有权语义。一个非空的std::unique_ptr总是拥有其所指向的资源移动一个std::unique_ptr会将所有权从源指针移动到目标指针(原指针被置空)。拷贝操作是不允许的,因为如果你能拷贝一个std::unique_ptr,你会得到指向相同内容的两个std::unique_ptr,每个都认为自己拥有资源,销毁时也会出现重复销毁。因此,std::unique_ptr只支持移动操作。std::unique_ptr销毁时,其指向的资源也执行析构函数。而原始指针需要显式调用delete来销毁指针指向的资源

std::unique_ptr的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个基类Investmen的继承结构。

class Investment{ ... };
class Sock : public Investment { ... };
clsas Bond : public Investment { ... };
class RealEstate : public Investment { ... };

这种继承关系的工厂函数在堆上分配一个对象然后返回指针,调用方在不需要的时候,销毁对象,这使用场景完美匹配std::unique_ptr,因为调用者对工厂返回的资源负责(即堆该资源的专有所有权),并且std::unique_ptr会自动销毁指向的内容。可以这样声明

template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);

调用者应该在单独的作用域中使用返回的std::unique_ptr智能指针

{
    ...
    auto pInvestment = makeInvestment(arguments);
    ...
}	//destroy *pInvestment

如果所有权链由于异常或者其他非典型控制流出现中断(比如提前return或者循环中的break),则拥有托管资源的std::unique_ptr将保证指向的内容的析构函数被调用,销毁对应资源。

默认情况下,销毁将通过delete进行,但是在构造过程中,可以自定义std::unique_ptr指向对象的析构函数:任意函数(或者函数对象,包括lambda)。如果通过makeInvestment创建的对象不能直接被删除,应该首先写一条日志,可以实现如下:

auto delInvmt = [](Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&& params)
{
	std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
    if(/*a stock object should be created*/)
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if(/*a bond object should be created*/)
    {
		 pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if(/*a RealEstate object should be created*/)
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

这个实现确实相当棒,如果你理解了:

  • delInvmt是自定义的从makeInvestment返回的析构函数。所有的自定义的析构行为接受要销毁对象的原始指针,然后执行销毁操作。
  • 当使用自定义删除器时,必须将其作为第二个参数传给std::unique_ptr
  • makeInvestment的基本策略时创建一个空的std::unique_ptr,然后指向一个合适的类型的对象,然后返回,为了为pInv关联自定义删除器,作为构造函数的第二个参数。
  • 尝试将原始指针(比如new创建)赋值给std::unique_ptr通不过编译,因为不存在从原始指针到智能指针的隐式转换,这种隐式转换为出问题,所以禁止,这就是为什么通过reset来传递new指针的原因
  • 自定义删除器的参数类型是Investment*,尽管真实的对象类型是在makeInvestment内部创建的,它最终通过在lambda表达式中,作为Investment*对象被删除。这意味着我们通过基类指针删除派生类实例。为此,基类必须是虚析构函数。
  • 使用new时,要使用std::forward作为参数来完美转发给makeInvestment,这使调用者提供的所有信息可用于正在创建的对象的构造函数。
class Investment{
public:
  	...
    virtual ~Investment();
    ...
};

当使用默认删除器时,可以合理假设std::unique_ptr和原始指针大小相同。当自定义删除器时,情况可能不再如此。删除器是个函数指针,通常会使std::unique_ptr的字节从一个增加到两个对于删除器的函数对象来说,大小取决于函数对象中存储的状态多少,无状态函数对象(比如没有捕获的lambda表达式)对大小没有影响,这意味当自定义删除器可以被lambda实现时,尽量使用lambda

auto delInvmt = [](Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment,decltype(delInvmt)>
makeInvestment(Ts&& params);		//返回值尺寸与Investment*相同

void delInvmt2(Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... params);		//返回值尺寸等于Investment*的尺寸加上至少函数指针的尺寸

std::unique_ptr有两种形式,一种用于单个对象(std::unique_ptr<T>),一种用于数组(std::unique_ptr<T[]>

数组的std::unique_ptr的存在应该不被使用,因为std::arraystd::vectorstd::string这些更好用的数据容器应该取代原始数组

std::unique_ptr是C++11中表示专有所有权的方法,但是其最吸引人的功能之一是它可以轻松高效的转换为std::shared_ptr

std::shared_ptr<Investment> sp = makeInvestment(arguments);

这就是为什么std::unique_ptr非常适合用于工厂函数返回类型的关键部分。工厂函数无法直到调用者是否要对它们返回的对象使用专有所有权的语义,或者共享所有权std::shared_ptr是否更合适。通过返回std::unique_ptr,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用更灵活的兄弟替换它。

要点速记

  • std::unique_ptr是轻量级,快速的,具备只移类型的智能指针,对托管资源实施专属所有权定义
  • 默认的,资源析构采用delete运算符实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr类型的对象尺寸
  • std::unique_ptr转换成std::shared_ptr容易实现。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值