Skip to content

Commit f1ca6df

Browse files
feat(error_handling): Add Result<T, E> alias and macros to enable ergonomic Rust-style error handling. (#47)
Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com>
1 parent f65e98b commit f1ca6df

3 files changed

Lines changed: 209 additions & 0 deletions

File tree

src/ystdlib/error_handling/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ cpp_library(
44
PUBLIC_HEADERS
55
ErrorCode.hpp
66
TraceableException.hpp
7+
Result.hpp
78
utils.hpp
9+
PUBLIC_LINK_LIBRARIES
10+
outcome::hl
811
TESTS_SOURCES
912
test/constants.hpp
1013
test/test_ErrorCode.cpp
14+
test/test_Result.cpp
1115
test/test_TraceableException.cpp
1216
test/types.cpp
1317
test/types.hpp
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#ifndef YSTDLIB_ERROR_HANDLING_RESULT_HPP
2+
#define YSTDLIB_ERROR_HANDLING_RESULT_HPP
3+
4+
#include <system_error>
5+
6+
#include <outcome/config.hpp>
7+
#include <outcome/std_result.hpp>
8+
#include <outcome/success_failure.hpp>
9+
#include <outcome/try.hpp>
10+
11+
namespace ystdlib::error_handling {
12+
/**
13+
* A Rust-style `Result<T, E>` type for standardized, exception-free error handling.
14+
*
15+
* This alias standardizes error handling across the codebase by defaulting the error type to
16+
* `std::error_code`, which interoperates with the `ystdlib::error_handling::ErrorCode`, making it
17+
* easier to compose errors and propagate them across different modules and libraries.
18+
*
19+
* @tparam ReturnType The type returned on success.
20+
* @tparam ErrorType The type used to represent errors.
21+
*/
22+
template <typename ReturnType, typename ErrorType = std::error_code>
23+
using Result = OUTCOME_V2_NAMESPACE::std_result<ReturnType, ErrorType>;
24+
25+
/**
26+
* @return A value indicating successful completion of a function that returns a void result (i.e.,
27+
* `Result<void, E>`).
28+
*/
29+
[[nodiscard]] inline auto success() -> OUTCOME_V2_NAMESPACE::success_type<void> {
30+
return OUTCOME_V2_NAMESPACE::success();
31+
}
32+
33+
/**
34+
* A function-style macro that emulates Rust’s try (`?`) operator for error propagation.
35+
*
36+
* @param expr An expression that evaluates to a `Result` object.
37+
*
38+
* Behavior:
39+
* - If `expr` represents an error (i.e., `expr.has_error()` returns true), the macro performs an
40+
* early return from the enclosing function with the contained error.
41+
* - Otherwise, it unwraps and yields the successful value as an rvalue reference (`expr.value()`).
42+
*
43+
* NOTE: This macro is only supported on GCC and Clang due to reliance on compiler-specific
44+
* extensions.
45+
*/
46+
#ifdef OUTCOME_TRYX
47+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
48+
#define YSTDLIB_ERROR_HANDLING_TRYX(expr) OUTCOME_TRYX(expr)
49+
#endif
50+
51+
/**
52+
* A function-style macro for propagating errors from expressions that evaluate to a void result
53+
* (`Result<void, E>`).
54+
*
55+
* @param expr An expression that evaluates to a `Result<void, E>` object.
56+
*
57+
* Behavior:
58+
* - If `expr` represents an error (i.e., `expr.has_error()` returns true), the macro performs an
59+
* early return from the enclosing function with the contained error.
60+
* - Otherwise, execution continues normally.
61+
*/
62+
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
63+
#define YSTDLIB_ERROR_HANDLING_TRYV(expr) OUTCOME_TRYV(expr)
64+
} // namespace ystdlib::error_handling
65+
66+
#endif // YSTDLIB_ERROR_HANDLING_RESULT_HPP
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#include <memory>
2+
#include <system_error>
3+
#include <type_traits>
4+
5+
#include <ystdlib/error_handling/Result.hpp>
6+
7+
#include <catch2/catch_test_macros.hpp>
8+
9+
#include "types.hpp"
10+
11+
using ystdlib::error_handling::Result;
12+
using ystdlib::error_handling::success;
13+
using ystdlib::error_handling::test::AlwaysSuccessErrorCode;
14+
using ystdlib::error_handling::test::AlwaysSuccessErrorCodeEnum;
15+
using ystdlib::error_handling::test::BinaryErrorCode;
16+
using ystdlib::error_handling::test::BinaryErrorCodeEnum;
17+
18+
namespace {
19+
constexpr int cTestInt{123};
20+
constexpr auto cVoidFunc = [](bool is_error) -> Result<void> {
21+
if (is_error) {
22+
return BinaryErrorCode{BinaryErrorCodeEnum::Failure};
23+
}
24+
return success();
25+
};
26+
constexpr auto cIntFunc = [](bool is_error) -> Result<int> {
27+
if (is_error) {
28+
return std::errc::bad_message;
29+
}
30+
return cTestInt;
31+
};
32+
constexpr auto cUniquePtrFunc = [](bool is_error) -> Result<std::unique_ptr<int>> {
33+
if (is_error) {
34+
return AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success};
35+
}
36+
return std::make_unique<int>(cTestInt);
37+
};
38+
} // namespace
39+
40+
namespace ystdlib::error_handling::test {
41+
TEST_CASE("test_result_void", "[error_handling][Result]") {
42+
auto const result_no_error{cVoidFunc(false)};
43+
REQUIRE_FALSE(result_no_error.has_error());
44+
REQUIRE(std::is_void_v<decltype(result_no_error.value())>);
45+
46+
auto const result_has_error{cVoidFunc(true)};
47+
REQUIRE(result_has_error.has_error());
48+
REQUIRE(BinaryErrorCode{BinaryErrorCodeEnum::Failure} == result_has_error.error());
49+
}
50+
51+
TEST_CASE("test_result_void_in_main", "[error_handling][Result]") {
52+
auto main_func = [&](bool is_error) -> Result<void> {
53+
YSTDLIB_ERROR_HANDLING_TRYV(cVoidFunc(is_error));
54+
return success();
55+
};
56+
auto const main_no_error{main_func(false)};
57+
REQUIRE_FALSE(main_no_error.has_error());
58+
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);
59+
60+
auto const main_has_error{main_func(true)};
61+
REQUIRE(main_has_error.has_error());
62+
REQUIRE(BinaryErrorCode{BinaryErrorCodeEnum::Failure} == main_has_error.error());
63+
}
64+
65+
TEST_CASE("test_result_int", "[error_handling][Result]") {
66+
auto const result_no_error{cIntFunc(false)};
67+
REQUIRE_FALSE(result_no_error.has_error());
68+
REQUIRE(cTestInt == result_no_error.value());
69+
70+
auto const result_has_error{cIntFunc(true)};
71+
REQUIRE(result_has_error.has_error());
72+
REQUIRE(std::errc::bad_message == result_has_error.error());
73+
}
74+
75+
TEST_CASE("test_result_int_in_main", "[error_handling][Result]") {
76+
auto main_func = [&](bool is_error) -> Result<void> {
77+
YSTDLIB_ERROR_HANDLING_TRYV(cIntFunc(is_error));
78+
return success();
79+
};
80+
auto const main_no_error{main_func(false)};
81+
REQUIRE_FALSE(main_no_error.has_error());
82+
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);
83+
84+
auto const main_has_error{main_func(true)};
85+
REQUIRE(main_has_error.has_error());
86+
REQUIRE(std::errc::bad_message == main_has_error.error());
87+
}
88+
89+
TEST_CASE("test_result_int_propagate", "[error_handling][Result]") {
90+
auto main_func = [&](bool is_error) -> Result<int> {
91+
return YSTDLIB_ERROR_HANDLING_TRYX(cIntFunc(is_error));
92+
};
93+
auto const main_no_error{main_func(false)};
94+
REQUIRE_FALSE(main_no_error.has_error());
95+
REQUIRE(cTestInt == main_no_error.value());
96+
97+
auto const main_has_error{main_func(true)};
98+
REQUIRE(main_has_error.has_error());
99+
REQUIRE(std::errc::bad_message == main_has_error.error());
100+
}
101+
102+
TEST_CASE("test_result_unique_ptr", "[error_handling][Result]") {
103+
auto const result_no_error{cUniquePtrFunc(false)};
104+
REQUIRE_FALSE(result_no_error.has_error());
105+
REQUIRE(cTestInt == *(result_no_error.value()));
106+
107+
auto const result_has_error{cUniquePtrFunc(true)};
108+
REQUIRE(result_has_error.has_error());
109+
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == result_has_error.error()
110+
);
111+
}
112+
113+
TEST_CASE("test_result_unique_ptr_in_main", "[error_handling][Result]") {
114+
auto main_func = [&](bool is_error) -> Result<void> {
115+
YSTDLIB_ERROR_HANDLING_TRYV(cUniquePtrFunc(is_error));
116+
return success();
117+
};
118+
auto const main_no_error{main_func(false)};
119+
REQUIRE_FALSE(main_no_error.has_error());
120+
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);
121+
122+
auto const main_has_error{main_func(true)};
123+
REQUIRE(main_has_error.has_error());
124+
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == main_has_error.error());
125+
}
126+
127+
TEST_CASE("test_result_unique_ptr_propagate", "[error_handling][Result]") {
128+
auto main_func = [&](bool is_error) -> Result<std::unique_ptr<int>> {
129+
return YSTDLIB_ERROR_HANDLING_TRYX(cUniquePtrFunc(is_error));
130+
};
131+
auto const main_no_error{main_func(false)};
132+
REQUIRE_FALSE(main_no_error.has_error());
133+
REQUIRE(cTestInt == *(main_no_error.value()));
134+
135+
auto const main_has_error{main_func(true)};
136+
REQUIRE(main_has_error.has_error());
137+
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == main_has_error.error());
138+
}
139+
} // namespace ystdlib::error_handling::test

0 commit comments

Comments
 (0)