咱们在不少编程场合下都须要用到“可选”的概念,好比可选的参数,可选的返回值等。但对这一方面,传统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; }
请继续关注个人公众号文章