提问人:Tom 提问时间:8/4/2022 最后编辑:Nicol BolasTom 更新时间:5/7/2023 访问量:1841
C++ 是否有 std::expected 的实现不考虑默认构造的“预期”具有值?
C++ is there an implementation of std::expected that doesn't consider a default constructed `expected` to have a value?
问:
我一直在寻找一种改进 C++ 库错误处理的好方法,目的是在保持效率的同时降低容易出错的代码的风险。
我在YouTube上偶然发现了Andrei Alexandrescu的Expect the Expected演讲,并被吸引,预计(呵呵)将成为C++23标准的一部分。std::expected
但是,正如所提议的那样,有一些我不喜欢的东西:根据 Alexandrescu 的说法,与 和 方法一致 当未设置值时具有未定义的行为。对我来说,这使得程序太容易与潜在的腐败状态一起徘徊了。std::expected
std::optional
operator*
operator->
幸运的是,有一个选项,即仅使用代替,当值未设置时(即使值类型是基元)也会抛出。std::optional::value()
bad_optional_access
但是,对于类似实现,我们有什么条件呢?std::expected
到目前为止,我已经看过了
两者看起来都很棒(尽管我承认它们使用了许多我仍在努力学习的概念。如果我对自己的能力更有信心,我会自己写。
但是,我的抱怨是,这些不会像尝试读取未设置的值时那样抛出异常。std::optional::value()
cpp::result<int, int> res;
res.value(); // totally fine for some reason - returns '0'.
防止这种情况的一种方法是仅使用没有默认构造函数的结果类型。但我真正想要的是,如果我尝试使用尚未设置的值(至少不是显式设置),则会出现异常。
我是不是想多了?如果没有 - 谁能告诉我是否有不允许使用未设置值的实现(理想情况下是 C++14)?
答:
如果要防止用户忽略或忘记错误,则从函数引发异常不是一个好主意。异常在函数签名中是不可见的,因此很容易被遗忘和忽略。如果错误不经常发生,代码很容易通过所有测试和代码审查,最终在生产中崩溃。也很难知道该函数可以引发哪些异常,除非该函数有非常详细的文档记录。最好不要让忽略错误的代码进行编译。
我不知道有任何库实现了C++的结果类型,它不允许忽略错误路径的代码进行编译,但可以使用 std::variant<T、E> 和 std::visit 来实现 Result 类,它强制您处理成功和错误情况,并且不允许访问结果, 发生错误时。这个想法是创建一个匹配成员函数,从 Rust 模式匹配中汲取灵感,它强制用户为两个执行路径提供实现。
match 函数将 lambda 作为参数,并使用重载技巧为 std::visit 创建处理程序。然后,它调用 std::visit 以获取结果。这是一个简化的示例,说明这将如何工作,适当的实现应该更仔细地考虑细节。
#include <variant>
#include <string_view>
#include <iostream>
template<typename T, typename... Es>
class Result {
template<class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
// explicit deduction guide (not needed as of C++20)
template<class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
std::variant<T, Es...> result;
public:
Result(decltype(result) value) : result(value) {}
template<typename Type>
Result(Type value) : result(value) {}
template<typename... Lambdas>
[[nodiscard]] auto match(Lambdas... lambdas) {
Overloaded handler{lambdas...};
return visit(handler, result);
}
};
struct Error1 {
std::string_view text;
[[nodiscard]] std::string_view message() { return text; };
};
struct Error2 {
std::string_view text;
[[nodiscard]] std::string_view message() { return text; };
};
[[nodiscard]] Result<int, Error1, Error2> libraryFunction(int val) {
if(val > 0) {
return 0;
} else if(val == 0) {
return Error1{"Error 1"};
} else {
return Error2{"Error 2"};
}
}
int main() {
std::cout <<
libraryFunction(1).match(
[](int res) { return res; },
[](auto err) -> int { std::cout << err.message() << std::endl; abort(); }
)
<< std::endl;
libraryFunction(0).match(
[](int res) { std::cout << res << std::endl; },
[](Error1 err) { std::cout << err.message() << std::endl; },
[](Error2 err) { std::cout << err.message() << std::endl; abort(); }
);
libraryFunction(-1).match(
[](auto res) {
if constexpr(std::is_same<decltype(res), int>())
std::cout << res << std::endl;
else {
std::cout << res.message() << std::endl;
abort();
}
}
);
return 0;
}
用户仍然可以提供一个空的错误处理程序,但在这里很难意外地忘记它。在代码审查中,当需要处理错误以及处理程序是否为空时,也应该很容易注意到。如果向函数添加新的错误类型,则关心错误类型的代码也将停止编译,直到修复为止。从这个角度来看,异常是可怕的,在标准库中随处可见,在不允许异常和动态内存分配的嵌入式系统中,许多代码都很危险。基于 boost::variant2 而不是 std::variant 也可能是有意义的,可以摆脱必须通过异常状态来考虑无值。
std::expected 提供的接口允许用户不处理意外情况,而 operator* 接口甚至允许未定义的行为。它还缺少与 match -method 等效的方法,这似乎是最安全的选择。允许用户使用任何额外的接口(允许忽略错误)的问题在于,他们可能会使用它们来代替,因为大多数人不喜欢错误处理。
预期和异常的命名对于用于错误处理的东西也很差。将错误称为异常或意外会使程序员认为错误是异常的,不太常见,因此不那么重要。在许多情况下,错误实际上可能比成功的执行路径更有可能发生,甚至更重要。
也许人们应该重新考虑你想要的行为是否是一个好主意。
请务必记住,这通常用于错误代码(通常是整数或枚举)的情况。这非常重要,因为许多错误代码默认构造为一个值,该值具体表示“不是错误”。这就是工作原理。这就是工作原理。这就是 std::exception_ptr
的工作方式(默认为不指向错误)。查看任何具有错误代码的基于 C 的库,错误代码的 0 值表示“不是错误”的可能性很大。expected<T, E>
E
std::errc
std::error_code
expected<T, E>
当然,它是否“有错误”也随之而来,因此任何“没有错误”的状态都是无关紧要的。但是,由于通常只是 C 错误代码类型,因此任何“非错误”状态的剔除都应由组成 .也就是说,您可以通过执行以下操作来报告错误:E
E
expected<T, E>
if(error_code) return unexpected(error_code);
通过将默认构造设置为默认错误代码值,您实际上是在掷骰子,以确定“有错误”状态是否实际上意味着有错误,而不仅仅是具有表示“不是错误”的错误代码。expected<T, E>
expected
评论
.value()
return {};