第20課 unique_ptr獨佔型智能指針

一. unique_ptr的基本用法ios

(一)初始化方式編程

  1. 直接初始化:unique<T> myPtr(new T);  //ok。但不能經過隱式轉換來構造,如unique<T> myPtr = new T()。由於unique_ptr構造函數被聲明爲explicit數組

  2. 移動構造:unique<T> myOtherPtr = std::move(myPtr);但不容許複製構造,如unique<T> myOther = myPtr; 由於unique是個只移動類型。ide

  3. 經過make_unique構造:unique<T> myPtr = std::make_unique<T>(); //C++14支持的語法。可是make_都不支持添加刪除器,或者初始化列表函數

  4. 經過reset重置:如std::unique_ptr up; up.reset(new T());源碼分析

(二)指定刪除器測試

  1. unique_ptr<T,D>  u1(p,d);刪除器是unique_ptr類型的組成部分,但是普通函數指針或lambda表達式。注意,當指定刪除器時須要同時指定其類型,即D不可省略。優化

  2.使用默認的deleter時,unique_ptr對象和原始指針的大小是同樣的。當自定義deleter時,若是deleter是函數指針,則unique_ptr對象的大小爲8字節。對於函數對象的deleter,unique_ptr對象的大小依賴於存儲狀態的多少,無狀態的函數對象(如不捕獲變量的lambda表達式),其大小爲4字節ui

二. 剖析unique_ptr this

(一)源碼分析【節選】

//指向單對象
template <class _Ty, class _Dx> //注意,刪除器也是unique_ptr類型的一部分
class unique_ptr { // non-copyable pointer to an object
private:
    _Compressed_pair<_Dx, pointer> _Mypair;
public:

    using pointer      = _Ty*;//裸指針類型
    using element_type = _Ty; //對象類型
    using deleter_type = _Dx; //刪除器類型

    template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
    constexpr unique_ptr() noexcept : _Mypair(_Zero_then_variadic_args_t()) {} //構造一個空的智能指針

    unique_ptr& operator=(nullptr_t) noexcept; //重置指針爲nullptr

    //注意,explicit阻止隱式構造,如unique_ptr<int> up = new int(100);編譯錯誤。只能顯示構造,如unique_ptr<int> up(new int(100));
    template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
    explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t(), _Ptr) {} 

    template <class _Dx2 = _Dx, enable_if_t<is_constructible_v<_Dx2, const _Dx2&>, int> = 0>
    unique_ptr(pointer _Ptr, const _Dx& _Dt) noexcept : _Mypair(_One_then_variadic_args_t(), _Dt, _Ptr) {}

    unique_ptr(unique_ptr&& _Right) noexcept;  //移動構造

    unique_ptr& operator=(unique_ptr&& _Right) noexcept;//移動賦值

    void swap(unique_ptr& _Right) noexcept;//交換兩個智能指針所指向的對象

    ~unique_ptr() noexcept; //析構函數,調用刪除器釋放資源。

    Dx& get_deleter() noexcept; //返回刪除器

    const _Dx& get_deleter() const noexcept;//返回刪除器

    add_lvalue_reference_t<_Ty> operator*() const; //解引用

    pointer operator->() const noexcept; //智能指針->運算符

    pointer get() const noexcept; 

    explicit operator bool() const noexcept; //類型轉換函數,用於條件語句,如if(uniptr)之類

    pointer release() noexcept; //返回裸指針,並釋放全部權

    void reset(pointer _Ptr = pointer()) noexcept ; //重置指針

    unique_ptr(const unique_ptr&) = delete; //不可拷貝
    unique_ptr& operator=(const unique_ptr&) = delete; //不可拷貝賦值
};

//指向數組類型
template <class _Ty, class _Dx>
class unique_ptr<_Ty[], _Dx> { 
private:
    _Compressed_pair<_Dx, pointer> _Mypair; 
public:
    using pointer      = typename _Get_deleter_pointer_type<_Ty, remove_reference_t<_Dx>>::type;
    using element_type = _Ty;
    using deleter_type = _Dx;

    //...    //省略了與unique_ptr單對象類型相同的一些操做
   
    ~unique_ptr() noexcept; //析構函數,調用刪除器釋放資源。

