平时我们在编写查找函数时,如果没查到目标数据,可能会约定返回 -1 或空字符串 ""。但在记录零下温度或账户盈亏等场景下,-1 本身就是合法的正常数据,很容易和 “没找到结果” 混为一谈。
为了清晰区分这两种情况,C++17 在 <optional> 头文件中引入了 std::optional 包装类型。其中 optional 是 “可选的” 意思,专门用来表示一个数据 “可能包含有效值,也可能处于空状态”,没有结果时直接返回 std::nullopt 即可。
C++ optional 的语法与使用
在 C++ 中,使用 std::optional 时需要先引入 <optional> 头文件,并在尖括号 <> 里写上实际要存放的数据类型。
语法:
#include <optional>
std::optional<数据类型> 变量名;
std::optional<数据类型> 变量名 = 初始值;说明:
操作 std::optional 对象时,最常用的空状态标识与成员函数有以下 4 个:
- std::nullopt:表示 “没有值” 的空状态,其中 nullopt 是 “null optional(空可选值)” 的缩写。我们定义
std::optional变量时如果不赋初值,默认也是空状态。 - has_value():判断当前对象里是否存有有效值。有值时返回
true;空状态时返回false。 - value():取出对象里面当前保存的值。
- reset():清空对象里面保存的值,让它重新回到空状态。
示例 1:判断是否有值
#include <iostream>
#include <optional>
#include <string>
int main() {
std::optional<int> score = 95;
std::optional<std::string> nickname = std::nullopt;
if (score.has_value()) {
std::cout << "考试成绩:" << score.value() << '\n';
}
if (!nickname.has_value()) {
std::cout << "当前没有设置昵称" << '\n';
}
return 0;
}运行结果如下。
考试成绩:95
当前没有设置昵称分析:
上面代码中,score 定义时赋了初值 95,因此 score.has_value() 返回 true,调用 score.value() 就能取出 95。而 nickname 被显式设为 std::nullopt 空状态,所以 has_value() 返回 false。
在程序运行过程中,我们还可以随时给空的 std::optional 赋上新值,或者调用 reset() 把已有的值清空。下面我们再来看一个例子。
示例 2:赋值与清空状态
#include <iostream>
#include <optional>
#include <string>
int main() {
std::optional<std::string> nickname;
nickname = "Tom";
if (nickname.has_value()) {
std::cout << "设置后昵称:" << nickname.value() << '\n';
}
nickname.reset();
std::cout << "清空后是否有值:" << nickname.has_value() << '\n';
return 0;
}运行结果如下。
设置后昵称:Tom
清空后是否有值:0分析:
nickname 定义时没有赋初值,默认处于空状态。当我们把 "Tom" 赋给 nickname 之后,它就变成了有值状态。随后调用 nickname.reset() 将其清空,has_value() 返回 false,因此屏幕上输出了 0。
除了调用 has_value() 和 value() 之外,std::optional 还支持类似指针的简写方式:直接把对象放进 if 条件括号里判断是否有值,并在有值时使用星号 * 取出数据。对于上面例子来说,下面 2 种方式是等价的。
// 方式 1(推荐)
if (nickname.has_value()) {
std::cout << nickname.value() << '\n';
}
// 方式 2
if (nickname) {
std::cout << *nickname << '\n';
}不过小伙伴们要特别注意,取值前必须先确认对象里有值。如果我们对处于空状态的 std::optional 直接取值,就会引发运行错误,下面这两种写法都是错误的。
std::optional<int> emptyOpt = std::nullopt;
std::cout << emptyOpt.value() << '\n'; // 抛出异常
std::cout << *emptyOpt << '\n'; // 结果不可控对空对象调用 value() 时,程序会直接抛出 std::bad_optional_access,触发异常处理流程。而如果直接使用 *emptyOpt 不做任何检查,则可能读出垃圾值或导致程序崩溃。因此在实际开发中,我们更推荐统一采用方式 1,代码意图更直观,万一误读空值也更容易排查。
使用 optional 作为函数返回值
在实际开发中,std::optional 最实用的场景就是充当查询或解析函数的返回值类型。比如在一个保存学生成绩的 std::map 容器中按姓名查找分数:如果找到了,我们就直接返回对应的分数;如果查无此人,就返回 std::nullopt。
示例 3:作为查找函数返回值
#include <iostream>
#include <map>
#include <optional>
#include <string>
std::optional<int> findScore(
const std::map<std::string, int>& scores,
const std::string& name) {
auto it = scores.find(name);
if (it == scores.end()) {
return std::nullopt; // 没找到返回空状态
}
return it->second; // 找到直接返回成绩
}
int main() {
std::map<std::string, int> scores = {
{"Jack", 95},
{"Lucy", 88}
};
std::optional<int> res1 = findScore(scores, "Jack");
if (res1.has_value()) {
std::cout << "Jack 的成绩:" << res1.value() << '\n';
}
std::optional<int> res2 = findScore(scores, "Tony");
if (res2.has_value()) {
std::cout << "Tony 的成绩:" << res2.value() << '\n';
} else {
std::cout << "未找到 Tony 的成绩" << '\n';
}
return 0;
}运行结果如下。
Jack 的成绩:95
未找到 Tony 的成绩分析:
在这个例子中,findScore() 的返回类型是 std::optional<int>。可能有些小伙伴会问:“找到 "Jack" 时,return it->second; 返回的明明是一个普通整数 95,为什么不会报错呢?”
其实这是因为 std::optional<int> 支持把普通的 int 值自动包装成 “有值状态” 的对象,不需要我们手动构造。而查找 "Tony" 没找到时,返回 std::nullopt 则会自动生成 “空状态” 的对象。
回到 main() 函数拿到返回值后,哪怕某个学生考了 0 分,我们通过 has_value() 也能准确区分 “成绩是 0 分” 和 “查无此人”,不再需要靠返回 -1 来猜结果。
使用 value_or() 提供默认值
有些时候,我们并不想专门写 if...else 分支去判断,而是希望有值时直接取出里面的值,处于空状态时就自动采用一个预设的默认值。这时我们调用 value_or() 方法,一行代码就能完成。
语法:
optional对象.value_or(默认值)说明:
调用 value_or() 时,如果原对象里有值,就返回内部保存的值;如果是空状态,就返回小括号里传入的默认值。
需要注意的是,括号里的默认值类型必须能转换为 std::optional 内部存放的数据类型。而且 value_or() 只负责返回一个读取结果,不会修改原对象自身的状态,非常适合用来读取可选配置项。
示例 4:读取可选配置项
#include <iostream>
#include <optional>
int main() {
std::optional<int> timeout = std::nullopt;
std::cout << "当前超时:" << timeout.value_or(30) << '\n';
std::cout << "timeout 是否有值:" << timeout.has_value() << '\n';
timeout = 60;
std::cout << "修改后超时:" << timeout.value_or(30) << '\n';
return 0;
}运行结果如下。
当前超时:30
timeout 是否有值:0
修改后超时:60分析:
当我们第 1 次调用 timeout.value_or(30) 时,因为 timeout 处于空状态,所以直接返回了默认值 30。同时从第 2 行输出可以看到,timeout.has_value() 依然为 0,说明 value_or(30) 并没有把 30 存进 timeout 里。
后面我们执行 timeout = 60; 赋上新值后,它里面已经存有有效数据,第 2 次调用 timeout.value_or(30) 时就会直接返回实际保存的 60。
常见问题
1. std::optional<std::string> 为空(std::nullopt)和里面存了一个空字符串 "" 是一回事吗?
不是一回事。如果赋值为 std::nullopt,表示 “盒子里什么都没装”,此时 has_value() 返回 false。如果赋值为 "",表示 “盒子里已经装了一个字符串,只不过长度为 0”,此时 has_value() 返回 true。这就像调查问卷里 “这一栏压根没填” 和 “填了空白内容” 的区别一样。
2. std::optional 和指针都能表示 “空”,平时我们应该怎么选?
如果我们只是想表示某个整数、浮点数、字符串或小结构体 “可能存在,也可能不存在”,优先使用 std::optional 即可。它直接把数据存放在对象内部,不需要去堆内存上动态分配空间。只有当我们需要指向外部已有的对象、共享大型对象或实现多态时,才使用指针或智能指针。
