在编程中,我们经常遇到这样的情况:我们可能返回/传递/使用某种类型的对象。也就是说,我们可以有某个类型的值,也可以没有任何值。因此,我们需要一种方法来模拟类似指针的语义,在指针中,我们可以使用nullptr来表示没有值。
处理这个问题的方法是定义一个特定类型的对象,并用一个额外的布尔成员/标志来表示值是否存在。std::optional<>以一种类型安全的方式提供了这样的对象。

std::optional对象只是包含对象的内部内存加上一个布尔标志。因此,大小通常比包含的对象大一个字节。对于某些包含的类型,甚至可能根本没有大小开销,前提是附加信息可以放在包含的对象中。没有分配堆内存。对象使用与所包含类型相同的对齐方式。

然而,std::optional对象不仅仅是向值成员添加布尔标志功能的结构。例如,如果没有值,就不会为所包含的类型调用构造函数(因此,可以为对象提供没有值的默认状态)。

与std:: variable <>和std::any一样,生成的任何对象都具有值语义。也就是说,复制被实现为一个深度复制,创建一个独立的对象,该对象带有标记,如果在它自己的内存中包含值,则包含值。复制一个std::optional<>而不包含值是廉价的;
使用包含的值复制std::optional<>与复制包含的类型/值一样便宜/昂贵。支持Move语义。

1.1 使用std::optional<>

std::optional<>为任意类型的可空实例建模。实例可以是成员、参数或返回值。还可以认为std::optional<>是一个包含0或1个元素的容器。

1.1.1 std::optional<>返回值

下面的程序演示了std::optional<>作为返回值的处理:

