CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • C++17 详解 说明
  • 第一部分——语言特性
  • 1. 快速入门
  • 2. 移除或修正的语言特性
  • 3. 语言澄清(Language Clarification)
  • 4. 通用语言特性
  • 5. 模板(Templates)
  • 6. 代码标注
  • 第二部分 - 标准库的变化
  • 7. std::optional
    • 7. std::optional
      • 引言
      • 使用场景
      • 基本示例
      • std::optional的创建
      • 就地构造
      • 返回std::optional
      • 返回值时使用大括号要小心
      • 访问存储的值
      • std::optional的操作
      • 修改值和对象生命周期
      • 比较操作
      • 性能和内存考量
      • 从boost::optional迁移
      • 特殊情况:optional<bool>和optional<T*>
      • std::optional的示例
      • 带有可选昵称和年龄的用户名
      • 从命令行解析整数
      • 其他示例
      • 总结
      • 编译器支持
  • 8. std::variant
  • 9. std::any
  • 10. std::string_view
  • 11. 字符串转换
  • 12. 搜索器与字符串匹配
  • 13. 文件系统
  • 14. 并行STL算法
  • 15. 标准库中的其他变化
  • 16. 移除和弃用的库特性
  • 第三部分 - 更多示例和用例
  • 17. 使用std::optional和std::variant进行重构
  • 18. 使用[[nodiscard]]强制执行代码契约
  • 19. 用if constexpr替换enable_if——带可变参数的工厂函数
  • 20. 如何实现CSV读取器的并行化
目录

7. std::optional

# 7. std::optional

C++17添加了一些包装类型,让编写更具表现力的代码成为可能。在本章中,你将了解std::optional,它用于表示可空类型。借助这个工具,你的对象可以轻松表明它们没有任何值。这种行为比使用一些特殊值(如-1、null)来实现要直观得多。

在本章中,你将学习:

  • 为什么需要可空类型;
  • std::optional如何工作以及它的作用;
  • 对std::optional的操作;
  • 使用该类型的性能开销;
  • 示例用例。

# 引言

如何标记一个类型不包含任何值呢?

一种方法是通过使用特殊值(-1、无穷大、nullptr)来实现“可空性”。在使用前,需要将对象与预定义值进行比较,以查看它是否为空。这种模式在编程中很常见。例如,string::find返回一个表示位置的值,当找不到模式时返回npos,这里npos就相当于“空值” 。

另外,你可以尝试使用std::unique_ptr<Type>,并将空指针视为未初始化。这种方法可行,但需要为对象分配内存,并不是推荐的做法。

另一种技术是构建一个包装器,为其他类型添加一个布尔标志。这样的包装器可以快速判断对象的状态。简而言之,std::optional就是这样工作的。

源自函数式编程领域的可空类型带来了类型安全性和表达能力。大多数其他语言都有类似的类型:例如Rust中的std::option、Java中的Optional<T>、Haskell中的Data.Maybe 。

std::optional在C++17中被添加,它借鉴了多年来一直可用的boost::optional的许多经验。在C++17中,你只需#include <optional>,就可以使用这个类型。

此外,std::optional在库基础技术规范(Library Fundamentals TS)中也已存在,所以你的C++14编译器有可能在<experimental/optional>头文件中支持它。

std::optional仍然是一个值类型(因此可以通过深拷贝进行复制)。此外,std::optional无需在自由存储区分配任何内存。

std::optional与std::any、std::variant和std::string_view一样,是C++的标准库类型。

# 使用场景

通常可以在以下场景中使用可选包装器:

  1. 如果你想表示一个可空类型。
    • 而不是使用特殊值(如-1、nullptr、NO_VALUE等)。
    • 例如,用户的中间名是可选的。你可能认为空字符串在这里可行,但了解用户是否输入了内容可能很重要。std::optional<std::string>可以提供更多信息。
  2. 返回某些计算(处理)的结果,当计算未能产生值且并非错误时。
    • 例如,在字典中查找元素:如果某个键下没有元素,这不是错误,但我们需要处理这种情况。
  3. 实现资源的延迟加载。
    • 例如,如果资源类型的构造开销很大,或者没有默认构造函数,你可以将其定义为std::optional<Resource>。以这种形式,你可以在系统中传递它,然后在应用程序首次访问时对其进行初始化(加载资源)。
  4. 向函数传递可选参数。

