Effective Modern C++ Item 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的一个常见用法是在对象继承中作为工厂函数的返回型别。如果我们有一个Investment为基类的投资型别。如图:

classDiagram
    class Investment
    Investment <|-- Stock
    Investment <|-- Bond
    Investment <|-- RealEstate

代码大概类似于:

class Investment  {...};

class Stock:
    public Investment  {...};

class Bond:
    public Investment  {...};

class RealEstate:
    public Investment  {...};        

这种结构的工厂函数通常会在堆上分配一个对象并返回一个指涉到它的指针,并当不在需要该对象时,调用者负责删除。

std::unique_ptr对以上业务堪称完美匹配。因为调用者需要对工厂函数返回的资源负责,而当std::unique_ptr被析构时,又会自动指涉到对象实施delete

Investment继承谱系的工厂函数应该声明如下:

template<typename ... TS>           //返回std::unique_ptr
std::unique_ptr<Investment>         //指涉到根据指定实参的对象
makeInvestment(Ts&&... params);

调用者可以类似这样使用:

{
    ...
    auto pInvestment =                  //pInvestment的型别是
        makeInvestment( arguments );    //std::unique_ptr<Investment>
    ...
}                                       //pInvestment在此析构

即便所有权不断流转,std::unique_ptr也会在无人指涉的时候自动析构掉。

工厂模式,unique_ptr的完美应用场景

默认的,析构通过delete运算符实现,但是在析构过程也可以指定自定义析构器:析构资源时所调用的任意函数。例如如下例子:

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 ( /* 应创建一个Stock型别的对象 */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if (/* 应创建一个Bond型别的对象 */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if (/* 应创建一个RealEstate型别的对象 */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

以上形式,会让工厂模式完美运作。假定把makeInvestment的调用结果存储在auto变量中,则你可以忽略正在使用的资源需要在析构的时候特殊处理的事情。因为无论何时析构,std::unique_ptr都会自动完成所有工作。

  • std::unique_ptr需要使用自定义析构器的时候,必须指定为std::unique_ptr构造的第二个参数。

  • std::unique_ptr用于工厂模式的基本流程是创建一个空的std::unique_ptr,然后使起指涉到适当型别的对象,然后将其返回。

  • 将一个裸指针赋值给std::unique_ptr是不会通过的,因为将裸指针赋值给std::unique_ptr可能会造成std::unique_ptr无法独占资源,所以要用reset方式更新。

  • 对于万能引用要配合std::forward进行转发,使得创建对象的构造函数可以获得调用者提供的正确信息。

  • 自定义析构器接受一个型别为Investment*的形参,所以无论是基类还是子类对象被删除的时候,都会正确进行释放。这里再次强调作为基类,必须具备一个虚析构函数

class Investment {
public:
    ...
    virtual ~Investment();
    ...
};

在C++14中,写法可以更加简单一些,封装性更好一些,delInvmt可以写在模板内部,C++11之所以不能放进去,是因为auto如果写全会导致大delInvmt没定义之前就使用。

template<typename ...Ts>
auto makeInvestment(Ts&&... params)
{
    auto delInvmt = [](Investment* pInvestment){
        makeLogEntry(pInvestment);
        delete pInvestment;
    };

    std::unique_ptr<Investment, decltype(delInvmt)>
        pInv(nullptr, delInvmt);
    if ( /* 应创建一个Stock型别的对象 */) {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    } else if (/* 应创建一个Bond型别的对象 */) {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    } else if (/* 应创建一个RealEstate型别的对象 */) {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

unique_ptr特点

在没有自定义析构器的情况下,可以认为unique_ptr和裸指针尺寸相同

但如果有自定义析构器,就不同了。

  • 如果自定义析构器是函数指针,那么std::unique_ptr的尺寸一般会增加1到2个字长。

  • 如果自定义析构器是函数对象,那么std::unique_ptr的尺寸取决于该函数对象中存储了多少状态。特别指出,无状态的函数对象(例如,无捕获的lambda表达式)不会浪费存储尺寸

意味着,如果自定义表达器,既可以用函数也可以用无捕获的lambda表达式的时候,lambda表达式会是更好的选择。

    auto delInvmt1 = [](Investment* pInvestment){
        makeLogEntry(pInvestment);              //使用无状态的lambda捕获
        delete pInvestment;
    };

    template<typename ...Ts>
    std::unique_ptr<Investment, decltype(delInvmt1)>
    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在Pimpl场景也很实用

这里主要在Item22展开。

std::unique_ptr的小知识

std::unique_ptr以两种形式提供,一个是单对象(std::unique_ptr<T>),一个是数组(std::unique_ptr<T[]>)。这样区分的结果是,std::unique_ptr指向的对象不会产生二义性。对应的接口,单对象的时候没有operator[],数组则不提供operator*operator->。但是对于数组形式一般没什么特别好的应用场景,因为std::arraystd::vectorstd::string总是比裸数组好的。所以了解即可,无需使用。

std::unique_ptr也可以很方便的转换成std::shared_ptr

//将std::unique_ptr转换为std::shared_ptr型别
std::shared_ptr<Investment> sp = makeInvestment(argments);

可以看到直接就能转换过去,这种特性也让std::unique_ptr作为工厂模式返回值更加方便合理。

要点速记
1. std::unique_ptr是小巧的,高速的,只能移动的智能指针,对托管资源实施专属所有权语义。
2. 默认的,析构器采用delete实现,也可以指定自定义析构器。但有状态捕获和函数指针的实现方式会增加std::unique_ptr变量的尺寸。
3. 将std::unique_ptr转换成std::shared_ptr是很方便的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值