#include <iostream>
#include <optional>
#include <string>// convert string to int if possible:
std::optional<int> asInt(const std::string& s)
{try {return std::stoi(s);}catch (...) {return std::nullopt;}
}
int main()
{for (auto s : { "42", " 077", "hello", "0x33" }) {// convert s to int and use the result if possible:std::optional<int> oi = asInt(s);if (oi) {std::cout << "convert '" << s << "' to int: " << *oi << "\n";}else {std::cout << "can't convert '" << s << "' to int\n";}}
}

在程序中,asInt()是一个函数,用于将传递的字符串转换为整数。然而,这可能不会成功。因此,使用std::optional<>,这样我们就可以返回“no int”,避免为它定义一个特殊的int值,或者向调用者抛出异常。因此,我们要么返回调用stoi()的结果,它用int初始化返回值,要么返回std::nullopt,这表明我们没有int值。

结果如下:

我们可以实现相同的行为如下:

std::optional<int> asInt(const std::string& s)
{std::optional<int> ret; // initially no valuetry {ret = std::stoi(s);}catch (...) {}return ret;
}

在main()中,我们用不同的字符串调用这个函数。

for (auto s : {"42", " 077", "hello", "0x33"} )
{// convert s to int and use the result if possible:std::optional<int> oi = asInt(s);...
}

对于每个返回的std::optional<int> oi,我们计算,是否有一个值(通过将对象计算为Boolean表达式),并通过“解引用”可选对象来访问该值:

if (oi)
{std::cout << "convert '" << s << "' to int: " << *oi << "\n";
}

注意,对于字符串“0x33”调用asInt()会产生0,因为stoi()不会将字符串解析为十六进制值。

还可以通过如下方法来处理返回值,例如:

std::optional<int> oi = asInt(s);
if (oi.has_value())
{std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
}

在这里has_value()用来检查是否有返回值,如果有通过value()来获取。value()比操作符*更安全,因为没有值而调用该接口的话会抛出异常。操作符*只有你确认有值的情况下才能使用,否则程序会出现未定义行为。

注意,可以通过使用新的类型std::string_view来改进asInt()。

1.1.2 std::optional<>参数和数据成员

使用std::optional<>的另一个例子是参数的std::optional传递和做为数据成员:

#include <iostream>
#include <string>
#include <optional>class Name
{
private:std::string first;std::optional<std::string> middle;std::string last;
public:Name(std::string f,std::optional<std::string> m,std::string l): first{ std::move(f) }, middle{ std::move(m) }, last{ std::move(l) } {}friend std::ostream& operator << (std::ostream& strm, const Name& n) {strm << n.first << ' ';if (n.middle) {strm << n.middle.value() << ' ';}return strm << n.last;}
};
int main()
{Name n{ "Jim", std::nullopt, "Knopf" };std::cout << n << '\n';Name m{ "Donald", "Ervin", "Knuth" };std::cout << m << '\n';return 0;
}

类Name表示由一个名字,可选的中间名,姓氏组成。成员中间被定义为std::optional<>,当没有中间名字时构造函数允许传递std::nullopt参数,这与中间名称为空字符串的状态不同。

注意,对于具有值语义的类型,定义初始化相应成员的构造函数的最佳方法是按值获取参数并将参数移动到成员。

还要注意,std::optional<>改变了对成员中间值的访问。使用middle作为布尔表达式可以判断是否有中间名,如果有的话可以使用value()方法来获取值。

另外一种访问optional值的方法是用一个成员函数value_or(),该方法如果不存在值,则允许指定的值。例如,在类名内部我们也可以实现:

std::cout << middle.value_or(""); // print middle name or nothing

1.2 std::optional<>类型和操作

本节详细描述std::optional<>的类型和操作。

1.2.1 std::optional<>类型

在标准库的头文件<optional>中定义了std::optional<>如下;

namespace std
{
template<typename T> class optional;
}

此外,定义了类型和对象如下:

  • std::nullopt_t类型的nullopt是一种没有值的对象;
  • 异常类std::bad_optional_access,派生自std:: Exception,用于std::optional<>对象没有值的情况下访问是抛出的异常。

可选对象还使用<utility>中定义的对象std::in_place(类型为std::in_place_t)来初始化带有多个参数的可选对象的值(参见下面)。

1.2.2 std::optional<>操作

下面std::optional操作列出了为std::optional<>提供的所有操作:

std::optional的成员定于
         成员定义                                           说明
constructors 创建一个optional对象(可能调用包含类型的构造函数)
make_optional<>() 创建一个optional对象(传递值来初始化它)
destructor 销毁一个optional对象
= 分配一个新值
emplace() 为所包含的类型分配一个新值
reset() 销毁任何值(使对象为空)
has_value() 返回对象是否具有值
conversion to bool 返回对象是否具有值
* 访问值(如果没有值,则未定义行为)
-> 访问值的成员(如果没有值,则为未定义的行为)
value() 访问值(如果没有值,则抛出异常)
value_or() 访问值(如果没有值,则使用提供的值)
swap() 交换两个对象的值
==, !=, < , <=, >, >= 比较两个optional对象
hash<> 函数对象类型来计算哈希值
  1. 构造函数

特殊构造函数允许将参数直接传递给所包含的类型。

  • 可以创建一个没有值的可选对象。在这种情况下,必须指定包含的类型:
std::optional<int> o1;
std::optional<int> o2(std::nullopt);

这里不会为所包含的类型调用任何构造函数。

  • 可以传递一个值来初始化所包含的类型。根据推导指南,不必指定所包含的类型,如下:
std::optional o3{42}; // deduces optional<int>
std::optional<std::string> o4{"hello"};
std::optional o5{"hello"}; // deduces optional<const char*>

要初始化一个具有多个参数的可选对象,必须创建该对象或将std::in_place添加为第一个参数(所包含的类型无法推断):

std::optional o6{std::complex{3.0, 4.0}};
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};

注意,第二种形式避免创建临时对象。通过使用这种形式,甚至可以传递初始化器列表和附加参数:

// initialize set with lambda as sorting criterion:
auto sc = [] (int x, int y)
{return std::abs(x) < std::abs(y);
};std::optional<std::set<int,decltype(sc)>> o8{std::in_place, {4, 8, -7, -2, 0, 5}, sc};
  • 可以复制可选对象(包括类型转换):
