std::optional

  1. 背景

    在编程时,我们经常会遇到可能会返回/传递/使用一个确定类型对象的场景。也就是说,这个对象可能有一个确定类型的值也可能没有任何值。因此,我们需要一种方法来模拟类似指针的语义:指针可以通过 nullptr来表示没有值。解决方法是定义该对象的同时再定义一个附加的 bool类型的值作为标志来表示该对象是否有值。std::optional<>提供了一种类型安全的方式来实现这种对象。
  2. 占用内存大小

    可选对象所需的内存等于内含对象的大小加上一个 bool类型的大小。因此,可选对象一般比内含对象大一个字节(可能还要加上内存对齐的空间开销)。可选对象不需要分配堆内存,并且对齐方式和内含对象相同。
#include <iostream>
#include <optional> // 定义一个没有默认构造函数的类
class MyClass {
public:
explicit MyClass(int value) : data(value) {}
~MyClass() {} int getData() const {
return data;
} private:
int data;
}; // 输出 std::optional 是否包含值
void check_optional_value(std::optional<MyClass>& opt) {
if (opt) {
std::cout << "Value present: " << opt->getData() << std::endl;
} else {
std::cout << "No value present." << std::endl;
}
} int main() {
// 创建一个没有值的 std::optional<MyClass>
std::optional<MyClass> opt1;
check_optional_value(opt1); // 创建一个有值的 std::optional<MyClass>
std::optional<MyClass> opt2{MyClass(42)};
check_optional_value(opt2); // 尝试通过 emplace 添加值
opt1.emplace(24);
check_optional_value(opt1); // 尝试通过 operator= 添加值
opt1 = MyClass(56);
check_optional_value(opt1); return 0;
}

输出:

Size of i: 4 bytes

Size of St8optionalIiE: 8 bytes

Size of 7MyClass: 4 bytes

Size of St8optionalI7MyClassE: 8 bytes

然而,可选对象并不是简单的等价于附加了bool标志的内含对象。例如,在没有值的情况下,将不会调用内含对象的构造函数(通过这种方式,没有默认构造函数的内含类型也可以处于有效的默认状态)。

3.语义

和 std::variant<>、std::any一样,可选对象有值语义。也就是说,拷贝操作会被实现为深拷贝:将创建一个新的独立对象,新对象在自己的内存空间内拥有原对象的标记和内含值(如果有的话)的拷贝。拷贝一个无内含值的 std::optional<>的开销很小,但拷贝有内含值的 std::optional<>的开销约等于拷贝内含值的开销。另外,std::optional<>对象也支持 move语义。

4.应用

(1)std::optional<>模拟了一个可以为空的任意类型的实例。它可以被用作成员、参数、返回值等。

下面的示例程序展示了将 std::optional<>用作返回值的一些功能:

#include <optional>
#include <string>
#include <iostream> // 如果可能的话把string转换为int:
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"}) {
// 尝试把s转换为int,并打印结果:
std::optional<int> oi = asInt(s);
if (oi.has_value()) {
std::cout << "convert '" << s << "' to int: " << oi.value() << "\n";
}
else {
std::cout << "can't convert '" << s << "' to int\n";
}
}
}

(2) 另一个使用 std::optional<>的例子是传递可选的参数和设置可选的数据成员:

#include <optional>
#include <string>
#include <iostream> 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 << ' ';
}
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';
}

5.std::optional<>类型和操作

(1)std::optional<>类型标准库在头文件 中以如下方式定义了 std::optional<>类:

namespace std {

template class optional;

}

另外还定义了下面这些类型和对象:

• std::nullopt_t类型的 std::nullopt,作为可选对象无值时候的“值”。

• 从 std::exception派生的 std::bad_optional_access异常类,当无值时候访问值将会抛出该异常。

可选对象还使用了 头文件中定义的 std::in_place对象(类型是 std::in_place_t)来支持用多个参数初始化可选对象(见下文)。

(2)std::optional<>的操作

表std::optional的操作列出了 std::optional<>的所有操作:

