C++23中的std::expected:異常處理
眾所周知,C++23以前的異常處理是比較麻煩的,尤其是自己要在可能拋出異常的地方,需要自己去捕獲它,比如除數為0的異常、使用std::stoi函數將字符串轉換成int整型數據、處理文件讀寫的異常等等,不然很容易造成程序終止。
關于C++23中引入的std::expected,這一全新的詞匯表類型,它為函數返回結果的處理提供了一種更加優雅、類型安全的解決方案。
std::expected
是C++23標準庫中的一個模板類,定義于頭文件<expected>
中。它提供了一種方式來表示兩個值之一:類型T
的期望值,或類型E
的非期望值。std::expected
永遠不會是無值的。
// 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 的意外值。預期值永遠不會是無值的。
- 主模板。在其自身的存儲空間中包含預期值或意外值,該存儲空間嵌套在預期對象中。
- void 部分特化。表示預期 void 值或包含意外值。如果包含意外值,則嵌套在預期對象中。
如果程序使用引用類型、函數類型或 std::unexpected 的特化來實例化預期值,則程序格式錯誤。此外,T 不能是 std::in_place_t 或 std::unexpect_t。
模板參數
T - 預期值的類型。該類型必須是(可能為 cv 限定的)void
,或者滿足Destructible
即可析構性要求(特別是,不允許使用數組和引用類型)。
E - 意外值的類型。該類型必須滿足Destructible
要求,并且必須是 std::unexpected 的有效模板參數(特別是,不允許使用數組、非對象類型和 cv 限定的類型)。
示例程序
#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";elsestd::cout << "unexpected!\n"; // or invoke std::unreachable();};for (auto src : {"42", "42abc", "meow", "inf"})process(src);
}
執行結果如下:
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
#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 resultauto result = divide(a, b);if (result) {std::println("Result: {}", *result);} else {std::println("Error: {}", result.error());}// Example with valid divisionb = 2;result = divide(a, b);if (result) {std::println("Result: {}", *result);} else {std::println("Error: {}", result.error());}return 0;
}
運行結果如下:
Error: Division by zero error
Result: 5
關于std::expected
的代碼示例2-支持鏈式調用
#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;
}
運行結果如下:
esult: 5
Error occurred: Division by zero error
Final Result: 0
關于std::expected
的代碼示例3
#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;
}
運行結果如下:
Parsed number: 123
Error: Invalid number format
Error: Number out of range
總結
C++23引入的std::expected
為函數返回結果的處理提供了一種更加優雅、類型安全的解決方案。它解決了傳統錯誤處理方法中的一些痛點,如類型安全問題、代碼可讀性問題和性能開銷問題等。通過使用std::expected
,開發者可以編寫出更加健壯、可維護的代碼。在實際開發中,建議開發者積極采用std::expected
來處理函數的返回結果,特別是在對性能和代碼質量有較高要求的場景中。當然,注意:如果在c++23標準之前的老項目,可能就不支持std::expected
這種新特性了。
參考資料
- https://en.cppreference.com/w/cpp/utility/expected.html
- C++23 std::expected:一種新的詞匯表類型,用于返回函數的結果
- C++23中的新功能之expected和optional