    _Ty& operator[](size_t _Idx) const {  //數組[]操做符
        return _Mypair._Myval2[_Idx];
    }

    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
};
unique_ptr

  1. unique_ptr的構造函數被聲明爲explicit,禁止隱式類型轉換的行爲。緣由以下:

    ①可減小誤將智能指針指向棧對象的狀況。如unique_ptr<int> ui = &i;其中的i爲棧變量。

    ②可避免將一個普通指針傳遞給形參爲智能指針的函數。假設,若是容許將裸指針傳給void foo(std::unique_ptr<T>)函數,則在函數結束後會因形參超出做用域,裸指針將被delete的誤操做。

  2. unique_ptr的拷貝構造和拷貝賦值被聲明爲delete。所以沒法實施拷貝和賦值操做,但能夠移動構造和移動賦值。

  3. 刪除器是unique_ptr類型的一部分。默認爲std::default_delete,內部是經過調用delete來實現。

  4. unique_ptr能夠指向數組,並重載了operator []運算符。如unique_ptr<int[]> ptr(new int[10]); ptr[9]=9;但建議使用使做std::array、std::vector或std::string來代替這種原始數組。

(二)經常使用操做

  1.get():返回unique_ptr中保存的裸指針

  2.reset():重置unique_ptr。

  3.release():放棄對指針的控制權,返回裸指針,並將unique_ptr自身置空。一般用來初始化另外一個智能指針。

  4.swap(q):交換兩個智能指針所指向的對象。

【編程實驗】std::unique_ptr的基本用法

#include <iostream>
#include <vector>
#include <memory>  //for smart pointer

using namespace std;

class Widget {};

//返回值RVO優化:
unique_ptr<int> func()
{
    unique_ptr<int> up(new int(100));
    return  up; //up是個左值,調用拷貝構造給返回值? No。
                //C++標準要求當RVO被容許時,要麼消除拷貝,要麼隱式地把std::move用在要返回的局部
                //對象上去。這裏編譯器會直接在返回值位置建立up對象。所以根本不會發生拷貝構造,
                //unique_ptr自己也不能被拷貝構造。

    //return unique_ptr<int>(new int(100)); //右值,被移動構造。
}

void foo(std::unique_ptr<int> ptr)
{
}

void myDeleter(int* p)
{
    cout << "invoke deleter(void* p)"<< endl;
    delete p;
}

int main()
{
    //1. unique_ptr的初始化
    //1.1 經過裸指針建立unique_ptr(因爲unique_ptr的構造函數是explicit的,必須使用直接初始化,不能作隱式類型轉換)
    std::unique_ptr<Widget> ptr1(new Widget);      //ok; 直接初始化
    //std::unique_ptr<Widget> ptr1 = new Widget(); //error。不能隱式將Widget*轉換爲unqiue_ptr<Widget>類型。

    std::unique_ptr<int[]> ptr2(new int[10]); //指向數組

    //1.2 經過移動構造
    //std::unique_ptr<Widget> ptr3 = ptr1;    //error,unique_ptr是獨佔型,不能複製構造
    std::unique_ptr<Widget> ptr3 = std::move(ptr1);  //ok,unique_ptr是個只移動類型,能夠移動構造
    auto ptr4 = std::move(ptr3);     //ok, ptr4爲unique_ptr<Widget>類型

    //1.3 經過std::make_unique來建立
    auto ptr5 = std::make_unique<int>(10);

    //auto ptr6 = std::make_unique<vector<int>>({1,2,3,4,5}); //error,make_unique不支持初始化列表
    auto initList = { 1,2,3,4,5 };
    auto ptr6 = std::make_unique<vector<int>>(initList);

    //2. 傳參和返回值
    int* px = new int(0);
    //foo(px); //error,px沒法隱式轉爲unique_ptr。可防止foo函數執行完畢後,px會自動釋放。
    //foo(ptr5); //error,智能指針不能被拷貝。所以,能夠將foo的形參聲明爲引用,以免全部權轉移
    foo(std::move(ptr5)); //ok,經過移動構造

    auto ptr7 = func(); //移動構造

    //3.經常使用操做
    std::unique_ptr<Widget> upw1; //空的unique_ptr
    upw1.reset(new Widget);
    std::unique_ptr<Widget> upw2(new Widget);

    cout <<"before swap..." << endl;
    cout << "upw1.get() = " << hex << upw1.get() << endl;

    cout << "upw2.get() = " << hex << upw2.get() << endl;

    cout << "after swap..." << endl;
    upw1.swap(upw2); //交換指針所指的對象
    cout << "upw1.get() = " << hex << upw1.get() << endl;
    cout << "upw2.get() = " << hex << upw2.get() << endl;

    //upw1.release(); //release放棄了控制權不會釋放內存,丟失了指針
    Widget* pw = upw1.release();//放棄對指針的控制
    delete pw; //需手動刪除

    if (upw1) {  //unique_ptr重載了operator bool()
        cout << "upw1 owns resourse" << endl;
    }else {
        cout << "upw1 lost resourse" << endl;
    }

    upw1.reset(upw2.release()); //轉移全部權
    cout << "upw1.get() = " << hex << upw1.get() << endl;
    cout << "upw2.get() = " << hex << upw2.get() << endl;

    //upw1 = nullptr; //釋放upw1指向的對象,並將upw1置空
    //upw1.reset(nullptr);

    //4.unique_ptr的大小
    std::unique_ptr<int,decltype(&myDeleter)> upd1(new int(0), myDeleter); //自定義刪除器
    auto del = [](auto* p) {delete p; };
    std::unique_ptr<int, decltype(del)> upd2(new int(0), del); 
    cout << sizeof(upw1) << endl; //4字節,默認刪除器
    cout << sizeof(upd1) << endl; //8字節
    cout << sizeof(upd2) << endl; //4字節

    return 0;
}