boost.optional的文档中有关于何时应使用该类型的有用总结,见《何时使用Optional (opens new window)》:

建议在存在唯一一个各方都清楚的没有T类型值的原因,并且没有值与拥有任何常规T类型值一样自然的情况下,使用optional<T>。

虽然有时是否使用optional的决定可能并不明确,但当值为空且这是程序的正常状态时,它最为适用。

# 基本示例

下面是一个关于optional的简单示例:

// UI类...
std::optional<std::string> UI::FindUserNick() {
    if (IsNickAvailable())
        return mStrNickName; // 返回一个字符串
    return std::nullopt; // 等同于return { };
}

// 使用:
std::optional<std::string> UserNick = UI->FindUserNick();
if (UserNick)
    Show(*UserNick);
1
2
3
4
5
6
7
8
9
10
11

在上述代码中,我们定义了一个函数,它返回一个包含字符串的optional。如果用户的昵称可用,它将返回该字符串;否则,返回nullopt。之后,我们可以将其赋值给一个optional,并通过将其转换为bool来检查它是否包含值。optional定义了operator*,因此我们可以轻松访问存储的值。

在接下来的部分,你将了解如何创建std::optional、对其进行操作、传递它,甚至会介绍你可能需要考虑的性能开销。

# std::optional的创建

创建std::optional有几种方式:

  • 初始化为空;
  • 直接用值初始化;
  • 使用推导指南用值初始化;
  • 使用make_optional;
  • 使用std::in_place;
  • 从其他optional创建。

如下代码所示:

// 空初始化:
std::optional<int> oEmpty;
std::optional<float> oFloat = std::nullopt;

// 直接初始化:
std::optional<int> oInt(10);
std::optional oIntDeduced(10); // 推导指南

// make_optional
auto oDouble = std::make_optional(3.0);
auto oComplex = std::make_optional<std::complex<double>>(3.0, 4.0);

// in_place
std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0};
// 用{1, 2, 3}直接初始化vector
std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3});

// 从其他optional复制:
auto oIntCopy = oInt;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

从上述代码示例中可以看出,创建optional有很大的灵活性。对于基本类型来说很简单,这种简单性甚至扩展到了复杂类型。

如果你希望完全控制创建过程和效率,了解in_place辅助类型也很有帮助。

# 就地构造

std::optional是一种包装类型,因此你几乎可以用与创建被包装对象相同的方式来创建optional对象。在大多数情况下确实如此:

std::optional<std::string> ostr{"Hello World"};
std::optional<int> oi{10};
1
2

你也可以不写构造函数来编写上述代码:

std::optional<std::string> ostr{std::string{"Hello World"}};
std::optional<int> oi{int {10}};
1
2

因为std::optional有一个构造函数,它接受U&&(对一种类型的通用引用,该类型可转换为optional中存储的类型)。在我们的例子中,它被识别为const char*,而字符串可以由此初始化。

那么在std::optional中使用std::in_place_t有什么好处呢?至少有两个关键原因:

  • 默认构造函数
  • 对有多个参数的构造函数进行高效构造
  • 默认构造:如果你有一个带有默认构造函数的类,例如:
class UserName {
public:
    UserName() : mName("Default") {
    }
    // ...
};
1
2
3
4
5
6

你要如何创建一个包含UserName{}的optional对象呢?你可以这样写:

std::optional<UserName> u0;   // 空的optional
std::optional<UserName> u1{}; // 也是空的
// 包含默认构造对象的optional:
std::optional<UserName> u2{UserName()};
1
2
3
4

这样做是可行的,但它会创建一个额外的临时对象。如果我们追踪每个不同的构造函数和析构函数调用,会得到以下输出:

UserName::UserName('Default')
UserName::UserName(move 'Default')  // 移动临时对象
UserName::~UserName('')             // 删除临时对象
UserName::~UserName('Default')
1
2
3
4

