咱們在不少編程場合下都須要用到「可選」的概念,好比可選的參數,可選的返回值等。但對這一方面,傳統C/C++支持得略顯不足。下面經過幾個實例說明這一問題。python
在二分查找算法中,有可能咱們要查找的值不在集合裏,這時咱們該怎麼表示呢?二分算法在前面的文章中有提供,給出了Python和Haskell版本:ios
#python def binary_search(list, item): low = 0 high = len(list)—1 while low <= high: mid = (low + high) guess = list[mid] if guess == item: return mid if guess > item: high = mid - 1 else: low = mid + 1 return None
--Haskell import qualified Data.Vector as V binarySearch :: (Ord a)=> V.Vector a -> Int -> Int -> a -> Maybe Int binarySearch vec low high e | low > high = Nothing | vec V.! mid > e = binarySearch vec (mid+1) high e | vec V.! mid < e = binarySearch vec low (mid-1) e | otherwise = Just mid where mid = low + ((high-low) `div` 2)
能夠看出,Python使用了None
表示值找不到,Haskell使用Nothing
表示元素找不到,都沒使用一些特定的數字來表示找不到的錯誤;二者大同小異,都表示函數返回值是"可選"的,即返回結果可能失敗。最直觀的好處是:使用類型表示這種狀況能夠給調用者更多顯式的返回結果的信息,函數可讀性更高。算法
而在傳統的C/C++裏是沒有相應支持的,咱們只能:編程
int binary_search(const std::vector<int> &list, int item) { size_t low{0}; size_t high{list.size() - 1}; while (low <= high) { auto mid = (low + high); auto guess = list[mid]; if (guess == item) { return mid; } else if (guess > item) { high = mid - 1; } else { low = mid + 1; } } return -1; }
在這裏咱們使用特定值-1
表示item
沒有找到。函數
一樣,做爲函數參數,咱們在某些狀況下也有參數可選
的需求。若是咱們調用函數時,若不指定該參數,會使用參數的默認值填充該參數。在標準庫中,不少函數使用了這一策略。工具
好比:標準庫std::string
類中的成員函數:size_type find( const basic_string& str, size_type pos = 0 ) const noexcept;
size_type find_last_of( const basic_string& str, size_type pos = npos ) const noexcept;
spa
一個正向查找,一個反向查找,pos
參數默認取一個特定的值,在這裏分別取0
和std::string::npos
。code
然而在函數類型中,參數的類型仍然是size_type
,並無給調用者提供多少有用的信息。在其餘語言中,這方面作的要相對更好,好比Haskell中,咱們仍然可使用Maybe T
類型做爲函數的參數,一目瞭然就能夠看出這個參數須要處理可選狀況。圖片
下面咱們討論傳統方式都有哪些缺點。內存
從以上兩個應用實例可看出,傳統方式實際上就是經過特定的值表示「可選」的概念。這種方式有什麼缺點呢?
輸入參數經過默認參數機制實現,相對來講還能看出點信息;但返回值可選的狀況,咱們徹底從函數簽名裏看不出來一點信息,只能經過API文檔得知。
按照慣例,經過找不到的狀況,都會使用-1
、nullptr
等無心義的值;但慣例對編譯器是沒有約束力的,只能人爲遵照,因此頗有可能某些函數沒有按照慣例來,最後致使的:不一樣的庫慣例不一致,甚至同一個庫不一樣人寫的函數使用的慣例也不一致,千差萬別,會提升使用的成本。固然,標準庫是比較統一的,但這只是暫時掩蓋了問題,而沒有根除問題發生的緣由。
輸入參數取值更加不統一,有些人喜歡使用有效的參數值做爲默認參數,像find
函數那樣;有些人喜歡使用無效值做爲默認參數,像find_last_of
同樣。使用有效值的優勢是有助於理解,但某些狀況下沒法使用有效值,好比find_last_of
的狀況,由於字符串的大小是無法靜態知道的。使用無效值避免了有效值的問題,但引起其餘問題:偏函數的時候能夠找到無效值,但全函數對於全部的參數都是有效的,這怎麼找?
因此,因爲以上缺點,C++終於在C++17引入了std::optional<>
工具。
該工具相對容易使用,須要引入頭文件#include <optional>
。
下面分三塊說明其使用方式:
假設要改造標準庫的find
函數,咱們只需將簽名修改成:size_type find( const basic_string& str, std::optional<size_type> pos = std::nullopt) const noexcept
能夠看到,pos
已經成爲可選類型optional<size_type>
,同時咱們使用std::nullopt
常量做爲其默認值。std::nullopt
是標準庫定義的特殊常量,用來表示pos
參數沒有被賦值過。
即便參數換了類型,對函數的調用方式沒有任何影響。咱們仍然能夠這麼調用:
std::string line {"abcd123445555"}; line.find("add"); //使用默認值 line.find("add", 1); //從第二個字符開始
參照參數類型的改動,依葫蘆畫瓢地修改binary_search
爲:
std::optional<int> binary_search(const std::vector<int> &list, int item) { size_t low{0}; size_t high{list.size() - 1}; while (low <= high) { auto mid = (low + high); auto guess = list[mid]; if (guess == item) { return mid; } else if (guess > item) { high = mid - 1; } else { low = mid + 1; } } return std::nullopt; }
跟參數賦值同樣,因爲std::optional<T>
提供了類型T
到std::optional<T>
的賦值轉換,咱們能夠直接返回T
類型的值。
處理可選參數和可選返回值的操做是同樣的,咱們以處理可選返回值爲例說明。
... auto found = binary_search(list, 2); ////由於標準庫提供了到bool的默認類型轉換,能夠直接使用if判斷 if (found) { std::cout << "found " << *found ; //可以使用*found取值 } //咱們也能夠這樣使用has_value()成員函數 if (found.has_value()) { std::cout << "found " << found->value(); //使用成員函數value取值 } //由於<optional>已經對操做符重載,咱們還可使用. if (found != std::nullopt) { std::cout << "found " << (*found).value(); }
若是咱們不判斷found
是否包含有效值而直接使用,此時可能會拋出std::bad_optional_access
異常,須要捕捉;
try { int n = found.value(); } catch(const std::bad_optional_access& e) { std::cout << e.what() << '\n'; }
捕捉異常會讓執行流程中斷,若是咱們取到無效值的時候按0
處理,能夠:
int n = found.value_or(0);
這樣能讓流程更平滑地執行下去。
以上就是該工具主要的用法,咱們用一個例子結束該篇文章。模擬用戶登陸場景:用戶使用登陸名獲取用戶ID,從而完成登陸。咱們簡單模擬了這個過程,定義了兩個函數,get_user_from_login_name
和write_login_log
,函數比較簡單,就不解釋了。這裏簡化了登陸場景,只要用戶登陸名在系統內存在就算登陸成功。
#include<iostream> #include <vector> #include <optional> #include <map> void write_login_log(int user_id, std::optional<time_t> cur_time = std::nullopt) { time_t cur = 0; if (cur_time) { cur = *cur_time; } else { cur = time(nullptr); } std::cout << "User: " << user_id << ", time: " << cur << std::endl; } std::optional<int> get_user_from_login_name(const std::string &login_name) { std::map<std::string, int> map_login{{"login1", 1}, {"login2", 2}}; auto found = map_login.find(login_name); if (found != map_login.cend()) { return found->second; } return std::nullopt; } int main() { auto user = get_user_from_login_name("login1"); if (user) { write_login_log(*user); } return 0; }
請繼續關注個人公衆號文章