传统的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的Option
和Result
。和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::optional
和std::expected
(包括folly::Expected
等第三方库) 的使用方便很多。
发表回复