三. 使用場景

(一)做爲工廠函數的返回類型

  1. 工廠函數負責在堆上建立對象,可是調用工廠函數的用戶纔會真正去使用這個對象,而且要負責這個對象生命週期的管理。因此使用unique_ptr是最好的選擇

  2. unique_ptr轉爲shared_ptr很容易,做爲工廠函數自己並不知道用戶但願所建立的對象的全部權是專有的仍是共享的,返回unique_ptr時調用者能夠按照須要作變換。

(二)PImpl機制:(Pointer to Implemention)

  1. 操做方法

  (1)將曾經放在主類中的數據成員放到實現類中去,而後經過指針間接地訪問那些數據成員。此時主類中存在只有聲明而沒有定義的類型(也叫非完整類型),如Widget::Impl。

  (2)在實現類中,動態分配和歸還原那些本來應在主類中定義的那數據成員對象。即將這個數據成員放到實現類中定義(動態分配其內存)

  2. 注意事項

  (1)PImpl機制經過下降類的客戶和類實現者之間的依賴性,減小了構建遍數。

  (2)對於採用std::unique_ptr來實現的PImpl指針,須在類的頭文件中聲明特殊成員函數,但在實現文件中實現它們注意,不能直接在頭文件中實現,具體緣由見《編程實驗》中的說明)。如,必須同時聲明並實現類的析構函數。再因爲自定義了析構函數,編譯器再也不提供默認的移動構造和移動賦值函數,若是須要這些函數,則也必須在頭文件中聲明,並在實現類中去實現。

  (3)上述建議僅適用於std::unique_ptr,但並不適用於std::shared_ptr。由於刪除器在unique_ptr中是其類型的一部分,而在shared_ptr中則不是。聲明對象時,unique_ptr<T>支持T是個非完整類型,但在析構時T必須己經是個完整的類型。unique_ptr析構時會先判斷T是否爲完整類型再調用delete刪除其所指對象,但shared_ptr<T>則不會。

【編程實驗】unique_ptr的使用場合

//Widget.h

#ifndef  _WIDGET_H_
#define _WIDGET_H_
#include <memory>

//1.傳統的作法
//問題:數據成員會致使Widget.h文件必須include <string>
//      <vector>和gadget.h。當客戶包含Widget.h裏,會增長編譯時間,並且
//      若是其中的某個頭文件(如Gadget.h)發生改變,則Widget的客戶必須從新編譯!
//class Widget
//{
//    std::string name;
//    std::vector<double> data;
//    Gadget g1, g2, g3;// //自定義類型,位於gadget.h。
//public:
//    Widget();
//};

//2. 採用PImpl手法
class Widget
{
    //聲明實現結構體以及指向它的指針
    struct Impl; //注意只有聲明,沒實現。是個非完整類型。
    std::unique_ptr<Impl> pImpl; //使用智能指針而非裸指針。這裏聲明一個指針非完整類型的指針。注意針對非完整
                                 //類型,能夠作的事情極其有限。因爲unique_ptr中會將刪除器做爲其類型的一部分
                                 //所以,但unique_ptr析構被調用時,當delete其所指對象時,會先判斷T是不是個完
                                 //整類型。若是不是,則會報錯。所以必須在pImpl被析構前,確保Impl被定義(便是個完整類型)
                                 //所以,使用unique_ptr<非完整類型時>,必須爲該類同時定義析構函數!具體緣由見後面的分析。