std::optional o5{"hello"}; // deduces optional<const char*>
std::optional<std::string> o9{o5}; // OK

注意,还有一个方便的函数make_optional<>(),它允许使用单个或多个参数初始化(不需要in_place参数)。像往常一样make……函数推导:

auto o10 = std::make_optional(3.0); // optional<double>
auto o11 = std::make_optional("hello"); // optional<const char*>
auto o12 = std::make_optional<std::complex<double>>(3.0, 4.0);

然而,注意,没有构造函数接受一个值并根据它的值来决定是使用值初始化一个可选值还是使用nullopt。可以使用操作符?:,例如:

std::multimap<std::string, std::string> englishToGerman;
...
auto pos = englishToGerman.find("wisdom");
auto o13 = pos != englishToGerman.end()
? std::optional{pos->second}
: std::nullopt;

在这里,o13初始化为std::optional<std::string>,这是由于类模板参数的推导std::optionalf(pos->second)。对于std::nullopt类模板参数推导不起作用,但是运算符?:在推导表达式的结果类型时也将其转换为这种类型。

2. 访问值

    检查optional对象是否有值,可以将对象当做一个布尔表达式使用它,或者调用has_value():

std::optional o{42};
if (o) ... // true
if (!o) ... // false
if (o.has_value()) ... // true

为了访问该值,提供了指针语法。操作符*可以直接访问它所包含类型对象的值,而操作符->允许访问它所包含类型对象的成员:

std::optional o{std::pair{42, "hello"}};
auto p = *o; // initializes p as pair<int,string>
std::cout << o->first; // prints 42

注意,这些操作符要求optional包含一个值。在没有值的情况下使用它们是未定义的行为:

std::optional<std::string> o{"hello"};
std::cout << *o; // OK: prints ”hello”
o = std::nullopt;
std::cout << *o; // undefined behavior

注意,实际上第二个输出仍然会编译并执行一些输出,比如再次打印“hello”,因为用于optional对象值的底层内存没有被修改。
然而,你不能也不应该依赖它。如果你不知道一个可选对象是否有值,你只能调用以下函数:

if (o) std::cout << *o; // OK (might output nothing)

或者,您可以使用value(),如果没有包含值,它会抛出一个std::bad_optional_access异常:

std::cout << o.value(); // OK (throws if no value)

bad_optional_access直接派生自std::exception。

最后,如果optional对象没有值,您可以请求该值并传递回退值,回退值将被使用:

std::cout << o.value_or("fallback"); // OK (outputs fallback if no value)

回退参数作为rvalue引用传递,因此,如果不使用回退,则不需要花费任何代价,如果使用回退,则支持move语义。

3 比较操作

可以使用通常的比较操作符。操作数可以是optional对象、包含类型的对象和std::nullopt。

  • 如果两个操作数都是具有值的对象,则使用所包含类型的对应操作符。
  • 如果两个操作数都是没有值的对象,则认为它们是相等的(==产生true,所有其他比较产生false)。
  • 如果只有一个操作数是有值的对象,则没有值的操作数被认为小于另一个操作数。

例如:

std::optional<int> o0;
std::optional<int> o1{42};
o0 == std::nullopt // yields true
o0 == 42 // yields false
o0 < 42 // yields true
o0 > 42 // yields false
o1 == 42 // yields true
o0 < o1 // yields true

这意味着对于无符号整型的optional对象有一个小于0的值,对于bool的optional对象有一个小于0的值:

std::optional<unsigned> uo;
uo < 0 // yields true
std::optional<bool> bo;
bo < false // yields true

同样,支持底层类型的隐式类型转换:

std::optional<int> o1{42};
std::optional<double> o2{42.0};o2 == 42 // yields true
o1 == o2 // yields true

注意,optional的bool值或原始指针值可能会导致一些意外。

4 修改值

通过赋值和emplace来修改值:

