Effective Modern C++ 條款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會銷燬它的資源,默認情況下,資源銷燬是通過對std::unique_ptr內的原生指針使用delete來完成的。

std::unique_ptr的一個常見使用是作爲工廠函數的返回類型(在分層中)。假如我們有一個投資(investment)類型的分層(例如,包含股票stock,債券bond,房地產real estate,等等),基類是Inverstment:

這裏寫圖片描述

class Investment { ... };

class Stock : public Investment { ... };

class Bond : public Investment { ... };

class RealEstate : public Investment { ... };

這種分層的工廠返回通常從堆上分配一個對象,然後返回一個指向它的指針,當不再需要它的時候,調用者要負責delete這個對象。那真是完美匹配std::unique_ptr,因爲調用者需要爲工廠返回的資源負責(即獨佔資源的所有權),然後std::unique_ptr在它銷燬的時候可以自動delete它指向的對象。Investment分層的工廠函數應該這樣聲明:

template <typename... Ts>        // 返回一個由給定參數
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最終還是會調用析構函數,隨後銷燬管理的資源。

默認情況下,std::unique_ptr是藉助delete來銷燬管理的資源,但是,在構造std::unique_ptr期間,你可以指定使用自定義的刪除器:當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, decltypedelInvmt)>
       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 obejct should be created */ )
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }

    return pInv;
}

等下我會講解它是怎樣工作的,不過先來考慮在調用者看來,事情是怎樣的。假定你用auto變量存儲makeInvestment的結果,你對於你申請的資源在釋放時的特殊處理毫不知情,卻傻傻地欣然使用。事實上呢,你是可以沉浸在歡喜之中,因爲使用std::unique_ptr意味着你不用關心資源什麼時候銷燬,更不用說資源銷燬是一定會發生的。std::unique_ptr會自動處理好所有的事情,從用戶的角度看,makeInvestment這個接口太爽了。

一旦你理解了下面的內容,你會覺得makeInvestment的實現也非常漂亮:

  • delInvmt是從makeInvestment返回對象的自定義刪除器。所有的自定義刪除函數都是接受一個指向需要銷燬的對象的原生指針作爲參數,然後它做的事情是銷燬對象的必要工作。在這個例子中,刪除器的行爲是調用makelogEntry,然後應用delete。使用lambda表達式創建delInvmt是很方便的,不過我們很快就能看到,它還會比傳統函數高效。
  • 當我們使用自定義的刪除器的時候,它的類型要作爲std::unique_ptr的第二個模板參數。在這個例子中,那是delInvmt的類型,這也是爲什麼makeInvestment的返回類型是std::unique_ptr<Investment, decltype(delInvmt)>。(關於decltype,看條款3。)
  • makeInvestment的基本策略是先創建一個空的std::unique_ptr,然後指向一個類型合適的對象,然後返回它。爲了關聯刪除器delInvmtpInvmt,我們把刪除器作爲第二個構造參數傳遞給std::unique_ptr
  • 試圖將原生指針(例如new出來的指針)賦值給std::unique_ptr是不會通過編譯的,因爲這導致一個從原生指針到智能指針的隱式轉換,這樣的隱式轉換是有問題的,所以C++11的智能指針禁止這樣的轉換。那就是爲什麼reset被用來——讓pInvmt得到new出來的對象的所有權。
  • 對於每個new,我們都用std::forward來完美轉發makeInvestment的參數。這樣的話,創建對象的構造函數能得到調用者提供的所有的信息。
  • 自定義刪除器的參數類型是Investment *。不管makeInvestment函數實際創建的對象是什麼類型(例如,Stock,Bond,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)。如果刪除器是函數對象,std::unique_ptr的大小改變取決於函數對象存儲了多少狀態。無狀態的函數對象(例如,不捕獲變量lambda表達式)不會受到一絲代價,這意味着當自定義刪除器即可用函數實現又可用不捕獲變量的lambda表達式實現時,lambda實現會更好:

auto delInvmt1 = [](Investment* pInvestment)           // 自定義刪除器是
                 {     // 不捕獲變量的
                     makeLogEntry(pInvestment);      // lambda表達式
                     delete pInvestment;
                 };

template <typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt11)>   // 返回類型的大小
makeInvestment(Ts&&... args);                      // 與Investment*相同

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的技術。這種代碼不難實現,但是不夠直截了當,因此條款22我會專門講它。

std::unique_ptr有兩種形式,一種是單獨的對象(std::unique_ptr<T>),另一種是數組(std::unique_ptr<T[]>)。這樣的結果是,std::unique_ptr指向的實體類型決不會是含糊的。std::unique_ptr的設計可以精確匹配你使用的形式。例如,單獨的對象形式是沒有下標引用操作(operator[]),而數組形式沒有解引用操作(operator*和operator->)。

std::unique_ptr的數組形式的知道就好啦,因爲比起原生數組,std::arraystd::vetcorstd::string實際上更好的數據結構。我能想到的std::unique_ptr數組唯一有意義場景就是,當你使用C-like風格的API,並且這API返回一個指像數組的指針,數組是從堆分配的,你需要對這個數組負責。

在C++11中,std::unique_ptr是表達獨佔所有權的方式,但它最吸引人的一個特性是它能即簡單又高效地轉化爲std::shared_ptr

std::shared_ptr<Investment> sp =    // 把 std::unique_ptr轉換爲
    makeInvestment(argument);         // std::shared_ptr

這是爲什麼std::unique_ptr如此適合做工廠函數的關鍵原因,工廠函數不會知道:獨佔所有權語義和共享所有權語義哪個更適合調用者。通過返回一個std::unique_ptr,工廠提供給調用者的是最高效的智能指針,但它不妨礙調用者用std::shared_ptr來替換它(關於std::shared_ptr的信息,看條款19)。


總結

需要記住的3點:

  • std::unique_ptr是一個智能,快速,只可移動的智能指針,它以獨佔所有權語義管理資源。
  • 默認情況下,通過delete來銷燬資源,但可以指定自定義刪除器。有狀態的刪除器和函數指針作爲std::unique_ptr的刪除器會增加std::unique_ptr對象的大小。
  • std::unique_ptr轉換爲std::shared_ptr是容易的。
發佈了18 篇原創文章 · 獲贊 43 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章