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这种新特性了。

参考资料

相关推荐
buyutang_5 分钟前
C/C++ Linux系统编程:线程控制详解,从线程创建到线程终止
linux·c语言·c++·学习
jiaway10 分钟前
【C语言】第一课 环境配置
c语言·开发语言
Qiang_san31 分钟前
GNU Make | C/C++项目自动构建入门
c语言·c++·gnu
小红帽2.01 小时前
从零构建一款开源在线客服系统:我的Go语言实战之旅
开发语言·golang·开源
slim~1 小时前
Java基础第9天总结(可变参数、Collections、斗地主)
java·开发语言
源代码•宸1 小时前
Leetcode—2749. 得到整数零需要执行的最少操作数【中等】(__builtin_popcountl)
c++·经验分享·算法·leetcode·位运算
芒果敲代码1 小时前
单一职责原则(SRP)
c++·单一职责原则
ComputerInBook2 小时前
C++编程语言:标准库:第37章——正则表达式(Bjarne Stroustrup)
开发语言·c++·正则表达式
青草地溪水旁2 小时前
C/C++中的可变参数 (Variadic Arguments)函数机制
c语言·c++·可变参数
A尘埃2 小时前
智能工单路由系统(Java)
java·开发语言·智能工单