std::optional与函数返回值的讨论
这个话题是因为,最近一段时间看到有人在问std::optional有什么用,以及不理解为什么要有这个类。所以打算简单介绍一下std::optional,重点讨论返回值相关的内容。
1 std::optional
- 类模板 std::optional 管理一个可选的包含值,即可能存在也可能不存在的值。
#include <iostream>
#include <optional>
int main()
{std::optional<std::string> a{"abc"};std::optional<std::string> b;if(a) {std::cout << "a = " << *a << "\n";}if(!b) {std::cout << "b is not set\n";}return 0;
}
- optional 的一个常见用例是可能失败的函数的返回值。
#include <iostream>
#include <optional>std::optional<int> getScore(int id) {// dosomethingbool searchSuccessed = doSearchScoreExist(id);if(searchSuccessed) {int score = doSearchScore(id);return score;}return std::nullopt;
}int main()
{int validId = 2323131;int invalidId = 231310998;if(getScore(valid_id)) {std::cout << "search OK! id = " << validId << "\n";} else {std::cout << "search fail! id = " << validId << "\n";}if(getScore(invalidId)) {std::cout << "search OK! id = " << invalidId << "\n";} else {std::cout << "search fail! id = " << invalidId << "\n";}return 0;
}
- 它包含有两个优点,一是更好的可读性;二是可更好处理高成本对象(来自于cppreference描述,但存疑)。
对于可读性来说,在c++中为了实现可能存在或者可能不存在的值的功能时。我们可能会选择 创建一个结构体来描述它(坏处是每个类型都需要创建一个这样的结构体)、使用std::pair(坏处是不容易看懂什么意思)、自己造轮子(耗时,还得自己写测试)。
对于高成本对象来说,我未做深入测试,但是从布局来看,它和std::pair<T,bool>相比并没有什么空间和时间的优势。std::optional的可能构成方式:
struct EmptyByte {};
template<class T>
class optional
{bool engaged;union {T value;EmptyByte emptyByte;};
};
2 函数返回值
这里为了方便,我们扩展前面的成绩查询示例:
class ScoreManager {
public:void addScore(int id,int score);int getScore(int id);
};
上述的接口有几个问题,我们分别来看:
2.1 缺少返回值
无论是add还是get,均至少需要对用户返回一个结果,代表着运行正确或者错误。
对于get来说,有的人可能会考虑到score最小值是0,那么我用-1来代表它的结果是有效的。首先,这种使用“魔法值”来判定程序状态的返回值,存在几个重大问题:
一是对用户来说,极其容易用错,因为他需要阅读你的接口实现或者相关的注释才会知道这件事,更何况大部分情况下都是不读/不写注释的;
第二是代码出现bug,极其难以定位,假设某个平均成绩计算错误,在复杂场景中,根本无法确定问题;
第三是影响用户的单元测试,假设其他人调用了getScore,却不知道getScore还有失败的时候,它的单元测试根本覆盖不到这里。
接下来,我们做一定的补全:
class ScoreManager {
public:bool addScore(int id,int score);std::optional<int> getScore(int id);
};
2.2 没有对返回值的处理做限制
因为我们的addScore和getScore都可能会出现失败的情况,用户有可能会不对这种失败的情况做验证,因此,我们需要用户必须去处理返回值。我们期望的是这样:
void good()
{auto ret = scoreManager.getScore(0);if (!ret) {// dosomething}if(!scoreManager.addScore(1,0)) {// dosomething}
}// 我们期望没有处理返回值时,报错或者警告
void bad()
{scoreManager.addScore(10,10);scoreManager.getScore(5);
}int main()
{ScoreManager scoreManager;good();bad();return 0;
}
在这种情况下,我们可以使用c++中的[[nodiscard]]
属性,该属性的作用是:
如果将声明了nodiscard的函数的返回值忽略,则编译器发出警告。
我们更新一下ScoreManager:
class ScoreManager {
public:[[nodiscard]]bool addScore(int id,int score);[[nodiscard]]std::optional<int> getScore(int id);
};
2.3 没有失败时的报错信息
对于getScore接口来说,我们期望在它错误时能反馈一些信息给用户,以提示它的错误原因。这时不得不夸一下Rust的Result了,来看看它是如何做的:
fn div(x: f64, y: f64) -> Result<f64, MathError> {if y == 0.0 {// 此操作将会失败,那么(与其让程序崩溃)不如把失败的原因包装在// `Err` 中并返回Err(MathError::DivisionByZero)} else {// 此操作是有效的,返回包装在 `Ok` 中的结果Ok(x / y)}
}
上述的接口对用户来说,如果运行正确,则返回结果;否则,返回错误的原因。在c++当中没有自带这样的接口,不过,我们可以自己简单的实现一下:
template<typename V,typename E>
class Reust
{
private:std::optional<V> value;std::optional<E> error;Result(const std::optional<V> &value,const std::optional<E> &error):value(value),error(error){}
public:static Reust<V,E> Ok(V &&v) {return Result(std::forward<V>(v),std::nullopt);}static Reust<V,E> Err(E &&e) {return Result(std::nullopt,std::forward<E>(e));}bool isError() {return error.has_value();}const V & getValue() {return value.value();}const V & getError() {return error.value();}
};
到这里,我们造好了这个玩具轮子,接下来更新ScoreManager:
class ScoreManager {
public:enum class ManagerError{...};[[nodiscard]]std::optional<ManagerError> addScore(int id,int score);[[nodiscard]]Reust<int,std::ManagerError> getScore(int id);
};
因为对于addScore来说的话,它仅需要错误时的原因,所以采取了std::optional;对于getScore来说,我们支持了返回值和错误信息两点。