Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

發展

模型
語義
接口

在C++中,我們可以聲明類型T的一個對像(變量),並且我們可以給這個對象一個初始值(通過初始化 (參考 8.5))。當一個聲明中包含非空的初始化(即給出一個初始值),就稱這個對象已被初始化。如果一個聲明使用了空的初始化(即未給出初始值),而且也沒缺省值可用於初始化,則稱該對象是未初始化的。它的真實值是存在的,但具有一個不確定的初始值(參考 8.5.9)。optional<T> 的目的是規範初始化(或無初始化)的概念,讓程序可以檢測一個對象是否已初始化,並規定訪問一個未初始化對象的值是未定義的行為。即,如果一個變量被聲明為 optional<T> 而未給定初始值,則該變量就是正式的未初始化。一個正式的未初始化的 optional 對象是沒有值的,並且可以在運行期對此狀態進行測試。嘗試訪問一個未初始化的 optional 被正式規定為未定義行為。一個未初始化的 optional 可以被賦予一個值,這時它的初始化狀態就會變為已初始化。此外,還給出處理 optional 對象的初始化狀態的方法,甚至可以將一個 optional 重新置為未初始化

在C++裡沒有未初始化對象的正式概念,即是說對像總是帶有初始值的,即使是不確定的值。正如前面所討論的,這樣有一個缺點,因為你需要用額外的信息來告知一個對象是不是已經被有效地初始化了。一種以前常用的典型方法是使用特殊值:EOF, npos, -1, 等等... 這相當於增加一個特殊值到給定類型的可取值的集合中。由 T 加上某個 nil_tnil_t 是某個無狀態的POD -- 組成的超集在現代語言中可以被模仿為由 Tnil_t 組成的一個可識別聯合。可識別聯合通常稱為 variants. 一個 variant 具有 當前類型,在我們的例子中就是 T 或者 nil_t.使用 Boost.Variant 庫,以上模型可以用 boost::variant<T,nil_t> 來實現。用可識別聯合來模仿一個 optional 值是有先例的:HaskellMaybe 內建類型構造器。所以,可識別聯合 T+nil_t 可以作為一個概念上的基礎。

variant<T,nil_t> 自然而然地可用於擴展可取值的傳統慣用法,它增加一個"哨兵"值用於表示空值。不過,這個新增的空值與我們意圖大不相關,因為我們的目標是規範化未初始化對象的概念,雖然一個特殊的擴展值可以用於表達這個意思,但是它並不是嚴格必要的。

最近的研究表明,額外的 nil_t 對於 optional<T>目的 是無關的,我們建議另一種模型:一個容器,它具有 T 的值,或者為空。

在寫這篇文檔之時,我還不知道有任何一個大小可變、容量固定(為1)、基於棧的 optional 值的容器模型的先例,但是我相信這只是因為還沒有這樣一個容器的實際實現,而不是這樣一個容器模型的固有缺點。

無論如何,可識別聯合或單元素容器模型均可作為表示 optional 類 — 即可能未初始化的 — 對象的基礎。
例如,這些模型表現出了一個 optional 值包裝器所需的準確語義:

