第20課 unique_ptr獨占型智能指針


一. unique_ptr的基本用法

(一)初始化方式

  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是個只移動類型。

  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字節

二. 剖析unique_ptr

(一)源碼分析【節選】

//指向單對象
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前檢查出是個非完整類指針,從而報錯。
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM