众所周知,我们经常会用到带返回值的函数,并且需要利用到返回值做逻辑判断。但仅对返回值做处理,多数情况下不便于同时获取函数执行结果与函数的状态。例如返回null值,返回错误值,返回正确值。
我们可以使用C++17 std::optional
,来优雅的处理此情况,直接表达「有值」或「无值」的意图,避免魔数(如 -1、nullptr)。下面列举常见用法以及规避使用的情况。
1. std::optional
诞生的意义
std::optional
的引入解决了在函数返回值为“无值”或“错误值”时的两大问题:
-
明确性:当函数返回一个可能没有值的结果时,传统做法是返回一些哨兵值(如
-1
、nullptr
或空字符串)。这些值很容易被误用,也不清晰地表达该值是否有效。std::optional
明确表示返回的值要么有效(包含一个值),要么无效(没有值)。 -
类型安全:使用
std::optional
可以避免潜在的类型问题。例如,返回一个整数-1
来表示错误,这样就可能会与有效的-1
数据值冲突。而使用std::optional<int>
可以更清晰地表示返回值是否存在。
2. std::optional
常见用法
基本用法
#include <iostream>
#include <optional>
std::optional<int> divide(int a, int b) {
if (b == 0) {
return std::nullopt; // 返回无效值(没有结果)
} else {
return a / b; // 返回有效值
}
}
int main() {
std::optional<int> result = divide(10, 2);
if (result) {
std::cout << "Result: " << *result << std::endl; // 使用解引用运算符 *
} else {
std::cout << "Division by zero!" << std::endl;
}
result = divide(10, 0);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Division by zero!" << std::endl;
}
return 0;
}
常用操作
-
初始化:使用
std::optional
的构造函数来创建可选值。std::optional<int> opt1 = 42; // 有值 std::optional<int> opt2; // 无值,等价于 std::nullopt
-
判断是否有值:使用
operator bool()
来判断是否有值,或者使用has_value()
方法。if (opt1) { /* 有值 */ } if (!opt2) { /* 无值 */ }
-
访问值:使用解引用运算符
*
或value()
方法访问值。value()
如果没有值会抛出std::bad_optional_access
异常。if (opt1) { std::cout << *opt1 << std::endl; // 解引用访问 } std::cout << opt1.value() << std::endl; // 使用 value()
-
默认值:使用
value_or
方法,在没有值时返回一个默认值。int value = opt2.value_or(100); // 如果 opt2 没有值,返回 100
-
重置值:使用
reset()
方法来清除optional
内部的值。opt1.reset(); // 清除 opt1 的值
链式调用与转换
std::optional<int> add_one(std::optional<int> opt) {
return opt ? std::optional<int>(*opt + 1) : std::nullopt;
}
std::optional<int> multiply_by_two(std::optional<int> opt) {
return opt ? std::optional<int>(*opt * 2) : std::nullopt;
}
int main() {
std::optional<int> value = 5;
std::optional<int> result = add_one(value).flat_map(multiply_by_two);
if (result) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "No valid result" << std::endl;
}
}
在上面的例子中,flat_map
被用来进行链式调用,并且保持了 optional
的状态。
3. 常见坑和注意事项
坑 1: 不小心解引用无值的 optional
解引用一个无值的 optional
会导致未定义行为。为了避免这种情况,使用 has_value()
或者 operator bool()
来确保值是有效的。
std::optional<int> opt; // 无值
if (opt) {
std::cout << *opt << std::endl; // 不会出错
} else {
std::cout << "No value" << std::endl;
}
坑 2: value()
会抛出异常
value()
方法在 optional
没有值时会抛出 std::bad_optional_access
异常。所以,当你不确定 optional
是否有值时,使用 value_or()
或者先检查 has_value()
。
std::optional<int> opt;
// throw std::bad_optional_access
try {
std::cout << opt.value() << std::endl;
} catch (const std::bad_optional_access&) {
std::cout << "Caught exception: No value" << std::endl;
}
// 更安全的使用方式
std::cout << opt.value_or(42) << std::endl; // 如果没有值,返回默认值 42
坑 3: optional
不支持 null
值
std::optional
只能表示一个“有值”或“无值”状态,并不能直接用于表示指针的“空指针”或其他“无效”的指针情况。如果要处理指针类型的“空”值,通常仍然使用 nullptr
。
std::optional<std::string> opt1; // 这是“没有值”
std::string* ptr = nullptr; // 这是空指针
坑 4: 赋值和拷贝
optional
是有值时才会拷贝其值,否则为“无值”状态。在赋值时需要注意:
std::optional<int> opt = 42;
std::optional<int> opt2 = opt; // 拷贝值
opt2.reset(); // opt2 变为无值
std::optional<int> opt3; // opt3 没有值
opt3 = opt2; // opt3 也将是无值状态
坑 5: 和 std::nullopt
一起使用时的误解
std::nullopt
是一个特殊的值,表示 optional
没有值。它不能与具体的值一起使用,且必须与 std::optional
一起使用。
std::optional<int> opt = std::nullopt; // 正确
std::optional<int> opt2 = 5; // 正确
// std::optional<int> opt3 = std::nullopt = 5; // 错误,不能与值一起使用
坑 6: 性能问题
optional
本身是一个小对象,但它的内部实现可能包含动态内存分配或多次拷贝,尤其在存储大对象时会产生一些性能开销。对于性能敏感的应用,要特别注意这个问题。