【現代C++】"可選"在C++中的表達--std::optional<>

背景

咱們在不少編程場合下都須要用到「可選」的概念,好比可選的參數,可選的返回值等。但對這一方面,傳統C/C++支持得略顯不足。下面經過幾個實例說明這一問題。python

二分查找

在二分查找算法中,有可能咱們要查找的值不在集合裏,這時咱們該怎麼表示呢?二分算法在前面的文章中有提供,給出了PythonHaskell版本: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參數默認取一個特定的值,在這裏分別取0std::string::nposcode

然而在函數類型中,參數的類型仍然是size_type,並無給調用者提供多少有用的信息。在其餘語言中,這方面作的要相對更好,好比Haskell中,咱們仍然可使用Maybe T類型做爲函數的參數,一目瞭然就能夠看出這個參數須要處理可選狀況。圖片

下面咱們討論傳統方式都有哪些缺點。內存

傳統方式的缺點

從以上兩個應用實例可看出,傳統方式實際上就是經過特定的值表示「可選」的概念。這種方式有什麼缺點呢?

  1. 從數據類型沒法看出可選語義

輸入參數經過默認參數機制實現,相對來講還能看出點信息;但返回值可選的狀況,咱們徹底從函數簽名裏看不出來一點信息,只能經過API文檔得知。

  1. 可選慣例不統一,規格多樣

按照慣例,經過找不到的狀況,都會使用-1nullptr等無心義的值;但慣例對編譯器是沒有約束力的,只能人爲遵照,因此頗有可能某些函數沒有按照慣例來,最後致使的:不一樣的庫慣例不一致,甚至同一個庫不一樣人寫的函數使用的慣例也不一致,千差萬別,會提升使用的成本。固然,標準庫是比較統一的,但這只是暫時掩蓋了問題,而沒有根除問題發生的緣由。

輸入參數取值更加不統一,有些人喜歡使用有效的參數值做爲默認參數,像find函數那樣;有些人喜歡使用無效值做爲默認參數,像find_last_of同樣。使用有效值的優勢是有助於理解,但某些狀況下沒法使用有效值,好比find_last_of的狀況,由於字符串的大小是無法靜態知道的。使用無效值避免了有效值的問題,但引起其餘問題:偏函數的時候能夠找到無效值,但全函數對於全部的參數都是有效的,這怎麼找?

因此,因爲以上缺點,C++終於在C++17引入了std::optional<>工具。

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>提供了類型Tstd::optional<T>的賦值轉換,咱們能夠直接返回T類型的值。

處理std::optional<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_namewrite_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;
}

請繼續關注個人公衆號文章
圖片描述

相關文章
相關標籤/搜索