条款18:对于独占资源使用std::unique_ptr

当你需要一个智能指针时,std::unique_ptr通常是最合适的。可以合理假设,默认情况下,std::unique_ptr大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。这意味着你甚至可以在内存和时间都比较紧张的情况下使用它。如果原始指针够小够快,那么std::unique_ptr一样可以。

std::unique_ptr体现了专有所有权(exclusive ownership)语义。一个non-null std::unique_ptr始终拥有其指向的内容。移动一个std::unique_ptr将所有权从源指针转移到目的指针。(源指针被设为null。)拷贝一个std::unique_ptr是不允许的,因为如果你能拷贝一个std::unique_ptr,你会得到指向相同内容的两个std::unique_ptr,每个都认为自己拥有(并且应当最后销毁)资源,销毁时就会出现重复销毁。因此,std::unique_ptr是一种只可移动类型(move-only type)。当析构时,一个non-null std::unique_ptr销毁它指向的资源。默认情况下,资源析构通过对std::unique_ptr里原始指针调用delete来实现。

std::unique_ptr的常见用法是作为继承层次结构中对象的工厂函数返回类型。假设我们有一个投资类型(比如股票、债券、房地产等)的继承结构,使用基类Investment

class Investment { … };
class Stock: public Investment { … };
class Bond: public Investment { … };
class RealEstate: public Investment { … };

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

template<typename... Ts>            //返回指向对象的std::unique_ptr,
std::unique_ptr<Investment>         //对象使用给定实参创建
makeInvestment(Ts&&... params);

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

{
    …
    auto pInvestment =                  //pInvestment是
        makeInvestment( arguments );    //std::unique_ptr<Investment>类型
    …
}                                       //销毁 *pInvestment

但是也可以在所有权转移的场景中使用它,比如将工厂返回的std::unique_ptr移入容器中,然后将容器元素移入一个对象的数据成员中,然后对象过后被销毁。发生这种情况时,这个对象的std::unique_ptr数据成员也被销毁,并且智能指针数据成员的析构将导致从工厂返回的资源被销毁。如果所有权链由于异常或者其他非典型控制流出现中断(比如提前从函数return或者循环中的break),则拥有托管资源的std::unique_ptr将保证指向内容的析构函数被调用,销毁对应资源。(这个规则也有些例外。大多数情况发生于不正常的程序终止。如果一个异常传播到线程的基本函数(比如程序初始线程的main函数)外,或者违反noexcept说明(见条款14),局部变量可能不会被销毁;如果std::abort或者退出函数(如std::_Exitstd::exit,或std::quick_exit)被调用,局部变量一定没被销毁。) 

默认情况下,销毁将通过delete进行,但是在构造过程中,std::unique_ptr对象可以被设置为使用(对资源的)自定义删除器:当资源需要销毁时可调用的任意函数(或者函数对象,包括lambda表达式)。如果通过makeInvestment创建的对象不应仅仅被delete,而应该先写一条日志,makeInvestment可以以如下方式实现。(代码后有说明,别担心有些东西的动机不那么明显。)

