C++C++17薛定谔的盒子 - std::optional
苏丙榅1. 解决没有值的尴尬
作为 C++ 使用者,我们经常会经历以下这些场景:
- 查找失败:在一个
std::map或std::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
| 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; }
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对象的初始化常用的有以下几种方式:
- 默认初始化:如果不提供任何参数,
std::optional 会被初始化为不包含任何值的状态。
- 使用
std::nullopt 显式置空:这是 C++ 标准推荐的表示空值的方式,比使用 nullptr 或 {} 更语义化。
- 就地直接构造:使用
std::optional要存储的对象对应的类的构造函数,这是最推荐的方式之一。
- 使用
make_optional 辅助函数:类似于 std::make_pair 或 std::make_shared,它可以自动推导类型。
- 隐式类型转换:如果类型
U 可以转换为类型 T,你可以用 U 来初始化 std::optional<T>。
重置 / 赋值会改变 std::optional 的状态,可能涉及析构旧对象、构造新对象等逻辑。
- 赋值为
std::nullopt (重置为空):这是将一个已包含值的 optional 变为空的正确方式。
- 赋值为具体值:如果赋给
optional 一个新值,编译器会根据当前状态自动处理:
- 情况 A(当前为空):在
optional 内部直接调用 T 的构造函数(拷贝或移动)。
- 情况 B(当前有值):调用
T 的赋值运算符(operator=)将新值赋给旧对象。
- 使用
reset() 成员函数:reset() 是显式销毁内部值并将其置空的专用函数。
- 使用
emplace() :emplace() 是最高效的重置并构造方式。它会销毁当前包含的对象,然后直接在 optional 的内存空间中使用给定的参数构造一个新对象。
- 交换
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>
std::optional<int> get_data(bool should_return_data) { if (should_return_data) { return 10; } else { return std::nullopt; } }
int main() { std::optional<int> a; std::optional<int> b = std::nullopt;
std::optional<int> c = 20;
auto d = std::make_optional<std::string>("Hello"); std::optional<double> e = 42; d.reset(); c = std::nullopt; std::optional<std::string> f = "Old Data"; 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) { std::cout << "Name: " << *name << std::endl; } else { std::cout << "Name is empty." << std::endl; } return 0; }
|
2.2 值的获取
如果想要取出std::optional中存储的数据常用的方式有三种:
- 使用解引用操作符
* 或者箭头操作符 ->:
- 用法就像普通指针一样。
- 警告:如果
optional 为空时使用这两个操作符,行为是未定义的(通常直接崩溃)。
value() 成员函数:
- 返回值的引用。
- 安全特性:如果
optional 为空,它会抛出 std::bad_optional_access 异常。
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;
if (opt1) { std::cout << "opt1 的长度: " << (*opt1).length() << std::endl; std::cout << "opt1 的内容: " << *opt1 << std::endl; }
if (opt1) { std::cout << "opt1 的长度: " << opt1->length() << std::endl; }
try { std::cout << "opt2: " << opt2.value() << std::endl; } catch (const std::bad_optional_access& e) { std::cout << "捕获异常: " << e.what() << std::endl; }
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; } 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 (...),那么所有异常都会被它捕获。