当前位置: 首页 > news >正文

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来说,我们支持了返回值和错误信息两点。


http://www.mrgr.cn/news/63470.html

相关文章:

  • 系统性能测试笔记-JMeter性能测试
  • Mysql 8.4.3LTS 的离线部署
  • 设计模式最佳实践代码总结 - 结构型设计模式篇 - 代理模式最佳实践
  • java-web-day5
  • uni-app 运行HarmonyOS项目
  • 硬盘的管理
  • 【JVM详解JVM优化】聊聊JVM优化
  • 开源AI智能名片2+1链动模式S2B2C商城小程序领域的未来探索
  • 如何保护网站安全
  • “聚类+Transformer”俩搭档配享太庙!这方向发A会根本不用忧!
  • 为什么网络又称为云(cloud)?
  • 【Linux】编辑器vim 与 编译器gcc/g++
  • 别再为质量问题头疼了,六西格玛黑带培训来救场!
  • 视频制作软件新手必备:8款剪辑工具剪辑思路分享!
  • Qt 框架会经历一系列的步骤来处理这个事件。以下是完整的处理流程
  • 龙迅#LT8668EX显示器图像处理芯片 适用于HDMI1.4+VGA转4PORT LVDS,支持4K30HZ分辨率,可做OSD菜单亮度调节!
  • 【运维管理】如何像管理linux一样,批量管理windows主机
  • 【Python3】【力扣题】409. 最长回文串
  • 小程序与服务器通信webSocket和UDPSocket
  • 【前端】强制刷新、清空缓存
  • React中常用的hook函数(二)——useReducer和useContext
  • C++11之新特性 --- function包装器与lambda表达式
  • AI直播带货场景切换模块的搭建!
  • 第14课 异常处理
  • 沙盒正在源代码防泄漏行业盛行
  • 【MySQL系列】理解 `utf8mb4` 和 `utf8mb4_unicode_ci`