std::optional<std::complex<double>> o; // has no value
std::optional ox{77}; // optional<int> with value 77
o = 42; // value becomes complex(42.0, 0.0)
o = {9.9, 4.4}; // value becomes complex(9.9, 4.4)
o = ox; // OK, because int converts to complex<double>
o = std::nullopt; // o no longer has a value
o.emplace(5.5, 7.7); // value becomes complex(5.5, 7.7)

赋值std::nullopt将删除该值,如果之前有值,该值将调用所包含类型的析构函数。也可以通过调用reset()获得相同的效果:

o.reset(); // o no longer has a value

或赋值为空花括号:

o = {}; // o no longer has a value

最后,我们还可以使用操作符*修改值,因为它通过引用生成值。但是,请注意,这需要修改一个有存在的值:

std::optional<std::complex<double>> o;
*o = 42; // undefined behavior
...
if (o)
{*o = 88; // OK: value becomes complex(88.0, 0.0)*o = {1.2, 3.4}; // OK: value becomes complex(1.2, 3.4)
}

5 移动语义

std::optional<>也支持移动语义。如果将对象作为一个整体移动,则会复制状态并移动所包含的对象(如果有的话)。因此,一个从对象中移出的对象仍然具有相同的状态,但是移动后的值会变成是未指定的。但是也可以将一个值移动到或移出所包含的对象。例如:

std::optional<std::string> os;
std::string s = "a very very very long string";
os = std::move(s); // OK, moves
std::string s2 = *os; // OK copies
std::string s3 = std::move(*os); // OK, moves

注意,在最后一次调用之后os仍然有一个字符串值,但是对于os对象这个值未指定的。因此,不能对移动后的对象的值做任何假设就去使用它。当然,可以给移动后的对象分配一个新的值。

在visual studio上调试,os.have_value返回的是ture,只不过string为空了;

6 Hashing

std::optional对象的哈希值是包含的非常量类型(如果有的话)的哈希值。

1.3 特定情况

特定的可选值类型可能导致特殊或意外的行为。

1.3.1 bool或者原始指针的optional

注意,使用比较操作符与使用optional对象作为布尔值具有不同的语义。如果包含的类型是bool或指针类型,这就会变得很混乱:例如:

std::optional<bool> ob{false}; // has value, which is false
if (!ob) ... // yields false  -->判断ob是否包含bool值
if (ob == false) ... // yields true   -->判断ob包含的bool值是否为false
std::optional<int*> op{nullptr};
if (!op) ... // yields false    -->判断ob是否包含指针值
if (op == nullptr) ... // yields true  -->判断ob包含的指针值是否等于nullptr

1.3.2 Optional的Optional

原则上,你也可以定义一个optional的optional值:

std::optional<std::optional<std::string>> oos1;
std::optional<std::optional<std::string>> oos2 = "hello";
std::optional<std::optional<std::string>> oos3{std::in_place, std::in_place, "hello"};
std::optional<std::optional<std::complex<double>>> ooc{std::in_place, std::in_place, 4.2, 5.3};

你也可以分配新的值,即使隐式转换:

oos1 = "hello"; // OK: assign new value
ooc.emplace(std::in_place, 7.2, 8.3);

由于optional的无值有两个层次,optional的optional使“无值”出现在外部或内部,可以有不同的语义含义:

*oos1 = std::nullopt; // inner optional has no value
oos1 = std::nullopt; // outer optional has no value

但必须特别注意处理optional值:

if (!oos1) std::cout << "no value\n";
if (oos1 && !*oos1) std::cout << "no inner value\n";
if (oos1 && *oos1) std::cout << "value: " << **oos1 << '\n';

