【现代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;
}

请继续关注个人公众号文章
图片描述

相关文章
相关标签/搜索