C++17中的std::optional的具體使用
直入主題
本篇之中,僅僅述及 std::optional ,其它和 variant 相關(guān)的話題以后再說(shuō)吧。
std::optional 也劃入 variant 類(lèi)別中,其實(shí)它還是談不上可稱(chēng)為變體類(lèi)型的,但新版本中的三大件(optional,any and variant)也可以歸一類(lèi)無(wú)妨。
C++17 之前
在 C 時(shí)代以及早期 C++ 時(shí)代,語(yǔ)法層面支持的 nullable 類(lèi)型可以采用指針?lè)绞剑?T* ,如果指針為 NULL (C++11 之后則使用 nullptr ) 就表示無(wú)值狀態(tài)(empty value)。
typedef template <typename T> T* NullableT; NullableT<int> pInt = nullptr;
為了更好地使用這個(gè)類(lèi)別而不是總是采用指針,需要對(duì)其進(jìn)行封裝。下面給出一個(gè)示例(但并未完善):
// 使用 C++11 語(yǔ)法 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> }
所以,這個(gè) Nullable<T> 現(xiàn)在很像 C# 或者 Kotlin 中的 T?。使用它和直接使用 T 差不多,只是隱含著 new/delete 的額外開(kāi)銷(xiāo),當(dāng)然我們也可以采用別的實(shí)現(xiàn)方案例如增加一個(gè)額外的 bool 成員變量來(lái)表示是否尚未賦值,這樣就可以去掉 heap allocating 開(kāi)銷(xiāo),孰優(yōu)孰劣也未必可以計(jì)較。
std::optional in C++17
std::optional 類(lèi)似于 Nullable<T> 和 std::variant 的聯(lián)合體,它管理一個(gè) Nullable 變體類(lèi)型。
但它和 Nullable<T> 不同之處在于,optional 實(shí)現(xiàn)的更為精煉和全面:Nullable 是剛才我手寫(xiě)的,甚至沒(méi)經(jīng)過(guò)編譯器檢驗(yàn),也缺乏大多數(shù)重載以及構(gòu)造特性。optional 在構(gòu)造對(duì)象的開(kāi)銷(xiāo)方面比 Nullable 好無(wú)數(shù)倍,因?yàn)樗軌蚶迷粯?gòu)造特性使得自身的開(kāi)銷(xiāo)趨向于 0 而只需要 T 對(duì)象的構(gòu)造開(kāi)銷(xiāo),而 Nullable 為了表達(dá)出早期(C++03)的狀態(tài)直接采用了 new/delete 來(lái)簡(jiǎn)化代碼。
如果想要改進(jìn)前文中 Nullable<T> 的實(shí)現(xiàn),使其和 optional 一樣地完善,則需要關(guān)注如下幾點(diǎn):
- 去掉 new / delete 機(jī)制,考慮采用一個(gè)空結(jié)構(gòu)來(lái)表達(dá)尚未賦值的狀態(tài):事實(shí)上,optional 使用了 std::nullopt_t 來(lái)表述該狀態(tài)。
- 完善操作符重載
- 加入 swap 特性支持
- 加入原位構(gòu)造特性支持
optional 和 variant 也不同,variant 是提前確定好一組可選的類(lèi)型,你只能在這一組類(lèi)型中進(jìn)行變換,而 optional 是具體化到一個(gè)特定類(lèi)型的,你不能動(dòng)態(tài)地將不同類(lèi)型的值賦予 optional 的變量。
optional 從語(yǔ)法意義上來(lái)說(shuō),就是一個(gè)完美版的 Nullable<T> ,你可以將其和 Kotlin 的可空類(lèi)型等價(jià)。
使用
我們可以以多種方式來(lái)構(gòu)造、聲明 optional 的變量,最原始的方式是在構(gòu)造參數(shù)時(shí)傳入值對(duì)象:
std::optional<int> opt_int(72); std::optional opt_int2(8); std::optional opt_int2(std::string("a string"));
使用 std::make_optional<T> 是比較 meaningful 的一種,而且也是更整潔的原位構(gòu)造:
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};
使用原位構(gòu)造
// 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 可以用于測(cè)試有沒(méi)有值,是否尚未賦值:
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 值的方法,含義明顯,不必贅述。當(dāng)無(wú)值或者類(lèi)型不能轉(zhuǎn)換時(shí),value() 有可能拋出異常 std::bad_optional_access,如果想要避免則可以使用 value_or。
對(duì)于復(fù)合對(duì)象來(lái)說(shuō),原位構(gòu)造方式賦值 emplace 也是可用的。同樣地也可以善加利用 swap。
應(yīng)用
optional 相當(dāng)于一個(gè)全類(lèi)型的 Nullable 類(lèi)型,所以在運(yùn)用工廠模式時(shí)將其作為創(chuàng)建器的返回值將會(huì)是非常適合的選擇,好過(guò)無(wú)包裝的 T* 或者智能指針。因?yàn)楫?dāng)你使用智能指針的工廠模式時(shí),創(chuàng)建器只能創(chuàng)建基于一個(gè)公共基類(lèi)的實(shí)例,所以受制較多。但采用 optional 時(shí)則不會(huì)收到基類(lèi)指針的限制。
下面是來(lái)自于 cppreference 的示例:
#include <string> #include <functional> #include <iostream> #include <optional> // optional 可用作可能失敗的工廠的返回類(lèi)型 std::optional<std::string> create(bool b) { if(b) return "Godzilla"; else return {}; } // 能用 std::nullopt 創(chuàng)建任何(空的) 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 的工廠函數(shù)可用作 while 和 if 的條件 if (auto str = create2(true)) { std::cout << "create2(true) returned " << *str << '\n'; } if (auto str = create_ref(true)) { // 用 get() 訪問(wèn) 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
此外,在搜索算法中返回搜索結(jié)果或者返回沒(méi)找到狀態(tài),可以不必使用 bool 加上 search::result 了,可以直接返回 std::optional<search::result>。
這樣的設(shè)計(jì)策略完全可以產(chǎn)生深遠(yuǎn)的影響。從有潔癖的我的心態(tài)出發(fā),大多數(shù)類(lèi)庫(kù)都可以據(jù)此重新改寫(xiě),從而得到更簡(jiǎn)練、更 meaningful 的接口。而更富有表達(dá)力的接口反過(guò)來(lái)也能影響到算法的實(shí)現(xiàn)部分,它們將會(huì)變得更易讀,更可維護(hù)。
那些 Machine Learning 算法,寫(xiě)出來(lái)如同天書(shū)一般,但借助新的手段重構(gòu)的話,有望可以增進(jìn)理解程度。
所以,像 C# 具有了 Nullable 類(lèi)型幾十年(稍稍有點(diǎn)夸張)了之后,C++17 才正式支持 std::optional 實(shí)在是相當(dāng)操蛋的一件事情。
和 Kotlin 比較Permalink
和 Kotlin 相比較的話,現(xiàn)階段的 optional 不但冗長(zhǎng),而且缺乏一大組閉包工具(let,apply,類(lèi)型診斷,空安全)。多數(shù)人將這些工具稱(chēng)作語(yǔ)法糖,但我更希望它們被視為必需品。下面是一段 Kotlin 的代碼塊,可以看出整體上它們的簡(jiǎn)練性,而 std::optional 嘛,實(shí)際上還差得遠(yuǎn),看起來(lái)也不可能趕得上了:
if (obj is String!!) { // 對(duì)于 String? obj 也一樣生效,自動(dòng)升級(jí)為非空版本 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 自動(dòng)轉(zhuǎn)換為字符串 } } when (x) { is Int -> print(x + 1) is String -> print(x.length + 1) is IntArray -> print(x.sum()) } // 可空類(lèi)型的集合 val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull() // 可空類(lèi)型的簡(jiǎn)化診斷代碼塊 Int? zz = 8; zz?.let { sum += it // 僅當(dāng) zz 非空時(shí), 塊內(nèi)才被執(zhí)行,it 表示 zz 的非空版 }
Kotlin 的這套語(yǔ)法機(jī)制真的是讓人如同吃了人參果,無(wú)一個(gè)毛孔不舒服。但是它的實(shí)現(xiàn)機(jī)制是低代價(jià)而非無(wú)代價(jià)的,從這一點(diǎn)上來(lái)說(shuō),C++ 將不可能采納等效的新語(yǔ)法,只能使用 std::optional<T> 這樣的老奶奶裹腳布方案了。但它至少比沒(méi)有的好。
小結(jié)
通過(guò)和 Kotlin 的比較,我們不無(wú)悲哀地看到,比較于 C++11 甚至于 C++98,optional 固然是個(gè)提升,然而受制于 C++ 標(biāo)準(zhǔn)委員會(huì)以及歷史包袱的原因,簡(jiǎn)練有效的表達(dá)方式在現(xiàn)在不可能,在未來(lái)的 C++2x, 3x 中也應(yīng)該是行不通的。
參考鏈接
到此這篇關(guān)于C++17中的std::optional的具體使用的文章就介紹到這了,更多相關(guān)C++17 std::optional內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)通訊錄功能的流程與代碼
通訊錄是一個(gè)可以記錄親人、好友信息的工具,這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)通訊錄管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06C++實(shí)現(xiàn)對(duì)RGB圖片進(jìn)行編碼的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用得到的RGB信息重新對(duì)RGB圖片進(jìn)行編碼,以及對(duì)其他圖片如BMP所得到的RGB信息進(jìn)行編碼從而得到*.jpg文件,感興趣的可以了解一下2023-05-05C語(yǔ)言實(shí)現(xiàn)掃雷項(xiàng)目
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)掃雷項(xiàng)目,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-07-07C++ static詳解,類(lèi)中的static用法說(shuō)明
這篇文章主要介紹了C++ static詳解,類(lèi)中的static用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07C語(yǔ)言演示對(duì)歸并排序算法的優(yōu)化實(shí)現(xiàn)
這篇文章主要介紹了C語(yǔ)言演示對(duì)歸并排序算法的優(yōu)化實(shí)現(xiàn),歸并排序的最差時(shí)間復(fù)雜度為(n\log n),最優(yōu)時(shí)間復(fù)雜為(n),存在可以改進(jìn)的空間,需要的朋友可以參考下2016-05-05C語(yǔ)言FlappyBird飛揚(yáng)的小鳥(niǎo)實(shí)現(xiàn)開(kāi)發(fā)流程
因?yàn)樵诩艺撕枚嗵?,隨手玩了下自己以前做的一些小游戲,說(shuō)真的,有幾個(gè)游戲做的是真的劣質(zhì),譬如 flappybird 真的讓我難以忍受,于是重做了一波分享給大家2022-11-11C語(yǔ)言使用ffmpeg和sdl實(shí)現(xiàn)多路音頻播放
這篇文章主要為大家詳細(xì)介紹了一種基于ffmpeg和sdl實(shí)現(xiàn)的音頻多路混合的方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2023-06-06