C++之std::optional 解决“无值”表示的问题
C++新特性整理目录:
- C++11新特性(总结)
- C++14新特性(总结)
- C++17新特性(总结)
- C++20新特性(总结)
- C++23新特性(总结)
- C++新特性对比说明(11、14、17、20、23)
- C++新特性详细对比表(11、14、17、20、23)
std::optional简要说明
std::optional 是 C++17 引入的一种类模板,用于表示一个值可能存在也可能不存在的情况。它是一个轻量级的包装器,能够显式表达“可选值”的语义,避免使用特殊值(如 -1 或 nullptr)来表示无效状态,从而提高代码的可读性和安全性。
简单的说,std::optional主要是用来解决“无值”表示的问题,即C++ 程序员如何表示一个“可能没有值”的语义?
我们先来思考一下,什么情况下会需要表示一个“可能没有值”的语义呢?
(1)查找和搜索操作:当在集合中查找元素时,可能找不到目标
(2)解析和转换:将字符串解析为其他类型时,字符串可能不符合格式要求。
(3)配置和可选参数:某些配置项可能是可选的。
(4)函数可能失败的计算:某些数学运算在特定条件下无意义
(5)获取序列中的最大值,但序列可能为空。
等等。也许有的人在写程序时,并没有太过在意这些问题,有的人可能在这里纠结了半天:我该如何表示没有值的情况,没有值的情况下,我的函数该返回什么才能保证编译通过,才能在调用时能够知道当前是否有值或者是有效值?
那么,在 std::optional 出现之前,我们如何表示一个“可能没有值”的语义呢?
我们一般会这样做:即使用特殊值:例如用 -1 表示找不到的索引,用 nullptr 表示空指针,用 std::string::npos 表示未找到的位置,在自定义的类中使用valid变量记录该对象是否有效等等。这确实可以解决上面的问题,但是他们却都有一个很严重的问题:“它们都没有在类型系统中明确地表达“可能无值”这一意图,容易导致运行时错误”,这是语义上的缺陷
示例代码如下:// 使用特殊值-1表示无值情况 // 所有val都大于等于0,-1表示没有找到 int find_val_by_id(const std::map<std::string, int>& db, std::string id) { auto it = db.find(id); if (it != db.end()) return it->second; return -1; } // 使用特殊值nullptr表示无值情况 // nullptr表示没有找到 int* find_ptr_by_id(const std::map<std::string, int*>& db, std::string id) { auto it = db.find(id); if (it != db.end()) return it->second; return nullptr; } class MyClass { public: MyClass() {}; ~MyClass() {}; void setValid(bool v) { mIsValid = v; }; bool isValid() { return mIsValid; }; private: bool mIsValid = false; }; // 使用特殊值mIsValid=false表示无值情况 MyClass getMyClassObject(const std::string& input) { // 实现具体逻辑 MyClass obj; if (true) { obj.setValid(true); } return obj; } // 使用特殊值nullptr表示无值情况 MyClass* getMyClassObjectPtr(const std::string& input) { if (true) { return new MyClass(); } return nullptr; } int main() { std::map<std::string, int> db1; int r1 = find_val_by_id(db1, "001"); if (r1 != -1) // 思考一个问题,如果这个函数不是你实现的,你怎么知道要做设个判断? { // do some thing } std::map<std::string, int*> db2; int* r2 = find_ptr_by_id(db2, "001"); if (r2 != nullptr) // 思考一个问题,如果这个函数不是你实现的,你怎么知道要做设个判断? { // do some thing } MyClass obj1 = getMyClassObject("obj1"); if (obj1.isValid()) // 思考一个问题,如果这个函数不是你实现的,你怎么知道要做设个判断? { // do some thing } MyClass* objptr = getMyClassObjectPtr("objptr"); if (objptr != nullptr) // 思考一个问题,如果这个函数不是你实现的,你怎么知道要做设个判断? { // do some thing } }从上面的示例代码中,你能看出哪些函数存在可能没有值的情况吗?如果光从定义看,我们好像确实没办法确定。如果这些函数接口是我们自己用还好,因为我们自己了解函数输出情况,如果是给别人用,那就糟糕透了。
如果别人没有做无效值判断、没有做空值判断,想想这会是一件多么糟糕的事情?然儿对于函数使用者来说,我不知道有这种情况,我为什么要去判断?
其实判断空指针已经成为大家的一个共识、习惯了,所以这还好,但是如果我必须返回一个对象呢???而且使用指针也会引出一个新的问题:动态分配,这是一个效率问题;指针由谁释放,这是资源管理的问题。
那么使用std::optional后会怎么样呢,由于std::optional是一个明确的标准,是大家都知道的。我使用了std::optional就是要告诉你,我的函数存在“可能没有值”的情况,你在使用时要注意判断,如果你还是没有判断,那就怪不得我了,因为我在语义已经告诉你了,你不听,又怎能怪我呢,对吧?下面是使用std::optional的示例代码:// 在map中查找键 std::optional<std::string> find_name_by_id(const std::map<int, std::string>& db, int id) { auto it = db.find(id); if (it != db.end()) return it->second; return std::nullopt; } // 在vector中查找满足条件的元素 std::optional<int> find_first_even(const std::vector<int>& vec) { for (int num : vec) { if (num % 2 == 0) return num; } return std::nullopt; } // 在vector中查找满足条件的元素 std::optional<MyClass> getMyClassObject(const std::vector<MyClass>& vec,int index) { if (index >= 0 && index < vec.size()) return vec[index]; return std::nullopt; }说了这么多,那么使用std::optional的主要价值有哪些呢?
- 表达意图明确 - 清楚地表示"可能有值,可能没有"
- 类型安全 - 强制调用者处理"无值"的情况
- 性能优秀 - 通常实现为值语义,无动态分配开销
- API清晰 - 使接口设计更加自解释
std::optional 基本使用
1、基本声明
这很简单,代码如下// 基本声明 std::optional<int> opt_int; std::optional<std::string> opt_string; std::optional<double> opt_double = std::nullopt;2、创建和初始化
有直接初始化和使用std::make_optional两种方式,示例代码如下:// 直接初始化 // 包含值的情况 std::optional<int> opt1 = 42; // 从值初始化 std::optional<int> opt2{ 100 }; // 直接初始化 std::optional<std::string> opt3 = "hello"; // 字符串 // 空值情况 std::optional<int> opt4 = std::nullopt; // 明确为空 std::optional<int> opt5; // 默认构造为空 // 使用 std::make_optional auto opt6 = std::make_optional(42); // 自动推导类型 auto opt7 = std::make_optional("hello"); //auto opt8 = std::make_optional({ 1, 2, 3 }); //无法推导出类型 auto opt8 = std::make_optional<std::vector<int>>({ 1, 2, 3 }); auto opt9 = std::make_optional(std::vector<int>{ 1, 2, 3 });3、检查是否有值
std::optional<int> opt = 42; // 多种检查方式 if (opt.has_value()) { std::cout << "有值\n"; } if (opt) { // 重载了 bool 转换 std::cout << "有值\n"; } if (!opt) { std::cout << "无值\n"; }4、访问值的方法
std::optional<std::string> opt = "hello"; // 1. value() - 安全访问,无值时抛异常 try { std::string s1 = opt.value(); // 返回 "hello" } catch (const std::bad_optional_access& e) { std::cout << "无值异常: " << e.what() << std::endl; } // 2. operator* - 直接访问,无值时未定义行为 if (opt) { std::string s2 = *opt; // 解引用 std::cout << s2 << std::endl; // 输出 "hello" } // 3. operator-> - 访问成员 std::optional<std::string> opt_str = "test"; if (opt_str) { size_t len = opt_str->length(); // 相当于 (*opt_str).length() std::cout << "长度: " << len << std::endl; }5、安全获取值
std::optional<int> opt; // value_or() - 提供默认值 int value1 = opt.value_or(100); // opt为空,返回100 std::cout << value1 << std::endl; // 输出 100 opt = 42; int value2 = opt.value_or(100); // opt有值,返回42 std::cout << value2 << std::endl; // 输出 426、修改值
//赋值和重置 std::optional<int> opt; // 赋值 opt = 100; // 现在包含 100 opt = 200; // 修改为 200 opt = std::nullopt; // 重置为空 // reset() 方法 opt = 300; opt.reset(); // 变为空 // 原地构造 (emplace) std::optional<std::vector<int>> opt_vec; // 原地构造,避免拷贝 opt_vec.emplace({ 1, 2, 3, 4, 5 }); // 直接构造vector // 等同于 opt_vec = std::vector<int>{ 1, 2, 3, 4, 5 }; // 交换sawp std::optional<int> a = 10; std::optional<int> b = 20; // 交换两个有值的 optional a.swap(b);
emplace与sawp函数功能分析
我们先自定义一个类,以便于后面的分析,类的实现如下:
class Point { public: int x, y, z; std::string description; Point(int x, int y,int z = 10, std::string description = "unkonwn") : x(x), y(y), z(z) , >description(description){ std::cout << "Point constructed: " << x << ", " << y << ", " << z << ", " << description << >std::endl; } Point(const Point& other) : x(other.x), y(other.y), z(other.z), description(other.description) { std::cout << "Point copied: " << x << ", " << y << ", " << z << ", " << description << std::endl; } Point& operator=(const Point& other) { x = other.x; y = other.y; z = other.z; description = other.description; std::cout << "Point assigned: " << x << ", " << y << ", " << z << ", " << description << std::endl; return *this; } };我们先来看看下面这样一段代码的运行结果吧:
std::optional<Point> p1 = Point(10, 10, 10, "001"); std::optional<Point> p2 = Point(20, 20, 20, "002"); std::optional<Point> tp; tp = p1; p1 = p2; p2 = tp;在这段代码中,我们首先创建了p1和p2,然后想交互一下p1、p2的值,我们来看看运行结果
执行了两次构造函数、三次拷贝构造函数、两次拷贝赋值运算。
我们再来看看使用sawp的运行结果:
代码如下:std::optional<Point> p1 = Point(10, 10, 10, "001"); std::optional<Point> p2 = Point(20, 20, 20, "002"); p1.swap(p2); std::cout << "p1:" << p1->description; std::cout << "p2:" << p2->description;运行结果如下:
依旧是执行了两次构造函数、三次拷贝构造函数、两次拷贝赋值运算。值也成果交换了
我们继续对代码进行修改,代码如下:std::optional<Point> p1; p1.emplace(10, 10, 10, "001"); std::optional<Point> p2; p1.emplace(20, 20, 20, "002"); p1.swap(p2); std::cout << "p1:" << p1->description << std::endl; std::cout << "p2:" << p2->description << std::endl;运行结果如下:
少了两次拷贝构造。
std::optional 的 Monadic 方法(C++23)
- transform - 变换值:transform 对包含的值应用函数,返回新的 optional
#include <optional> #include <string> #include <iostream> std::optional<int> to_int(const std::string& s) { try { return std::stoi(s); } catch (...) { return std::nullopt; } } int main() { std::optional<std::string> opt_str = "42"; // 传统方式 std::optional<int> old_way; if (opt_str.has_value()) { old_way = to_int(opt_str.value()); } // 使用 transform (C++23) auto result = opt_str.transform([](const std::string& s) { return s + "!"; }).transform([](const std::string& s) { return s.size(); }); // 如果 opt_str 有值: result 包含字符串长度 // 如果 opt_str 为空: result 也为空 std::cout << result.value_or(0) << std::endl; // 输出: 3 ("42!") }
- and_then - 链式返回 optional 的操作:and_then 类似于 transform,但要求函数返回 std::optional
#include <optional> #include <string> #include <iostream> #include <charconv> std::optional<int> parse_int(const std::string& s) { int value; auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value); if (ec == std::errc() && ptr == s.data() + s.size()) { return value; } return std::nullopt; } std::optional<int> double_value(int x) { return x * 2; } int main() { std::optional<std::string> input = "123"; // 传统方式需要多层嵌套检查 auto old_result = [&]() -> std::optional<int> { if (!input.has_value()) return std::nullopt; auto parsed = parse_int(*input); if (!parsed.has_value()) return std::nullopt; return double_value(*parsed); }(); // 使用 and_then 优雅链式处理 auto result = input.and_then(parse_int) .and_then(double_value); if (result) { std::cout << "Result: " << *result << std::endl; // 输出: 246 } }
- or_else - 处理空值情况:or_else 在 optional 为空时调用函数,通常用于错误处理或提供回退
#include <optional> #include <iostream> #include <string> std::optional<int> get_from_cache(const std::string& key) { std::cout << "Checking cache for: " << key << std::endl; return std::nullopt; // 模拟缓存未命中 } std::optional<int> get_from_db(const std::string& key) { std::cout << "Querying database for: " << key << std::endl; return 42; // 模拟数据库有数据 } int main() { std::string key = "user:123"; // 传统方式 auto value = get_from_cache(key); if (!value) { std::cout << "Cache miss, falling back to DB" << std::endl; value = get_from_db(key); } // 使用 or_else - 优雅的失败回退 auto result = get_from_cache(key).or_else([&] { std::cout << "Cache miss, falling back to DB" << std::endl; return get_from_db(key); }); if (result) { std::cout << "Final value: " << *result << std::endl; } }
更多推荐





所有评论(0)