Chaofan

For the next train

传统的C风格API使用错误码表示错误值:

// 0 for success, <0 for error
int get_configuration(struct Config *conf);

struct Config conf;
if (!get_configuration(&conf))
  return -1;

这种方法简单易用,开销低,也不需要复杂的语言构造,因此在底层API中始终流行。但现实世界的错误可能千奇百怪,以至于不能用简单的int类型表示。同时错误码实质上是把正确和错误的情况糅合在一起,非常容易遗漏。在C++中,还有一个问题:RAII风格非常流行,即对象的存在本身就代表了成功状态,因此并不应该先定义对象再传入某个函数进行构造,最好是把对象当返回值。

经典C++的解法是异常。但异常开销实在不小,且并没有真正解决上面提到的容易遗漏等问题,不明显的throw-catch执行流还会造成心智负担,写代码的时候看不清哪里会突然发生栈回溯从而产生资源泄漏。

Config conf = get_configuration(); // May this throw?

随着强类型函数式语言成为显学,用抽象数据类型 (Abstract Data Type, ADT) 承载不同结果返回值的做法也在主流语言中流行起来。典型例子有Rust,Option<T>表示一个可能为T也可能为空的值,Result<T, E>表示一个可能为T也可能为E但必是其中之一且不会两者皆是的值。ADT的好处是,Option<T>并不是T,因此写代码的人不得不意识到错误的存在 (虽然也可以不检查),否则编译不过。

C++引进了类似的概念。C++17引入的std::optional和C++23引入的std::expected分别对应了上面提到的Rust的OptionResult。和Rust类似,这两个模板也提供了方法检查状态、强制取值,或者传入匿名函数,当有值时调用它:

std::optional<int> opt_int;

opt_int.has_value(); // bool
opt_int.value(); // int&, throw if not exists
*opt_int; // int&, UB if not exists
opt_int.value_or(0); // int

// std::optional<float>, nothing if not exists
opt_int.and_then([](std::optional<int> o){
  return std::optional{float(*o)};
});

// std::optional<float>, nothing if not exists
opt_int.transform([](int o){
  return float(o);
});

// std::optional<int>, nothing if exists
opt_int.or_else([]{
  return std::optional<int>(0);
});

这三种传入lambda并变更对象类型的调用风格有时也被称为monadic,核心思想在于把optional当作容器。

但多数时候我们无法把程序写得如此理想,还是不得不用过程式的方法先检查再early return。Rust提供了两个好的语法构造:模式匹配和?运算符。此处不讨论模式匹配,介绍一下问号的用法:

fn get_configuration() -> Option<Config>;

let config = get_configuration()?;
// EQUALS TO:
// let config = get_configuration();
// if config.is_none() {
//   return config
// }
// let config = config.unwrap();

这里的问号相当于一个提前返回的语法糖,否则大量Option对象都需要手动检查再return,在链式调用或者涉及运算符的情况非常不便。很可惜,C++中没有这种语法。

但我们可以利用GCC和Clang的Statement Expression语法扩展完成类似的事情:

#define TRY_OPT(opt) ({   \
  auto&& val = (opt);     \
  if (!val.has_value()) { \
    return {};            \
  }                       \
  std::move(*val);        \
})

// early return if got null
auto config = TRY_OPT(get_configuration());

这个语法会创建一个作用域,但return等语句依然保留在当前函数体的作用,同时把最后一个语句当作整个表达式的值。大量运用这个写法,会让std::optionalstd::expected (包括folly::Expected等第三方库) 的使用方便很多。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注