C++23中的std::expected:异常处理

C++23中的std::expected:异常处理

众所周知,C++23以前的异常处理是比较麻烦的,尤其是自己要在可能抛出异常的地方,需要自己去捕获它,比如除数为0的异常、使用std::stoi函数将字符串转换成int整型数据、处理文件读写的异常等等,不然很容易造成程序终止。

关于C++23中引入的std::expected,这一全新的词汇表类型,它为函数返回结果的处理提供了一种更加优雅、类型安全的解决方案。

std::expected是C++23标准库中的一个模板类,定义于头文件<expected>中。它提供了一种方式来表示两个值之一:类型T的期望值,或类型E的非期望值。std::expected永远不会是无值的。

cpp 复制代码
// Defined in header <expected>
template< class T, class E >
class expected;
(1)	(since C++23)
template< class T, class E >
    requires std::is_void_v<T>
class expected<T, E>;

类模板 std::expected 提供了一种表示两种值的方式:类型为 T 的预期值或类型为 E 的意外值。预期值永远不会是无值的。

  1. 主模板。在其自身的存储空间中包含预期值或意外值,该存储空间嵌套在预期对象中。
  2. void 部分特化。表示预期 void 值或包含意外值。如果包含意外值,则嵌套在预期对象中。
    如果程序使用引用类型、函数类型或 std::unexpected 的特化来实例化预期值,则程序格式错误。此外,T 不能是 std::in_place_t 或 std::unexpect_t。

模板参数

T - 预期值的类型。该类型必须是(可能为 cv 限定的)void,或者满足Destructible即可析构性要求(特别是,不允许使用数组和引用类型)。

E - 意外值的类型。该类型必须满足Destructible要求,并且必须是 std::unexpected 的有效模板参数(特别是,不允许使用数组、非对象类型和 cv 限定的类型)。

示例程序

cpp 复制代码
#include <cmath>
#include <expected>
#include <iomanip>
#include <iostream>
#include <string_view>
 
enum class parse_error
{
    invalid_input,
    overflow
};
 
auto parse_number(std::string_view& str) -> std::expected<double, parse_error>
{
    const char* begin = str.data();
    char* end;
    double retval = std::strtod(begin, &end);
 
    if (begin == end)
        return std::unexpected(parse_error::invalid_input);
    else if (std::isinf(retval))
        return std::unexpected(parse_error::overflow);
 
    str.remove_prefix(end - begin);
    return retval;
}
 
int main()
{
    auto process = [](std::string_view str)
    {
        std::cout << "str: " << std::quoted(str) << ", ";
        if (const auto num = parse_number(str); num.has_value())
            std::cout << "value: " << *num << '\n';
            // If num did not have a value, dereferencing num
            // would cause an undefined behavior, and
            // num.value() would throw std::bad_expected_access.
            // num.value_or(123) uses specified default value 123.
        else if (num.error() == parse_error::invalid_input)
            std::cout << "error: invalid input\n";
        else if (num.error() == parse_error::overflow)
            std::cout << "error: overflow\n";
        else
            std::cout << "unexpected!\n"; // or invoke std::unreachable();
    };
 
    for (auto src : {"42", "42abc", "meow", "inf"})
        process(src);
}

执行结果如下:

shell 复制代码
str: "42", value: 42
str: "42abc", value: 42
str: "meow", error: invalid input
str: "inf", error: overflow

油管中TheCherno的一个视频C++ FINALLY Improved Error Handling with std::expected!对于std::exepected讲解得不错,感兴趣的可以去看看。

  • 0:00 - Quick look at std::expected
  • 3:30 - Life before std::expected
  • 8:07 - Using std::expected for error handling
  • 12:25 - Some more useful features of std::expected
  • 17:06 - More advanced use case (file reading)

关于std::expected的代码示例1

cpp 复制代码
#include <iostream>
#include <print>
#include <expected>

// https://en.cppreference.com/w/cpp/utility/expected

// https://en.cppreference.com/w/cpp/utility/expected/expected

// Example of using std::expected in C++23
// This example demonstrates how to use std::expected to handle errors gracefully.
std::expected<int, std::string> divide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::unexpected("Division by zero error");
    }
    return numerator / denominator;
}