这段代码创建了一个临时对象,然后将其移动到optional中存储的对象里。在这里,我们可以使用一种更高效的构造函数——利用std::in_place_t:

std::optional<UserName> opt{std::in_place};
1

追踪构造函数和析构函数时,你会得到以下输出:

UserName::UserName('Default')
UserName::~UserName('Default')
1
2

optional中存储的对象是就地创建的,就像你调用UserName{}一样。不需要额外的复制或移动操作。

查看Optional/optional_in_place_default.cpp中的示例。在该文件中,你还可以看到构造函数和析构函数的追踪信息。

  • 不可复制/不可移动类型:正如你在上一节的示例中看到的,如果你使用一个临时对象来初始化std::optional中包含的值,那么编译器将不得不使用移动或复制构造函数。但如果你的类型不允许这样做呢?例如,std::mutex是不可移动也不可复制的。在这种情况下,std::in_place是处理这类类型的唯一方法。
  • 有多个参数的构造函数:另一个用例是当你的类型在构造函数中有更多参数时。默认情况下,optional可以处理单个参数(右值引用),并有效地将其传递给被包装的类型。但如果你想初始化Point(x, y)呢?你总是可以创建一个临时副本,然后在构造时传递它:
// Optional/optional_point.cpp
struct Point {
    Point(int a, int b) : x(a), y(b) {}
    int x;
    int y;
};
std::optional<Point> opt{Point{0, 0}}; // 创建了临时对象!
1
2
3
4
5
6
7

或者使用in_place以及处理可变参数列表的构造函数版本:

template < class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args );

// 或者初始化列表:
template < class U, class... Args >
constexpr explicit optional( std::in_place_t,
                             std::initializer_list<U> ilist,
                             Args&&... args );
1
2
3
4
5
6
7
8

你的代码可以这样写:

std::optional<Point> opt{std::in_place_t, 0, 0};
1

第二种选择虽然冗长,但避免了创建临时对象。对于容器或较大的对象来说,临时对象的效率不如就地构造。

  • std::make_optional():如果你不喜欢std::in_place,那么可以看看make_optional工厂函数。下面的代码:
auto opt = std::make_optional<UserName>();
auto opt = std::make_optional<Point>(0, 0);
1
2

与下面的代码效率相同:

std::optional<UserName> opt{std::in_place};
std::optional<Point> opt{std::in_place_t, 0, 0};
1
2

std::make_optional实现了等同于return std::optional<T>(std::in_place, std::forward<Args>(args)...);的就地构造。而且由于C++17强制的复制省略²,其中不会涉及临时对象。

# 返回std::optional

如果你从一个函数返回optional,那么直接返回std::nullopt或计算出的值会非常方便。

// Optional/optional_return_rvo.cpp
std::optional<std::string> TryParse(Input input) {
    if (input.valid())
        return input.asString();

    return std::nullopt;
}
// 使用:
auto oStr = TryParse(Input{...});
1
2
3
4
5
6
7
8
9

在上述示例中,你可以看到函数返回从input.asString()计算得到的std::string,并将其包装在optional中。如果值不可用,则返回std::nullopt。

由于C++17强制的复制省略(更多内容请阅读“通用语言特性”一章中“保证复制省略”一节。 ),optional对象oStr将在调用者处创建。

或者,你也可以尝试非标准化的命名返回值优化(Named Returned Value Optimisation)。当你在函数开头创建一个对象,然后返回它时,就会发生这种情况。

// Optional/optional_return_rvo.cpp
std::optional<std::string> TryParseNrvo(Input input) {
    std::optional<std::string> oOut; // 空的
    if (input.valid())
        oOut = input.asString();

    return oOut; 
}
// 使用:
auto oStr = TryParseNrvo(Input{...});
1
2
3
4
5
6
7
8
9
10

在第二个示例中,oStr对象也应该在调用者处创建。你可以尝试这个示例,其中包含额外的日志记录,用于检查optional变量的地址。

# 返回值时使用大括号要小心

下面这段代码³可能会让你感到惊讶:

std::optional<std::string> CreateString() {
    std::string str {"Hello Super Awesome Long String"};
    return {str}; // 这会导致拷贝
    // return str;   // 这会进行移动操作
}
1
2
3
4
5