auto delInvmt = [](Investment* pInvestment)         //自定义删除器
                {                                   //(lambda表达式)
                    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变量中,那么你将在愉快中忽略在删除过程中需要特殊处理的事实。当然,你确实幸福,因为使用了unique_ptr意味着你不需要关心什么时候资源应被释放,不需要考虑在资源释放时的路径,以及确保只释放一次,std::unique_ptr自动解决了这些问题。从使用者角度,makeInvestment接口很棒。

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

  • delInvmt是从makeInvestment返回的对象的自定义的删除器。所有的自定义的删除行为接受要销毁对象的原始指针,然后执行所有必要行为实现销毁操作。在上面情况中,操作包括调用makeLogEntry然后应用delete。使用lambda创建delInvmt是方便的,而且,正如稍后看到的,比编写常规的函数更有效。

  • 当使用自定义删除器时,删除器类型必须作为第二个类型实参传给std::unique_ptr。在上面情况中,就是delInvmt的类型,这就是为什么makeInvestment返回类型是std::unique_ptr<Investment, decltype(delInvmt)>。(对于decltype,更多信息查看条款3)

  • makeInvestment的基本策略是创建一个空的std::unique_ptr,然后指向一个合适类型的对象,然后返回。为了将自定义删除器delInvmtpInv关联,我们把delInvmt作为pInv构造函数的第二个实参。

  • 尝试将原始指针(比如new创建)赋值给std::unique_ptr通不过编译,因为是一种从原始指针到智能指针的隐式转换。这种隐式转换会出问题,所以C++11的智能指针禁止这个行为。这就是通过reset来让pInv接管通过new创建的对象的所有权的原因。

  • 使用new时,我们使用std::forward把传给makeInvestment的实参完美转发出去(查看条款25)。这使调用者提供的所有信息可用于正在创建的对象的构造函数。

  • 自定义删除器的一个形参,类型是Investment*,不管在makeInvestment内部创建的对象的真实类型(如StockBond,或RealEstate)是什么,它最终在lambda表达式中,作为Investment*对象被删除。这意味着我们通过基类指针删除派生类实例,为此,基类Investment必须有虚析构函数:

class Investment {
public:
    …
    virtual ~Investment();          //关键设计部分!
    …
};

在C++14中,函数的返回类型推导存在(参阅条款3),意味着makeInvestment可以以更简单,更封装的方式实现: 

template<typename... Ts>
auto makeInvestment(Ts&&... params)                 //C++14
{
    auto delInvmt = [](Investment* pInvestment)     //现在在
                    {                               //makeInvestment里
                        makeLogEntry(pInvestment);
                        delete pInvestment; 
                    };

    std::unique_ptr<Investment, decltype(delInvmt)> //同之前一样
        pInv(nullptr, delInvmt);
    if ( … )                                        //同之前一样
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if ( … )                                   //同之前一样
    {     
        pInv.reset(new Bond(std::forward<Ts>(params)...));   
    }   
    else if ( … )                                   //同之前一样
    {     
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));   
    }   
    return pInv;                                    //同之前一样
}

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

auto delInvmt1 = [](Investment* pInvestment)        //无状态lambda的
                 {                                  //自定义删除器
                     makeLogEntry(pInvestment);
                     delete pInvestment; 
                 };

template<typename... Ts>                            //返回类型大小是
std::unique_ptr<Investment, decltype(delInvmt1)>    //Investment*的大小
makeInvestment(Ts&&... args);

void delInvmt2(Investment* pInvestment)             //函数形式的
{                                                   //自定义删除器
    makeLogEntry(pInvestment);
    delete pInvestment;
}
template<typename... Ts>                            //返回类型大小是
std::unique_ptr<Investment, void (*)(Investment*)>  //Investment*的指针
makeInvestment(Ts&&... params);                     //加至少一个函数指针的大小

具有很多状态的自定义删除器会产生大尺寸std::unique_ptr对象。如果你发现自定义删除器使得你的std::unique_ptr变得过大,你需要审视修改你的设计。

工厂函数不是std::unique_ptr的唯一常见用法。作为实现Pimpl Idiom(译注:pointer to implementation,一种隐藏实际实现而减弱编译依赖性的设计思想,《Effective C++》条款31对此有过叙述)的一种机制,它更为流行。代码并不复杂,但是在某些情况下并不直观,所以这安排在条款22的专门主题中。

std::unique_ptr有两种形式,一种用于单个对象(std::unique_ptr<T>),一种用于数组(std::unique_ptr<T[]>)。结果就是,指向哪种形式没有歧义。std::unique_ptr的API设计会自动匹配你的用法,比如operator[]就是数组对象,解引用操作符(operator*operator->)就是单个对象专有。

你应该对数组的std::unique_ptr的存在兴趣泛泛,因为std::arraystd::vectorstd::string这些更好用的数据容器应该取代原始数组。std::unique_ptr<T[]>有用的唯一情况是你使用类似C的API返回一个指向堆数组的原始指针,而你想接管这个数组的所有权。

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

std::shared_ptr<Investment> sp =            //将std::unique_ptr
    makeInvestment(arguments);              //转为std::shared_ptr

这就是std::unique_ptr非常适合用作工厂函数返回类型的原因的关键部分。 工厂函数无法知道调用者是否要对它们返回的对象使用专有所有权语义,或者共享所有权(即std::shared_ptr)是否更合适。 通过返回std::unique_ptr,工厂为调用者提供了最有效的智能指针,但它们并不妨碍调用者用其更灵活的兄弟替换它。(有关std::shared_ptr的信息,请转到条款19。)

请记住:

  • std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
  • 默认情况,资源销毁通过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、付费专栏及课程。

余额充值