    //std::shared_ptr<Impl> pImpl; //因爲刪除器不是shared_ptr類型的組成部分。當pImpl被析構時,不會判斷T是否爲完整類型。
                                   //所以,不要求Widget必須自定義析構函數。

public:
    Widget();
    ~Widget(); //Impl是個非完整類型,這裏必須聲明析構函數,並在Widget.cpp中實現它。
                //注意,不能在該文件中實現,由於此時unique_ptr看到的Impl是個非完整類型,unique_ptr內部要求delete前,其
                //其指向的必須是個完整類的指針。

    //移動構造和移動賦值(因爲自定義了析構函數,因此編譯器再也不提供默認的移動構造和移動賦值函數,這裏需手動填加)
    Widget(Widget&& rhs); //只能聲明,須放在.cpp中去實現。編譯器會在move構造函數內拋出異常的事件中生成析構pImpl代碼,
                          //而此處Impl爲非完整類型。
    Widget& operator=(Widget&& rhs); //只能聲明,須放在.cpp中去實現。由於移動賦值pImpl時,須要先析構pImpl所指對象,但
                                     //此時仍爲非完整類型。

    //讓Widget支持複製操做。注意unique_ptr不可複製
    Widget(const Widget& rhs);  //僅聲明
    Widget& operator=(const Widget& rhs); //僅聲明
};

#endif // ! _WIDGET_H_
Widget.h

//Widget.cpp

#include "Widget.h"

//將對string和vector和Gadget頭文件的依賴從Wigdget.h轉移動Wigdget.cpp文件中。如此,Widget類的使用者
//只需依賴Widget.h,而把複雜的依賴關係留給Widget的實現者(Widget.cpp)去處理
#include <string>
#include <vector>
class Gadget {}; //本應#include "Gardget.h",但爲了簡明起見,就直接在這裏聲明該類

//Widget::Impl的實現(包括此前在Widget中的數據成員)
struct Widget::Impl
{
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};

Widget::Widget():pImpl(std::make_unique<Impl>())
{}

//注意:析構函數必須在Widget::Impl類以後定義。由於此時調用~Widget時,會調用unique_ptr的析構函數
//而unique_ptr中會調用delete刪除其指向的對象,因爲~Widget定義在Widget::Impl以後,所以這時看到的
//Impl是個完整的類,delete前經過了unique_ptr內部完整類型的判斷!
Widget::~Widget() {}//或Widget::~Widget = default;

Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;

//make_unique(Ts&&... params)== std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
Widget::Widget(const Widget& rhs):pImpl(std::make_unique<Impl>(*rhs.pImpl))//深拷貝!
{
}

Widget& Widget::operator=(const Widget& rhs)
{
    *pImpl = *rhs.pImpl; //深拷貝!複製兩個指針所指向的內容。pImpl自己是隻移動類型
    return *this;
}

//main.cpp

#include <iostream>
#include <memory>
#include <functional>
#include "Widget.h"
using namespace std;

enum class InvestmentType {itSock, itBond, itRealEstate};
class Investment//投資
{
public:
    virtual ~Investment() {} //聲明爲virtual,以便正確釋放子類對象
};

class Stock : public Investment {};//股票
class Bond : public Investment {};  //債券
class RealEstate : public Investment {}; //不動產

void makeLogEntry(Investment* pInvmt) {}

//工廠函數
template<typename... Ts>
auto makeInvestment(Ts&&... params) //返回unique_ptr智能指針
{
    //自定義deleter
    auto delInvmt = [](Investment* pInvmt) //父類指針
    {
        makeLogEntry(pInvmt);
        delete pInvmt; //delete父類指針,全部析構函數須聲明爲virtual
    };

    std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);

    if (1/*a Stock Object should be created*/) {
        pInv.reset(new Stock(std::forward<Ts>(params)...)); //原始指針沒法隱式轉爲unique_ptr,使用reset重置全部權
    }
    else if (0/*a Bond Object should be created*/)
    {
        pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if (0/*a RealEstate should be created*/)
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }

    return pInv;
}


int main()
{
    //1. unique_ptr做爲工廠函數的返回值。
    std::shared_ptr<Investment> sp =  makeInvestment();  //從std::unique_ptr轉換到std::shared_ptr(從獨佔到共享的
                                                         //轉換簡單而高效) 

    //2. PImpl手法的測試
    Widget w;  //注意Widget的析構函數必須手動實現。不然,則當w析構時編譯器會將默認的析構函數inline
               //到這裏來,但因爲include widget.h在inline動做以前,此時編譯器看到的是非完整類型的
               //Impl類。所以Widget類中的unique_ptr析構時,delete前檢查出是個非完整類指針,從而報錯。
}
相關文章
相關標籤/搜索