根据标准,如果将返回值用大括号{}括起来,就会阻止移动操作发生,返回的对象只会被拷贝。

这与不可拷贝类型的情况类似:

std::unique_ptr<int> foo() {
    std::unique_ptr<int> p;
    return {p};  // 尝试拷贝unique_ptr,编译会失败
    // return p;  // 这会进行移动操作,所以对于unique_ptr来说没问题
}
1
2
3
4
5

³感谢JFT指出这个问题。

标准在[class.copy.elision]/3⁴中规定:

在以下复制初始化上下文中,可以使用移动操作代替复制操作:
- 如果return语句([stmt.return])中的表达式是(可能带括号的)标识表达式,该表达式命名了在最内层封闭函数或lambda表达式的函数体或形参声明子句中声明的具有自动存储期的对象;
- 如果throw表达式的操作数是一个非易失性自动对象(不是函数或catch子句参数)的名称,且其作用域不超出最内层封闭try块(如果有的话)的末尾

可以试着运行位于Chapter Optional/optional_return.cpp中的示例代码。该代码展示了一些使用std::unique_ptr、std::vector、std::string和自定义类型的示例。

# 访问存储的值

对于std::optional(除了创建操作之外)来说,最重要的操作可能就是获取其中包含的值的方式了。有以下几种选择:

  • operator*和operator-> - 如果没有值,行为是未定义的!
  • value() - 返回值,如果没有值则抛出std::bad_optional_access异常。
  • value_or(defaultVal) - 如果有值则返回该值,否则返回defaultVal。

要检查是否有值,可以使用has_value()方法,或者通过if (optional)来检查,因为std::optional可以隐式转换为bool类型。

下面的示例展示了这些操作:

// 通过operator*
std::optional<int> oint = 10;
std::cout<< "oint " << *oint << '\n ';
// 通过value()
std::optional<std::string> ostr("hello");
try {
    std::cout << "ostr " << ostr.value() << '\n ';
}
catch (const std::bad_optional_access& e) {
    std::cout << e.what() << '\n ';
}
// 通过value_or()
std::optional<double> odouble; // 空值
std::cout<< "odouble " << odouble.value_or(10.0) << '\n ';
1
2
3
4
5
6
7
8
9
10
11
12
13
14

还有一种便捷的模式,先检查是否有值,然后再访问它:

// 计算字符串的函数:
std::optional<std::string> maybe_create_hello();
// ...
if (auto ostr = maybe_create_hello(); ostr)
    std::cout << "ostr " << *ostr << '\n ';
else
    std::cout << "ostr is null\n ";
1
2
3
4
5
6
7

# std::optional的操作

让我们看看这个类型还有哪些其他操作。

# 修改值和对象生命周期

如果你已经有一个std::optional对象,可以通过emplace、reset、swap、assign等操作快速修改其中包含的值。如果用nullopt进行赋值(或重置),并且std::optional中包含一个值,那么该值的析构函数将会被调用。

下面的示例展示了所有这些情况,代码位于Chapter Optional/optional_reset.cpp:

#include <optional> 
#include <iostream> 
#include <string>

class UserName {
public:
    explicit UserName(std::string str) : mName(std::move(str)) {
        std::cout << "UserName::UserName('" << mName << " ')\n ";
    }
    ~UserName() {
        std::cout << "UserName::~UserName('" << mName << " ')\n ";
    }
private:
    std::string mName; 
};