#include <iostream>
#include <optional>
#include <variant>
#include <vector>
#include <set>
#include <map>
#include <string>
#include <cmath>
#include <functional>
#include <cassert>
#include <complex> // 使用命名空间简化代码
using namespace std::string_literals; // 示例 1:构造 std::optional
void construct_optional() {
std::optional<int> o1; // 不含有值
assert(!o1.has_value()); std::optional<int> o2(std::nullopt); // 显式表示不含有值
assert(!o2.has_value()); std::optional o3{42}; // 推导出 std::optional<int>
assert(o3.has_value());
assert(*o3 == 42); std::optional o4{"hello"}; // 推导出 std::optional<const char*>
assert(o4.has_value());
assert(*o4 == "hello"); std::optional o5{"hello"s}; // 推导出 std::optional<std::string>
assert(o5.has_value());
assert(*o5 == "hello"); // 用多个参数初始化可选对象
std::optional<std::complex<double>> o6{std::in_place, 3.0, 4.0};
assert(o6.has_value());
assert(o6->real() == 3.0 && o6->imag() == 4.0); // 使用 std::make_optional
auto o13 = std::make_optional(3.0); // std::optional<double>
assert(o13.has_value());
assert(*o13 == 3.0); auto o14 = std::make_optional("hello"); // std::optional<const char*>
assert(o14.has_value());
assert(*o14 == "hello"); auto o15 = std::make_optional<std::complex<double>>(3.0, 4.0);
assert(o15.has_value());
assert(o15->real() == 3.0 && o15->imag() == 4.0);
} // 示例 2:访问值
void access_optional_value() {
std::optional<std::pair<int, std::string>> o{std::make_pair(42, "hello")};
assert(o.has_value());
assert(o->first == 42);
assert(o->second == "hello"); std::optional<std::string> o2{"hello"};
assert(o2.has_value());
assert(*o2 == "hello"); // 当没有值时访问会导致未定义行为
o2 = std::nullopt;
assert(!o2.has_value());
// std::cout << *o2 << std::endl; // 未定义行为
} // 示例 3:使用 value_or
void use_value_or() {
std::optional<std::string> o{"hello"};
std::cout << o.value_or("NO VALUE") << std::endl; // 输出 "hello" o = std::nullopt;
std::cout << o.value_or("NO VALUE") << std::endl; // 输出 "NO VALUE"
} // 示例 4:比较
void compare_optionals() {
std::optional<int> o0;
std::optional<int> o1{42};
assert(o0 == std::nullopt);
assert(!(o0 == 42));
assert(o0 < 42);
assert(!(o0 > 42));
assert(o1 == 42);
assert(o0 < o1);
assert(!(o0 > o1)); std::optional<unsigned> uo;
assert(uo < 0);
assert(uo < -42); std::optional<bool> bo;
assert(bo < false); std::optional<int> o2{42};
std::optional<double> o3{42.0};
assert(o2 == 42);
assert(o3 == 42);
assert(o2 == o3);
} // 示例 5:修改值
void modify_optional_value() {
std::optional<std::complex<double>> o; // 没有值
std::optional<int> ox{77}; // optional<int>,值为77
o = 42; // 值变为 complex(42.0, 0.0)
assert(o.has_value());
assert(o->real() == 42.0 && o->imag() == 0.0); o = std::complex<double>{9.9, 4.4}; // 值变为 complex(9.9, 4.4)
assert(o.has_value());
assert(o->real() == 9.9 && o->imag() == 4.4); o = ox; // OK,因为 int 转换为 complex<double>
assert(o.has_value());
assert(o->real() == 77.0 && o->imag() == 0.0); o = std::nullopt; // o 不再有值
assert(!o.has_value()); o.emplace(5.5, 7.7); // 值变为 complex(5.5, 7.7)
assert(o.has_value());
assert(o->real() == 5.5 && o->imag() == 7.7); o.reset(); // o 不再有值
assert(!o.has_value()); o = std::complex<double>{88.0, 0.0}; // OK:值变为 complex(88.0, 0.0)
assert(o.has_value());
assert(o->real() == 88.0 && o->imag() == 0.0); o = std::complex<double>{1.2, 3.4}; // OK:值变为 complex(1.2, 3.4)
assert(o.has_value());
assert(o->real() == 1.2 && o->imag() == 3.4);
} // 示例 6:使用 lambda 初始化 set
void initialize_set_with_lambda() {
auto sc = [](int x, int y) {
return std::abs(x) < std::abs(y);
}; std::optional<std::set<int, decltype(sc)>> o8{std::in_place,
std::initializer_list<int>{4, 8, -7, -2, 0, 5},
sc};
assert(o8.has_value());
assert(o8->size() == 6);
} int main() {
construct_optional();
access_optional_value();
use_value_or();
compare_optionals();
modify_optional_value();
initialize_set_with_lambda();
return 0;
}

6.注意

(1)value()和 value_or()

value()和 value_or()之间有一个需要考虑的差异:4 value_or()返回值,而 value()返回引用。这意味着如下调用:

std::cout << middle.value_or("");

和:

std::cout << o.value_or("fallback");

都会暗中分配内存,而 value()永远不会。

然而,当在临时对象 (rvalue)上调用 value_or()时,将会移动走内含对象的值并以值返回,而不是调用拷贝函数构造。这是唯一一种能让 value_or()适用于 move-only的类型的方法,因为在左值 (lvalue)上调用的 value_or()的重载版本需要内含对象可以拷贝。

因此,上面例子中效率最高的实现方式是:

std::cout << o ? o‐>c_str() : "fallback";

而不是:

std::cout << o.value_or("fallback");

value_or()是一个能够更清晰地表达意图的接口,但开销可能会更大一点。

(2)bool 类型或原生指针的可选对象

将可选对象用作 bool值时使用比较运算符会有特殊的语义。如果内含类型是 bool或者指针类型的话这可能导致令人迷惑的行为。例如:

std::optional ob{false}; // 值 为false

if (!ob) ... // 返 回false

if (ob == false) ... // 返 回true

std::optional<int*> op{nullptr};

if (!op) ... // 返 回false

if (op == nullptr) ... // 返 回true