int main()
{
    int a = 10;
    int b = 0;

    // Attempt to divide and handle the result
    auto result = divide(a, b);
    
    if (result) {
        std::println("Result: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }

    // Example with valid division
    b = 2;
    result = divide(a, b);
    
    if (result) {
        std::println("Result: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }

    return 0;
}

运行结果如下:

shell 复制代码
Error: Division by zero error
Result: 5

关于std::expected的代码示例2-支持链式调用

cpp 复制代码
#include <iostream>
#include <print>
#include <expected>

// https://en.cppreference.com/w/cpp/utility/expected

// 这是一个示例程序,演示如何使用 std::expected 进行错误处理。
std::expected<int, std::string> divide(int numerator, int denominator) {
    if (denominator == 0) {
        return std::unexpected("Division by zero error");
        return 0; // Return a default value
    }
    return numerator / denominator;
}

void test_001()
{
    auto result = divide(10, 2);

    if (result) {
        std::println("Result: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }
}

int test_002()
{
    auto result = divide(12, 3);
    result = result
        .and_then([](int value) { return divide(value, 0); })
        .or_else([](const std::string& error) {
            std::println("Error occurred: {}", error);
            return std::expected<int, std::string>{0};
        });

    if (result) {
        std::println("Final Result: {}", *result);
    }
}

int main()
{
    // 测试 std::expected 的基本用法
    test_001();

    // 测试 std::expected 的链式调用和错误处理
    test_002();

    return 0;
}

运行结果如下:

shell 复制代码
esult: 5
Error occurred: Division by zero error
Final Result: 0

关于std::expected的代码示例3

cpp 复制代码
#include <iostream>
#include <expected>
#include <string>
#include <print>

// https://en.cppreference.com/w/cpp/utility/expected

// Example of using std::expected in C++23
// This example demonstrates how to use std::expected to handle errors gracefully.
// 定义一个可能返回int或者字符串错误的expected类型
std::expected<int, std::string> parse_number(const std::string& str) {
    try {
        // 尝试将字符串转换为整数
        return std::stoi(str);
    } catch (const std::invalid_argument&) {
        // 如果转换失败,返回一个错误信息
        return std::unexpected("Invalid number format");
    } catch (const std::out_of_range&) {
        // 如果数字超出范围,返回一个错误信息
        return std::unexpected("Number out of range");
    }
}

int main()
{
    auto result = parse_number("123");
    if (result.has_value()) {
        std::println("Parsed number: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }

    result = parse_number("abc");
    if (result.has_value()) {
        std::println("Parsed number: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }

    result = parse_number("12345678901234567890");
    if (result.has_value()) {
        std::println("Parsed number: {}", *result);
    } else {
        std::println("Error: {}", result.error());
    }

    return 0;
}

运行结果如下:

shell 复制代码
Parsed number: 123
Error: Invalid number format
Error: Number out of range 

总结

C++23引入的std::expected为函数返回结果的处理提供了一种更加优雅、类型安全的解决方案。它解决了传统错误处理方法中的一些痛点,如类型安全问题、代码可读性问题和性能开销问题等。通过使用std::expected,开发者可以编写出更加健壮、可维护的代码。在实际开发中,建议开发者积极采用std::expected来处理函数的返回结果,特别是在对性能和代码质量有较高要求的场景中。当然,注意:如果在c++23标准之前的老项目,可能就不支持std::expected这种新特性了。

参考资料

相关推荐
TalkU浩克21 分钟前
C++中使用Essentia实现STFT/ISTFT
开发语言·c++·音频·istft·stft·essentia
awonw34 分钟前
[python][flask]flask静态资源
开发语言·python·flask
Chef_Chen41 分钟前
从0开始学习R语言--Day57--SCAD模型
开发语言·学习·r语言
医工交叉实验工坊1 小时前
R 语言绘制六种精美热图:转录组数据可视化实践(基于 pheatmap 包)
开发语言·信息可视化·r语言
小关会打代码1 小时前
Python编程进阶知识之第五课处理数据(matplotlib)
开发语言·python·机器学习·matplotlib·绘图
小比卡丘1 小时前
【C++进阶】第7课—红黑树
java·开发语言·c++
超浪的晨1 小时前
Java 单元测试详解:从入门到实战,彻底掌握 JUnit 5 + Mockito + Spring Boot 测试技巧
java·开发语言·后端·学习·单元测试·个人开发
不断努力的根号七1 小时前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt
赵英英俊2 小时前
Python day24
开发语言·python
Harbor Lau2 小时前
多线程插入保证事务的一致性,亲测可用方式一实测
java·开发语言