int main() {
    std::optional<UserName> oEmpty;
    // emplace:
    oEmpty.emplace("Steve");
    // 调用~Steve并创建新的Mark:
    oEmpty.emplace("Mark");
    // 重置,使其再次为空
    oEmpty.reset(); // 调用~Mark 
    // 等同于:
    //oEmpty = std::nullopt;
    // 赋值一个新值:
    oEmpty.emplace("Fred");
    oEmpty = UserName("Joe");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

每次对象发生改变时,当前存储的UserName的析构函数都会被调用。

# 比较操作

std::optional允许你几乎“自然地”比较其中包含的对象,但当操作数中有nullopt时会有一些特殊情况。如下所示,代码位于Chapter Optional/optional_comparision.cpp:

#include <optional> 
#include <iostream>

int main() {
    std::optional<int> oEmpty;
    std::optional<int> oTwo(2);  
    std::optional<int> oTen(10);

    std::cout << std::boolalpha;
    std::cout << (oTen > oTwo) << '\n ';
    std::cout << (oTen < oTwo) << '\n ';
    std::cout << (oEmpty < oTwo) << '\n ';
    std::cout << (oEmpty == std::nullopt) << '\n ';
    std::cout << (oTen == 10) << '\n ';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上述代码的输出结果为:

true  // (oTen > oTwo)
false // (oTen < oTwo)
true  // (oEmpty < oTwo)
true  // (oEmpty == std::nullopt)
true  // (oTen == 10)
1
2
3
4
5

当操作数都包含值(且类型相同)时,你会得到预期的结果。但当一个操作数是nullopt时,它总是比任何包含值的std::optional“小”。

# 性能和内存考量

使用std::optional时,会增加内存占用。std::optional类包装了你的类型,为其准备空间,然后添加了一个布尔型参数。这意味着它会根据对齐规则扩展你原来类型的大小。

从概念上讲,标准库中std::optional的实现可能类似这样:

template <typename T>
class optional {
    bool _initialized;
    std::aligned_storage_t<sizeof(t), alignof(T)> _storage;
public: // 操作
};
1
2
3
4
5
6

std::optional的对齐规则在optional.optional⁵中定义如下:

所包含的值应分配在std::optional存储区中适合类型T对齐的区域内

例如,假设sizeof(double) = 8且sizeof(int) = 4:

std::optional<double> od; // sizeof = 16字节
std::optional<int> oi;    // sizeof = 8字节
1
2

虽然bool类型通常只占用一个字节,但std::optional类型需要遵循对齐规则,所以它的大小大于sizeof(YourType) + 1字节。

例如,有两个类型:

struct Range {
    std::optional<double> mMin;
    std::optional<double> mMax;
};

struct RangeCustom {
    bool mMinAvailable;
    bool mMaxAvailable;
    double mMin;
    double mMax; 
};
1
2
3
4
5
6
7
8
9
10
11

Range占用的空间比RangeCustom更多。在第一种情况下,Range占用32字节!第二种情况是24字节。这是因为第二个类可以将布尔型变量“压缩”在结构体的开头,而Range中的两个std::optional对象必须对齐到double类型的边界。

你可以在Chapter Optional/optional_sizeof.cpp中查看完整代码。

# 从boost::optional迁移

std::optional直接改编自boost::optional,因此你可以预期在这两个版本中有相似的使用体验。从一个迁移到另一个应该很容易,当然,还是存在一些细微差异。

下表总结了这些变化:

方面 std::optional boost::optional(截至1.67.0⁶)
移动语义(Move semantics) 是 是
无异常(noexcept) 是 是
哈希支持(hash support) 是 否
抛出异常的值访问器 是 是
字面量类型(可用于constexpr表达式) 是 否
就地构造(in place construction) emplace,标签in_place emplace(),标签in_place_init_if_t、in_place_init_t,工具in_place_factory
未初始化状态标签 nullopt 无
可选引用(optional references) 否 是
从optional<U>转换到optional<T> 是 是
显式转换为指针(get_ptr) 否 是
推导指引(deduction guides) 是 否

主要的区别在于std::optional支持哈希运算、可用于constexpr上下文,并且还有推导指引。然而,boost::optional支持引用,而这在C++17版本的std::optional中是不支持的。

在https://www.fluentcpp.com/2018/10/05/pros-cons-optional-references/ (opens new window)的 “Why Optional References Didn’t Make It In C++17”(为什么可选引用未被纳入C++17)一文中可以了解更多信息。

# 特殊情况:optional<bool>和optional<T*>

虽然你可以在任何类型上使用optional,但对于布尔类型和指针类型需要特别注意。

  • optional<bool>——它表示什么呢?使用这种构造,你得到的是一个三态布尔值。如果你确实需要这种类型,或许寻找一个真正的三态布尔值,比如boost::tribool (opens new window)会更好。 此外,使用这种类型可能会造成混淆,因为optional<bool>可以转换为bool。而且,如果其中有值,访问该值返回的也是bool类型。
  • 类似地,指针类型也存在类似的歧义:
// 不要这样尝试,这只是个示例!
std::optional<int*> opi { new int (10) };
if (opi && *opi) {
    std::cout << **opi << std::endl;
    delete *opi;
}
if (opi)
    std::cout << "opi is still not empty!";
1
2
3
4
5
6
7
8

在上面的示例中,你必须检查opi来判断optional是否为空,但opi的值也可能是nullptr。 int指针本身就是 “可空的”,将其包装在optional中会让使用变得混乱。

# std::optional的示例

这里有一些更扩展的示例,展示std::optional的良好应用场景。在第一个示例中,你将看到如何在类中使用optional。在第二个示例中,我们将介绍整数解析并将结果存储在optional中。

# 带有可选昵称和年龄的用户名

Chapter Optional/optional_user_name.cpp

#include <optional>
#include <iostream>
using namespace std;

class UserRecord {
public:
    UserRecord(string name, optional<string> nick, optional<int> age)
        : mName{move(name)}, mNick{move(nick)}, mAge{age}
    { }
    friend ostream& operator << (ostream& stream, const UserRecord& user);
private:
    string mName;
    optional<string> mNick;
    optional<int> mAge;
};

ostream& operator << (ostream& os, const UserRecord& user) {
    os << user.mName;
    if (user.mNick)
        os << ' ' << *user.mNick;
    if (user.mAge)
        os << ' ' << "age of " << *user.mAge;

    return os;
}

int main() {
    UserRecord tim { "Tim", "SuperTim", 16 };
    UserRecord nano { "Nathan", nullopt, nullopt };
    cout << tim << '\n';
    cout << nano << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

上述示例展示了一个带有可选字段的简单类。其中名字是必填项,而其他属性 “昵称” 和 “年龄” 是可选的。

# 从命令行解析整数

Chapter Optional/optional_parsing.cpp

#include <optional>
#include <iostream>
#include <string>

std::optional<int> ParseInt(const char* arg) {
    try {
        return { std::stoi(std::string(arg)) };
    } catch (...) {
        std::cerr << "cannot convert '" << arg << " ' to int!\n";
    }
    return { };
}

int main(int argc, const char* argv[]) {
    if (argc >= 3) {
        auto oFirst = ParseInt(argv[1]);
        auto oSecond = ParseInt(argv[2]);
        if (oFirst && oSecond) {
            std::cout << "sum of " << *oFirst << " and " << *oSecond;
            std::cout << " is " << *oFirst + *oSecond << '\n';
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

上述代码使用optional来表示转换是否成功。请注意,我们实际上将异常处理转换为了optional,抑制了转换过程中可能出现的异常。这种技术可能看起来存在争议。 代码中使用了stoi,它可以被新的底层函数from_chars替代。你可以在 “字符串转换” 章节中了解更多关于新转换工具的信息。

# 其他示例

这里还有一些可以使用std::optional的场景:

  • 表示可选的配置值
  • 几何与数学:判断对象之间是否存在交集
  • Find*()函数的返回值(假设你不关心诸如连接中断、数据库错误之类的问题)

你可能会在“A Wall of Your std::optional Examples (opens new window)”这篇文章中发现其他有趣的用法。这篇博客文章包含了许多读者提交的示例。

# 总结

关于std::optional需要了解的几个核心要点:

  • std::optional是一种包装类型,用于表示 “可空” 类型。
  • std::optional不会使用任何动态内存分配。
  • std::optional要么包含一个值,要么为空。
    • 可以使用operator *、operator->、value()或value_or()来访问其内部的值。
  • std::optional可以隐式转换为bool,这样你可以轻松检查它是否包含值。

# 编译器支持

特性 GCC Clang MSVC
std::optional 7.1 4.0 VS 2017 15.0
上次更新: 2025/04/01, 13:21:34
第二部分 - 标准库的变化
8. std::variant

← 第二部分 - 标准库的变化 8. std::variant→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
第四章 Lambda函数
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式