薛定谔的盒子 - std::optional

1. 解决没有值的尴尬

作为 C++ 使用者,我们经常会经历以下这些场景:

  • 查找失败:在一个std::mapstd::vector里查找某个元素,如果没找到,此时该返回什么?
    • 返回 -1?如果容器里存的是负数怎么办?
    • 返回 nullptr?这个只能用于指针,而且得在堆上分配内存,万一忘了 delete 就内存泄漏了。
    • 抛出异常?如果没找到是很常见的情况,抛出异常太重了,影响性能。
  • 无效配置:读取一个配置文件,某个键可能不存在。我们不得不定义一个魔法值(比如 INT_MIN 或空字符串)来表示无效,这会让代码里充满 if (val != INT_MIN) 的检查,既难看又容易出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法1: 使用特殊值(容易混淆)
int findIndex(const std::vector<int>& v, int value)
{
for (size_t i = 0; i < v.size(); ++i)
{
if (v[i] == value) return static_cast<int>(i);
}
return -1; // -1 表示"未找到",但可能被误解
}

// 方法2: 使用指针(需要动态内存管理)
int* findElement(std::vector<int>& v, int value)
{
for (auto& elem : v)
{
if (elem == value) return &elem;
}
return nullptr; // 空指针表示"未找到"
}

C++17 引入了 std::optional,专门用来解决可能不存在值的问题。:std::optional<T> 是一个包装器,它要么包含一个类型为 T 的值,要么什么都不包含(表示空)。

  • 盒子可能是空的(表示无值状态)
  • 盒子可能包含一个特定类型的值
std::optional 类 API 在线查询

2. std::optional 操作

2.1 初始化和重置

这个类的头文件为:#include <optional>,关于std::optional对象的初始化常用的有以下几种方式:

  1. 默认初始化:如果不提供任何参数,std::optional 会被初始化为不包含任何值的状态。
  2. 使用 std::nullopt 显式置空:这是 C++ 标准推荐的表示空值的方式,比使用 nullptr{} 更语义化。
  3. 就地直接构造:使用std::optional要存储的对象对应的类的构造函数,这是最推荐的方式之一。
  4. 使用 make_optional 辅助函数:类似于 std::make_pairstd::make_shared,它可以自动推导类型。
  5. 隐式类型转换:如果类型 U 可以转换为类型 T,你可以用 U 来初始化 std::optional<T>

重置 / 赋值会改变 std::optional 的状态,可能涉及析构旧对象、构造新对象等逻辑。

  1. 赋值为 std::nullopt (重置为空):这是将一个已包含值的 optional 变为空的正确方式。
  2. 赋值为具体值:如果赋给 optional 一个新值,编译器会根据当前状态自动处理:
    • 情况 A(当前为空):在 optional 内部直接调用 T 的构造函数(拷贝或移动)。
    • 情况 B(当前有值):调用 T 的赋值运算符(operator=)将新值赋给旧对象。
  3. 使用 reset() 成员函数:reset() 是显式销毁内部值并将其置空的专用函数。
  4. 使用 emplace()emplace() 是最高效的重置并构造方式。它会销毁当前包含的对象,然后直接在 optional 的内存空间中使用给定的参数构造一个新对象。
  5. 交换 swap(opt):交换两个 optional 对象的状态。如果两者都有值,则交换其内部包含的值;如果状态不同,则按照逻辑移动值。
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <optional>
#include <string>
#include <vector>

// 定义一个可能存 int,也可能不存的变量
std::optional<int> get_data(bool should_return_data)
{
if (should_return_data)
{
return 10; // 返回一个值
}
else
{
return std::nullopt; // 明确表示“没值”
}
}

int main()
{
// 1. 默认构造:默认是空的
std::optional<int> a;
// std::cout << *a << std::endl; // 危险!解引用空的 optional 是未定义行为

// 2. 显式初始化为空
std::optional<int> b = std::nullopt;

// 3. 初始化为有值
std::optional<int> c = 20;

// 4. std::make_optional (类似于 make_pair)
auto d = std::make_optional<std::string>("Hello");

// 5. int 隐式转换为 double
std::optional<double> e = 42;

// 6. 重置为无值状态
d.reset(); // 方法1: 使用 reset()
c = std::nullopt; // 方法2: 赋值为 nullopt

std::optional<std::string> f = "Old Data";
// 销毁 "Old Data",然后直接调用 string(const char*, size_t) 构造函数
// 这种方式比 "f = std::string("New", 3)" 更高效,因为它不会产生临时 string 对象
f.emplace("New", 3);

// 判断是否为空
if (!d && !c)
{
std::cout << "c 和 d 现在都是空的" << std::endl;
}

a = 100;
b.swap(a);
std::cout << "a = " << a.value_or(0) << ", b = " << b.value_or(0) << std::endl;

return 0;
}

2.1 检查与访问