可識別聯合:

  • 深複製 語義:variant 的拷貝即為值的拷貝。
  • 深比較 語義:比較兩個 variants 要同時匹配類型和值。
  • 如果 variant 的當前類型為 T, 它就代表已初始化的 optional.
  • 如果 variant 的當前類型不是 T, 它就代表未初始化的 optional.
  • 測試 variant 的當前類型是否為 T 即測試 optional 是否已初始化。
  • 試圖從一個 variant 取出 T 而它的當前類型不是 T 時,正好符合了訪問一個未初始化 optional 值的未定義行為。
  • 單元素容器:

  • 深複製 語義:container 的拷貝即為值的拷貝。
  • 深比較 語義:比較兩個 containers 要先比較容器的大小,如果匹配再比較容器中的值。
  • 如果 container 非空(包含一個類型為 T 的對象),它就代表已初始化的 optional.
  • 如果 container 為空,它就代表未初始化的 optional.
  • 測試 container 是否為空即測試 optional 是否已初始化。
  • 試圖從一個空的 container 取出 T,正好符合了訪問一個未初始化 optional 值的未定義行為。
  • 語義

    類型 optional<T> 的對象的設計意圖是,用在那些使用可能未被初始化的類型 T 的對象的地方。因此,optional<T> 的意圖是對增加可能的未初始化狀態進行規範化。從這一任務的觀點出發,optional<T> 可以具有與 T 相同的操作語義,再加上與特殊狀態相對應的額外語義。同樣,optional<T> 可被視為 T 的一個 超類型supertype. 當然,我們不能在C++中這樣做,所以我們需要用一種不同的機制來得到想要的語義。使用其它方法實現它,如讓 optional<T> 作為 T子類,不僅是概念上的錯誤,而且也是不實際的:因為不允許從非類類型派生,如從內建類型派生。

    我們可以從 optional<T> 的目的描畫出所需的基本語義:

    • 缺省構造:引入一個正式的未初始化包裝對象。
    • 通過複製進行直接的值構造:引入一個正式的已初始化包裝對象,其值來自於某個對象的拷貝。
    • 深複製構造:獲得一個新的、等價的包裝對象。
    • 直接的值賦值(到已初始化的對象):將一個值賦值到包裝對象。
    • 直接的值賦值(到未初始化的對象):從某個對象的拷貝取得值,初始化包裝對象。
    • 賦值(到已初始化的對象):將另一個包裝對象的值賦值到包裝對象。
    • 賦值(到未初始化的對象):以另一個包裝對象的值初始化包裝對象。
    • 深比較操作(僅當被類型T支持時):比較兩個包裝對象的值,包括未初始化狀態的情形。
    • 值訪問:對包裝對像去包裝。
    • 初始化狀態查詢:判斷該對象是否已正式初始化。
    • 交換:交換包裝的對象。(無論 T 的交換操作提供何種異常安全性保證)。
    • 去初始化:釋放包裝的對象(如果有),並使包裝器保留未初始化狀態。

    其它操作也是有用的,如轉換構造函數和轉換賦值,就地構造和賦值,以及通過包裝對象的一個指針或空指針進行安全的值訪問。 

    由於 optional 的目的是允許我們使用增加了正式的未初始化狀態的對象,所以它的接口應該盡可能跟隨底層的類型 T. 為了選擇適當的原本的 T 接口,應該留意以下幾點:即使被類型 T 的實例所支持的所有操作是為該類的整個值域所定義的,但是 optional<T> 將該值域擴充了一個新值,所以多數操作對此沒有定義。

    此外,由於 optional<T> 本身只不過是一個 T 的包裝(類似於 T 的一個超類),任何針對未初始化的 optional 的操作定義都應完全與 T 相關。

    本庫所選擇的接口是,沿用 T 的接口,只要那些操作是有良好定義的(與類型 T 有關),即使某些操作數未初始化。這樣的操作包括有:構造,複製構造,賦值,交換和比較操作。

    對於值訪問操作,如果操作數是未初始化的,則是未定義的(與類型 T 有關),所以選用了不同的接口(稍後將會解釋)。

    還有,未初始化狀態的可能出現需要有更多的操作,這些操作是 T 本身所不具備的,它們將以特定的接口提供。

    對可能未初始化的 optional 對象的帶提示的值訪問:操作符 * 和 ->

    指針的一個特點是它可以具有 空指針值。這是一個特殊值,用於表示該指針沒有指向任何對象。換句話說,空指針值表達的意思是不存在的對象。

    空指針值的這個意義使得指針成為了處理 optional 對象的事實標準,因為當你要引向一個實際上不存在的值時,你要做的只是使用適當類型的空指針值。指針已經被使用了幾十年 — 從 C APIs 一直到現代的 C++ 庫 — 用來表示 optional (即可能不存在的)對像;尤其是用作函數的可選參數,但也常被用作可選數據成員。

    空指針值的可能出現使得訪問被指物值的操作可能是無定義的,所以,使用提領和訪問操作符的表達式,如 ( *p = 2 )( p->foo()), 暗含了可選性的概念,此類信息被表達式的語法所依賴。即,操作符 *-> 本身 — 無需任何額外的上下文 —就說明了該表達式可以是無定義的,除非被指物確實存在。

    這個表示 optional 對象的事實上的慣用法可以用一個概念來規範化:OptionalPointee 概念。該概念包含了操作符 *, -> 以及轉換到 bool 的語法,以表達可選性的概念。

    雖然指針適合於表示 optional 對象,但是並不特別適合處理 optional 對象的其它方面,如初始化或移動/複製。問題主要在於指針語義的淺複製:如果你需要高效地移動或複製對象,那麼僅有指針是不夠的。問題是指針的拷貝並不代表被指物的拷貝。例如,像在"動機"一節中所討論的,僅用指針並不能從一個函數返回 optional 對象,因為該對像必須從函數中移至外部,進入調用者的上下文。

    解決淺複製問題的一個常用方法是,採用動態分配並使用智能指針來自動處理其細節。例如,如果一個函數可選性地返回一個對像 X, 則可以使用 shared_ptr<X> 作為返回值。不過,這要求對 X 進行動態分配。如果 X 是內建的或是小的 POD, 該技術在資源方面很不划算。Optional 對像本質上是值對象,所以,如果可以像我們處理普通的值對像那樣,使用自動存儲和深複製語義來維護 optional 值,才是最方便的。指針不具有這種語義,所以不適於初始化和轉移 optional 值,但還是非常適合於訪問有可能無定義的值,因為該用法已存在於以指針為代表的 OptionalPointee 概念中。

    Optional<T> 作為 OptionalPointee 的模型

    對於值訪問操作,optional<> 使用操作符 *-> 來提醒有可能出現的未初始化狀態,這類似於指針語義中的空指針用法。

    [Warning] 警告

    但是,要重點留意的是,optional<> 對像不是指針。optional<> 不是也不符合指針的概念

    例如,optional<> 沒有淺複製,所以也沒有別名:兩個不同的 optional 永遠不會引向同一個值,除非 T 本身是一個引用(但可以有相等的值)。必須注意 optional<T> 和指針間的區別,特別是因為它們的比較操作符的語義是不同的:因為 optional<T> 是一個值包裝器,比較操作符是"深"的:它們比較 optional 的值;而指針的比較操作符則是"淺"的:它們不比較被指物的值。所以,你也許可以在某些情形下用 T* 替換 optional<T>,但並不是總可以這樣做。特別是,在為兩者所寫的泛型代碼中,你不能直接使用比較操作符,而必須使用模板函數 equal_pointees()less_pointees() 來代替。


    PrevUpHomeNext