C++17之std::optional相关推荐

  1. std::optional

    c++17之后引入optional,其实就是把boost库里面实现机制引入进来.作用:良好地处理构造开销高昂的对象,并更加可读.有点像指针的用法.个人认为,没啥鸟用,和用指针没啥区别 例子: #inc ...

  2. C++ std::any、std::variant和std::optional的原位构造(In-Place Construction)

    本文翻译自 Bartlomiej Filipek 的博客文章 In-Place Construction for std::any, std::variant and std::optional,翻译 ...

  3. C++17之std::variant

    从C中采用的c++提供了对union的支持,union是能够保存可能类型列表之一的对象.但是,这种语言特性也有一些缺点: 对象不知道它们当前持有的值的类型. 由于这个原因,您不能有non-trivia ...

  4. 【C++ 泛型编程 高级篇】 C++ 17 解析std::apply 的多种应用场景

    目录标题 1. 引言 1.1. C++17标准的引入 1.2. std::apply的基本概念 2. std::apply的基本用法 2.1. std::apply的函数签名 2.2. std::ap ...

  5. C++17类型std::variant介绍

    std::variant代表了类型安全的union类型,与union类似,std::variant的一个实例要么存储了可选类型中的一个值,要么没有存储.但与union比,其优势是可以判断当前真实的存储 ...

  6. C++17之std::visit

    它们必须明确地为每种可能的类型提供函数调用操作符.然后,使用相应的重载来处理当前的备选项类型. 1. 使用对象函数方式访问 例1: #include <iostream> #include ...

  7. std::expected以及其开源实现

    std::expected以及其开源实现 背景 std::expected和std::optional的区别 std::expected的接口介绍 基本接口 Monad接口 std::expected ...

  8. 2020-11-17 1)C标准库头文件 2)C ++标准库标头

    1.C标准库头文件   https://en.cppreference.com/w/c/header   C标准库的接口由以下标头集合定义. <assert.h> 有条件编译的宏,将其参数 ...

  9. std::future ---C++17 多线程

    std::future -C++17 多线程 std::future C++标准程序库使用future来模拟这类一次性事件:若线程需等待某个特定的一次性事件发生,则会以恰当的方式取得一个future, ...

最新文章

  1. hadoop2 作业执行过程之作业提交
  2. mysql mysqld_multi 单机多进程
  3. SQL 必知必会·笔记5创建计算字段
  4. mysql之 mysql 5.6不停机主主搭建(活跃双主基于日志点复制)
  5. python 搭建web应用程序_用Python构建数据科学Web应用程序
  6. cmd无法输出java结果_cmd中执行java命令没有输出结果
  7. 深度神经网络中处理数据和训练模型的一些技巧
  8. 怎样恢复计算机管理员身份,电脑高手必备:2招教你找回Windows管理员权限?
  9. linux添加删除回环地址,CentOS7如何添加本地回环地址?CentOS7添加本地回环地址的方法...
  10. linux vps 运行exe文件夹,在centos环境下运行.exe文件
  11. k-近邻算法-优化约会网站的配对效果
  12. python集成开发环境运行快捷键_Python初学者选择集成开发环境必看 python开发
  13. java 单例 初始化_单例数据库对象启动时参数化初始化?
  14. Java服务端获取微信小程序openid(简单实现,搞懂原理)
  15. 如何进行windows数据恢复呢
  16. 马克思主义基本原理知识框架图——政治经济学中各种资本的划分
  17. 什么是外包公司,外包公司与互联网公司的区别
  18. 链塔小程序产品更新说明
  19. vue-cli3.0以上 + typeScript 教程学习指导(一) 入门typeScript
  20. linux安装firefox

热门文章

  1. sja1000 中断_SJA1000的错误中断处理
  2. matlab图片白边_matlab绘图白边设定
  3. 3月18日云栖精选夜读 | 开发者必看!探秘阿里云Hi购季开发者分会场:海量学习资源0元起!...
  4. CSharp 创建项目
  5. C++ L1-034. 点赞
  6. 5y平台计算机应用测试题,2016电大网考计算机应用基础统考试题模拟真题及答案 含小抄复习资料.docx...
  7. 基础电子元器件介绍-4.二极管
  8. 涠洲岛日出日落时间表,1月10日涠洲岛开船时间/日出日落/天气预报
  9. 让我摘下星星送给你_抖音想摘下星星给你摘下月亮给你是什么歌? 《星球坠落》让你忍不住单曲循环...
  10. 个人号的微信API接口,微信机器人二次开发