在使用 std::optional 时,检查其是否包含值(即是否有效)是至关重要的,因为直接解引用一个空的 std::optional 会导致未定义行为,通常是程序崩溃。

2.1.1 使用 has_value() 成员函数

使用 has_value() 成员函数这是最直接、语义最清晰的方法。has_value() 返回一个 bool,如果 optional 包含值则返回 true,否则返回 false

适用场景: 逻辑简单,只需要判断是否有值的场合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <optional>
#include <iostream>

std::optional<int> getData(bool flag)
{
if (flag) return 42;
return std::nullopt;
}

int main()
{
std::optional<int> data = getData(true);

if (data.has_value())
{
std::cout << "有值" << std::endl;
}
else
{
std::cout << "没有值." << std::endl;
}
return 0;
}

2.1.2 显式转换为 bool

std::optional 重载了布尔转换运算符。我们可以直接将 optional 对象放在 if 条件判断中。这与 has_value() 的效果完全一致,但代码更简洁。

适用场景: 绝大多数日常检查场景,这是现代 C++ 最推荐的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <optional>
#include <iostream>

std::optional<std::string> getName()
{
return "Alice";
}

int main()
{
auto name = getName();

// 直接判断,代码简洁
if (name)
{
// 如果 name 包含值,条件为真
std::cout << "Name: " << *name << std::endl;
}
else
{
std::cout << "Name is empty." << std::endl;
}
return 0;
}

2.2 值的获取

如果想要取出std::optional中存储的数据常用的方式有三种:

  1. 使用解引用操作符 * 或者箭头操作符 ->
    • 用法就像普通指针一样。
    • 警告:如果 optional 为空时使用这两个操作符,行为是未定义的(通常直接崩溃)。
  2. value() 成员函数:
    • 返回值的引用。
    • 安全特性:如果 optional 为空,它会抛出 std::bad_optional_access 异常。
  3. value_or() 成员函数:这不仅仅是一个检查方法,更是一个实用的访问模式,它会检查 optional 是否有值:
    • 如果有值,返回该值。
    • 如果为空,返回你指定的默认值。
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
33
34
35
36
37
38
#include <iostream>
#include <optional>
#include <string>

int main()
{
std::optional<std::string> opt1 = "Hello World";
std::optional<std::string> opt2; // 空

// --- 方式 1: operator* (不安全,需先检查) ---
if (opt1)
{
std::cout << "opt1 的长度: " << (*opt1).length() << std::endl;
std::cout << "opt1 的内容: " << *opt1 << std::endl;
}

// --- 方式 2: operator-> (不安全,需先检查) ---
if (opt1)
{
std::cout << "opt1 的长度: " << opt1->length() << std::endl;
}

// --- 方式 3: value() (抛异常版) ---
try
{
std::cout << "opt2: " << opt2.value() << std::endl;
}
catch (const std::bad_optional_access& e)
{
std::cout << "捕获异常: " << e.what() << std::endl;
}

// --- 方式 4: value_or() ---
// 如果 opt2 有值就用 opt2 的值,没值就用 "默认值"
std::cout << "opt2 或默认值: " << opt2.value_or("Hello Dabing") << std::endl;

return 0;
}

3. 使用实例

下面我们通过std::optional类实现一个配置文件解析的功能。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include <iostream>
#include <optional>
#include <string>
#include <map>
#include <sstream>

class Config
{
private:
std::map<std::string, std::string> data;

public:
void set(const std::string& key, const std::string& value)
{
data[key] = value;
}

// 返回 optional 表示键可能不存在
std::optional<std::string> getString(const std::string& key) const
{
auto it = data.find(key);
if (it != data.end())
{
return it->second;
}
return std::nullopt;
}

std::optional<int> getInt(const std::string& key) const
{
auto strOpt = getString(key);
if (!strOpt)
{
return std::nullopt;
}

try
{
return std::stoi(*strOpt);
}
catch (...)
{
return std::nullopt;
}
}

std::optional<double> getDouble(const std::string& key) const
{
auto strOpt = getString(key);
if (!strOpt)
{
return std::nullopt;
}

try
{
return std::stod(*strOpt);
}
catch (...)
{
return std::nullopt;
}
}
};

int main()
{
Config config;
config.set("server.port", "8080");
config.set("server.host", "localhost");
config.set("timeout", "30.5");

// 安全获取配置
auto port = config.getInt("server.port");
if (port)
{
std::cout << "端口: " << *port << std::endl;
}
else
{
std::cout << "端口未配置\n";
}

auto timeout = config.getDouble("timeout");
std::cout << "超时: " << timeout.value_or(60.0) << "秒\n";

// 不存在的配置
auto missing = config.getString("missing.key");
if (!missing)
{
std::cout << "配置项不存在\n";
}

return 0;
}

catch (...)是异常捕获器的最后一张底牌。如果前面有具体的 catch (const std::exception& e),那么它们会优先匹配;如果没有匹配到,或者只有 catch (...),那么所有异常都会被它捕获。