C++17新特性探索:拥抱std::optional,让代码更优雅、更安全的更多相关文章

  1. c++17 新特性

    编译环境说明:gcc 8.1 + eclipse +windows 10 eclipse cpp默认支持c++14,做c++17开发时,需要手动进行配置. 1.关键字 1)constexpr c++1 ...

  2. Java 17 新特性:switch的模式匹配(Preview)

    还记得Java 16中的instanceof增强吗? 通过下面这个例子再回忆一下: Map<String, Object> data = new HashMap<>(); da ...

  3. C++17 新特性之 std::optional(上)

    最近在学习 c++ 17 的一些新特性,为了加强记忆和理解,把这些内容作为笔记记录下来,有理解不对的地方请指正,欢迎大家留言交流. 引言 在介绍之前,我们从一个问题出发,C++ 的函数如何返回多个值? ...

  4. C++17新特性optional和string_view

    1. optional的作用 类模板 std::optional 管理一个可选的容纳值,即可以存在也可以不存在的值. 一种常见的 optional 使用情况是一个可能失败的函数的返回值.与其他手段,如 ...

  5. Microsoft Dynamics AX 7 新特性探索 - Demo 部署(Part 1)

    Dynamics AX 7已经发布了一段时间了,我们知道这次微软为我们带来了许多令人激动的新特性.在这个系列里,Reinhard将揭开New Dynamics AX的神秘面纱,和大家一起探索这些新的特 ...

  6. C++11新特性,利用std::chrono精简传统获取系统时间的方法

    一.传统的获取系统时间的方法 传统的C++获取时间的方法须要分平台来定义. 相信百度代码也不少. 我自己写了下,例如以下. const std::string getCurrentSystemTime ...

  7. C++11 & C++14 & C++17新特性

    C++11:C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto.decltype,和模板的大量改进. 新的关键字 auto C++11中引入auto第一种作用是为了自动类型推导 ...

  8. 【Java8新特性】不了解Optional类,简历上别说你懂Java8!!

    写在前面 最近,很多读者出去面试都在Java8上栽了跟头,事后自己分析,确实对Java8的新特性一知半解.然而,却在简历显眼的技能部分写着:熟练掌握Java8的各种新特性,能够迅速使用Java8开发高 ...

  9. 从Java 9 到 Java 17 新特性梳理

    Java 9 新的创建集合的方法  // [1, 2, 3, 4]  List<Integer> integers = List.of(1, 2, 3, 4);  // {1,2,3}   ...

  10. 转:【Java并发编程】之二十二:并发新特性—障碍器CyclicBarrier(含代码)

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/17512983 CyclicBarrier(又叫障碍器)同样是Java5中加入的新特性,使用 ...

随机推荐

  1. Redis巡检检查 redis-check-aof

    一.AOF1.AOF  是什么以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,R ...

  2. 2行JS代码实现Vue全选和反选

    实现效果: 第一行:子选项的选中状态 等于 全选框的状态 this.letters.forEach(item => item.check = this.checkAll) 第二行: 使用数组 e ...

  3. django 设置外键的时候,related_name的值大写还是小写,规则怎样

    django 设置外键的时候,related_name的值大写还是小写,规则怎样 在Django中,related_name参数用于定义反向关系的名称,即通过外键字段反向查询关联模型的对象.relat ...

  4. Node.js 处理 File

    Node.js 处理 File fs 模块 常规使用 运用递归遍历目录树 创建文件和目录 读写文件 path 模块 对于 file 的理解,此处 fs 模块 Node.js 提供了处理文件系统的内置模 ...

  5. 新做了一个MySQL 数据库 DDL 差异对比的网站

    MySQL 数据库 DDL 差异对比的网站 摘要 新做了个网站,用来对比不同环境下的 DDL 差异,生成变更点和 迁移 DDL 网站地址:https://ddlcompare.com/ 对比过程中如果 ...

  6. rtmp流程解析

    如果rtmp推流地址:rtmp://服务器地址:rtmp端口/路径/名称对应的websocket地址:ws://服务器地址:websocket端口/路径/名称.flv举例:live作为路径,s作为流名 ...

  7. selenium高亮显示定位到的页面元素

    from selenium import webdriver import unittest,time def highLightElement(driver,element): #封装好的高亮显示页 ...

  8. 【Java】Input,Output,Stream I/O流 01 概述 & 4个基础流

    Input,Output,Stream IO流 I/O Input/Output缩写.I/O技术用于处理设备之间的数据传输,读写文件,网络通讯 Java程序对于数据的操作以Stream流的形式进行 i ...

  9. 图解Java设计模式

    待补充 设计模式介绍 设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案. 设计模式使用的位置 功能模块+框架上. 设计模式在软件中哪里?面向对象( ...

  10. 在使用pytorch官方给出的torchvision中的预训练模型参数时为保证收敛性要求使用原始的数据预处理方式

    本文主要内容如题: 在使用pytorch官方给出的torchvision中的预训练模型参数时为保证收敛性要求使用原始的数据预处理方式 具体的pytorch官方讨论: https://github.co ...