C++ 是否有 std::expected 的实现不考虑默认构造的“预期”具有值?

C++ is there an implementation of std::expected that doesn't consider a default constructed `expected` to have a value?

提问人:Tom 提问时间:8/4/2022 最后编辑:Nicol BolasTom 更新时间:5/7/2023 访问量:1841

问:

我一直在寻找一种改进 C++ 库错误处理的好方法,目的是在保持效率的同时降低容易出错的代码的风险。

我在YouTube上偶然发现了Andrei Alexandrescu的Expect the Expected演讲,并被吸引,预计(呵呵)将成为C++23标准的一部分。std::expected

但是,正如所提议的那样,有一些我不喜欢的东西:根据 Alexandrescu 的说法,与 和 方法一致 当未设置值时具有未定义的行为。对我来说,这使得程序太容易与潜在的腐败状态一起徘徊了。std::expectedstd::optionaloperator*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++ 标准-预期

评论

2赞 Stephen Newell 8/4/2022
此处的答案建议您可以使用,如果没有值,则会出现异常。.value()
0赞 Goswin von Brederlow 8/5/2022
@StephenNewell 他的问题是预期值有一个默认值,所以有一个值。
1赞 Goswin von Brederlow 8/5/2022
为什么要在函数中创建预期类型的变量?仅当有值或错误时,才在 return 语句中创建它们。这样,您就不会意外返回默认值。
0赞 Tom 8/6/2022
@GoswinvonBrederlow这是一个很好的观点。如果我有我的方式,我会在编译过程中以某种方式强制执行,但我认为这会起作用。
0赞 Goswin von Brederlow 8/7/2022
有时您可能希望return {};

答:

0赞 Apo 5/7/2023 #1

如果要防止用户忽略或忘记错误,则从函数引发异常不是一个好主意。异常在函数签名中是不可见的,因此很容易被遗忘和忽略。如果错误不经常发生,代码很容易通过所有测试和代码审查,最终在生产中崩溃。也很难知道该函数可以引发哪些异常,除非该函数有非常详细的文档记录。最好不要让忽略错误的代码进行编译。

我不知道有任何库实现了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 等效的方法,这似乎是最安全的选择。允许用户使用任何额外的接口(允许忽略错误)的问题在于,他们可能会使用它们来代替,因为大多数人不喜欢错误处理。

预期和异常的命名对于用于错误处理的东西也很差。将错误称为异常或意外会使程序员认为错误是异常的,不太常见,因此不那么重要。在许多情况下,错误实际上可能比成功的执行路径更有可能发生,甚至更重要。

1赞 Nicol Bolas 5/7/2023 #2

也许人们应该重新考虑你想要的行为是否是一个好主意。

请务必记住,这通常用于错误代码(通常是整数或枚举)的情况。这非常重要,因为许多错误代码默认构造为一个值,该值具体表示“不是错误”。这就是工作原理。这就是工作原理。这就是 std::exception_ptr 的工作方式(默认为不指向错误)。查看任何具有错误代码的基于 C 的库,错误代码的 0 值表示“不是错误”的可能性很大。expected<T, E>Estd::errcstd::error_code

expected<T, E>当然,它是否“有错误”也随之而来,因此任何“没有错误”的状态都是无关紧要的。但是,由于通常只是 C 错误代码类型,因此任何“非错误”状态的剔除都应由组成 .也就是说,您可以通过执行以下操作来报告错误:EEexpected<T, E>if(error_code) return unexpected(error_code);

通过将默认构造设置为默认错误代码值,您实际上是在掷骰子,以确定“有错误”状态是否实际上意味着有错误,而不仅仅是具有表示“不是错误”的错误代码。expected<T, E>expected