C++ optional

平时我们在编写查找函数时,如果没查到目标数据,可能会约定返回 -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 即可。它直接把数据存放在对象内部,不需要去堆内存上动态分配空间。只有当我们需要指向外部已有的对象、共享大型对象或实现多态时,才使用指针或智能指针。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号