您好,登錄后才能下訂單哦!
這篇文章給大家介紹std::optional如何在C++17中使用,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
在 C 時代以及早期 C++ 時代,語法層面支持的 nullable 類型可以采用指針方式: T* ,如果指針為 NULL (C++11 之后則使用 nullptr ) 就表示無值狀態(empty value)。
typedef template <typename T> T* NullableT; NullableT<int> pInt = nullptr;
為了更好地使用這個類別而不是總是采用指針,需要對其進行封裝。下面給出一個示例(但并未完善):
// 使用 C++11 語法 namespace cmdr { template<typename T> class Nullable { public: Nullable() = default; virtual ~Nullable(){ if (_value) delete _value; } public: Nullable(const Nullable &o) { _copy(o); } Nullable &operator=(const Nullable &o) { _copy(o); return *this; } Nullable &operator=(const T &o) { this->_value = o; return *this; } private: void _copy(const Nullable &o) { this->_value = o._value; } public: T &val() { return *_value; } const T &val() const { return *_value; } void val(T &&v) { if (!_value) _value = new T; (*_value) = v; } explicit operator T() const { return val(); } explicit operator T() { return val(); } // operator -> // operator * [[nodiscard]] bool is_null() const { return !_value; } private: T *_value{nullptr}; };// class Nullable<T> }
所以,這個 Nullable<T> 現在很像 C# 或者 Kotlin 中的 T?。使用它和直接使用 T 差不多,只是隱含著 new/delete 的額外開銷,當然我們也可以采用別的實現方案例如增加一個額外的 bool 成員變量來表示是否尚未賦值,這樣就可以去掉 heap allocating 開銷,孰優孰劣也未必可以計較。
std::optional 類似于 Nullable<T> 和 std::variant 的聯合體,它管理一個 Nullable 變體類型。
但它和 Nullable<T> 不同之處在于,optional 實現的更為精煉和全面:Nullable 是剛才我手寫的,甚至沒經過編譯器檢驗,也缺乏大多數重載以及構造特性。optional 在構造對象的開銷方面比 Nullable 好無數倍,因為它能夠利用原位構造特性使得自身的開銷趨向于 0 而只需要 T 對象的構造開銷,而 Nullable 為了表達出早期(C++03)的狀態直接采用了 new/delete 來簡化代碼。
如果想要改進前文中 Nullable<T> 的實現,使其和 optional 一樣地完善,則需要關注如下幾點:
去掉 new / delete 機制,考慮采用一個空結構來表達尚未賦值的狀態:事實上,optional 使用了 std::nullopt_t 來表述該狀態。
完善操作符重載
加入 swap 特性支持
加入原位構造特性支持
optional 和 variant 也不同,variant 是提前確定好一組可選的類型,你只能在這一組類型中進行變換,而 optional 是具體化到一個特定類型的,你不能動態地將不同類型的值賦予 optional 的變量。
optional 從語法意義上來說,就是一個完美版的 Nullable<T> ,你可以將其和 Kotlin 的可空類型等價。
我們可以以多種方式來構造、聲明 optional 的變量,最原始的方式是在構造參數時傳入值對象:
std::optional<int> opt_int(72); std::optional opt_int2(8); std::optional opt_int2(std::string("a string"));
使用 std::make_optional<T> 是比較 meaningful 的一種,而且也是更整潔的原位構造:
auto opt_double = std::make_optional(3.14); auto opt_complex = std::make_optional<std::complex<double>>(3.0, 4.0); std::optional<std::complex<double>> opt_complex2{std::in_place, 3.0, 4.0};
使用原位構造
// constructing a string in-place std::optional<std::string> o1(std::in_place, "a string"); // with a repeated spaces std::optional<std::string> o1(std::in_place, 8, ' ');
has_value 可以用于測試有沒有值,是否尚未賦值:
auto x = std::make_optional(9); std::optional<int> y; assert(x.hash_value() == true); assert(y.hash_value() == false); std::cout << x.value(); std::cout << y.value_or(0);
value() 和 value_or() 是抽出 T 值的方法,含義明顯,不必贅述。當無值或者類型不能轉換時,value() 有可能拋出異常 std::bad_optional_access,如果想要避免則可以使用 value_or。
對于復合對象來說,原位構造方式賦值 emplace 也是可用的。同樣地也可以善加利用 swap。
optional 相當于一個全類型的 Nullable 類型,所以在運用工廠模式時將其作為創建器的返回值將會是非常適合的選擇,好過無包裝的 T* 或者智能指針。因為當你使用智能指針的工廠模式時,創建器只能創建基于一個公共基類的實例,所以受制較多。但采用 optional 時則不會收到基類指針的限制。
下面是來自于 cppreference 的示例:
#include <string> #include <functional> #include <iostream> #include <optional> // optional 可用作可能失敗的工廠的返回類型 std::optional<std::string> create(bool b) { if(b) return "Godzilla"; else return {}; } // 能用 std::nullopt 創建任何(空的) std::optional auto create2(bool b) { return b ? std::optional<std::string>{"Godzilla"} : std::nullopt; } // std::reference_wrapper 可用于返回引用 auto create_ref(bool b) { static std::string value = "Godzilla"; return b ? std::optional<std::reference_wrapper<std::string>>{value} : std::nullopt; } int main() { std::cout << "create(false) returned " << create(false).value_or("empty") << '\n'; // 返回 optional 的工廠函數可用作 while 和 if 的條件 if (auto str = create2(true)) { std::cout << "create2(true) returned " << *str << '\n'; } if (auto str = create_ref(true)) { // 用 get() 訪問 reference_wrapper 的值 std::cout << "create_ref(true) returned " << str->get() << '\n'; str->get() = "Mothra"; std::cout << "modifying it changed it to " << str->get() << '\n'; } } // Output create(false) returned empty create2(true) returned Godzilla create_ref(true) returned Godzilla modifying it changed it to Mothra
此外,在搜索算法中返回搜索結果或者返回沒找到狀態,可以不必使用 bool 加上 search::result 了,可以直接返回 std::optional<search::result>。
這樣的設計策略完全可以產生深遠的影響。從有潔癖的我的心態出發,大多數類庫都可以據此重新改寫,從而得到更簡練、更 meaningful 的接口。而更富有表達力的接口反過來也能影響到算法的實現部分,它們將會變得更易讀,更可維護。
那些 Machine Learning 算法,寫出來如同天書一般,但借助新的手段重構的話,有望可以增進理解程度。
所以,像 C# 具有了 Nullable 類型幾十年(稍稍有點夸張)了之后,C++17 才正式支持 std::optional 實在是相當操蛋的一件事情。
和 Kotlin 相比較的話,現階段的 optional 不但冗長,而且缺乏一大組閉包工具(let,apply,類型診斷,空安全)。多數人將這些工具稱作語法糖,但我更希望它們被視為必需品。下面是一段 Kotlin 的代碼塊,可以看出整體上它們的簡練性,而 std::optional 嘛,實際上還差得遠,看起來也不可能趕得上了:
if (obj is String!!) { // 對于 String? obj 也一樣生效,自動升級為非空版本 print(obj.length) } if (obj !is String) { // 與 !(obj is String) 相同 print("Not a String") } else { print(obj.length) } fun demo(x: Any) { if (x is String) { print(x.length) // x 自動轉換為字符串 } } when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) } // 可空類型的集合 val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull() // 可空類型的簡化診斷代碼塊 Int? zz = 8; zz?.let { sum += it // 僅當 zz 非空時, 塊內才被執行,it 表示 zz 的非空版 }
關于std::optional如何在C++17中使用就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。