Skip to content

feat(error_handling): Add Result<T, E> alias and macros to enable ergonomic Rust-style error handling. #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 8, 2025
4 changes: 4 additions & 0 deletions src/ystdlib/error_handling/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ cpp_library(
PUBLIC_HEADERS
ErrorCode.hpp
TraceableException.hpp
Result.hpp
utils.hpp
PUBLIC_LINK_LIBRARIES
outcome::hl
TESTS_SOURCES
test/constants.hpp
test/test_ErrorCode.cpp
test/test_Result.cpp
test/test_TraceableException.cpp
test/types.cpp
test/types.hpp
Expand Down
66 changes: 66 additions & 0 deletions src/ystdlib/error_handling/Result.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef YSTDLIB_ERROR_HANDLING_RESULT_HPP
#define YSTDLIB_ERROR_HANDLING_RESULT_HPP

#include <system_error>

#include <outcome/config.hpp>
#include <outcome/std_result.hpp>
#include <outcome/success_failure.hpp>
#include <outcome/try.hpp>

namespace ystdlib::error_handling {
/**
* A Rust-style `Result<T, E>` type for standardized, exception-free error handling.
*
* This alias standardizes error handling across the codebase by defaulting the error type to
* `std::error_code`, which interoperates with the `ystdlib::error_handling::ErrorCode`, making it
* easier to compose errors and propagate them across different modules and libraries.
*
* @tparam ReturnType The type returned on success.
* @tparam ErrorType The type used to represent errors.
*/
template <typename ReturnType, typename ErrorType = std::error_code>
using Result = OUTCOME_V2_NAMESPACE::std_result<ReturnType, ErrorType>;

/**
* @return A value indicating successful completion of a function that returns a void result (i.e.,
* `Result<void, E>`).
*/
[[nodiscard]] inline auto success() -> OUTCOME_V2_NAMESPACE::success_type<void> {
return OUTCOME_V2_NAMESPACE::success();
}

/**
* A function-style macro that emulates Rust’s try (`?`) operator for error propagation.
*
* @param expr An expression that evaluates to a `Result` object.
*
* Behavior:
* - If `expr` represents an error (i.e., `expr.has_error()` returns true), the macro performs an
* early return from the enclosing function with the contained error.
* - Otherwise, it unwraps and yields the successful value as an rvalue reference (`expr.value()`).
*
* NOTE: This macro is only supported on GCC and Clang due to reliance on compiler-specific
* extensions.
*/
#ifdef OUTCOME_TRYX
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define YSTDLIB_ERROR_HANDLING_TRYX(expr) OUTCOME_TRYX(expr)
#endif

/**
* A function-style macro for propagating errors from expressions that evaluate to a void result
* (`Result<void, E>`).
*
* @param expr An expression that evaluates to a `Result<void, E>` object.
*
* Behavior:
* - If `expr` represents an error (i.e., `expr.has_error()` returns true), the macro performs an
* early return from the enclosing function with the contained error.
* - Otherwise, execution continues normally.
*/
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define YSTDLIB_ERROR_HANDLING_TRYV(expr) OUTCOME_TRYV(expr)
} // namespace ystdlib::error_handling

#endif // YSTDLIB_ERROR_HANDLING_RESULT_HPP
139 changes: 139 additions & 0 deletions src/ystdlib/error_handling/test/test_Result.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include <memory>
#include <system_error>
#include <type_traits>

#include <ystdlib/error_handling/Result.hpp>

#include <catch2/catch_test_macros.hpp>

#include "types.hpp"

using ystdlib::error_handling::Result;
using ystdlib::error_handling::success;
using ystdlib::error_handling::test::AlwaysSuccessErrorCode;
using ystdlib::error_handling::test::AlwaysSuccessErrorCodeEnum;
using ystdlib::error_handling::test::BinaryErrorCode;
using ystdlib::error_handling::test::BinaryErrorCodeEnum;

namespace {
constexpr int cTestInt{123};
constexpr auto cVoidFunc = [](bool is_error) -> Result<void> {
if (is_error) {
return BinaryErrorCode{BinaryErrorCodeEnum::Failure};
}
return success();
};
constexpr auto cIntFunc = [](bool is_error) -> Result<int> {
if (is_error) {
return std::errc::bad_message;
}
return cTestInt;
};
constexpr auto cUniquePtrFunc = [](bool is_error) -> Result<std::unique_ptr<int>> {
if (is_error) {
return AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success};
}
return std::make_unique<int>(cTestInt);
};
} // namespace

namespace ystdlib::error_handling::test {
TEST_CASE("test_result_void", "[error_handling][Result]") {
auto const result_no_error{cVoidFunc(false)};
REQUIRE_FALSE(result_no_error.has_error());
REQUIRE(std::is_void_v<decltype(result_no_error.value())>);

auto const result_has_error{cVoidFunc(true)};
REQUIRE(result_has_error.has_error());
REQUIRE(BinaryErrorCode{BinaryErrorCodeEnum::Failure} == result_has_error.error());
}

TEST_CASE("test_result_void_in_main", "[error_handling][Result]") {
auto main_func = [&](bool is_error) -> Result<void> {
YSTDLIB_ERROR_HANDLING_TRYV(cVoidFunc(is_error));
return success();
};
auto const main_no_error{main_func(false)};
REQUIRE_FALSE(main_no_error.has_error());
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);

auto const main_has_error{main_func(true)};
REQUIRE(main_has_error.has_error());
REQUIRE(BinaryErrorCode{BinaryErrorCodeEnum::Failure} == main_has_error.error());
}

TEST_CASE("test_result_int", "[error_handling][Result]") {
auto const result_no_error{cIntFunc(false)};
REQUIRE_FALSE(result_no_error.has_error());
REQUIRE(cTestInt == result_no_error.value());

auto const result_has_error{cIntFunc(true)};
REQUIRE(result_has_error.has_error());
REQUIRE(std::errc::bad_message == result_has_error.error());
}

TEST_CASE("test_result_int_in_main", "[error_handling][Result]") {
auto main_func = [&](bool is_error) -> Result<void> {
YSTDLIB_ERROR_HANDLING_TRYV(cIntFunc(is_error));
return success();
};
auto const main_no_error{main_func(false)};
REQUIRE_FALSE(main_no_error.has_error());
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);

auto const main_has_error{main_func(true)};
REQUIRE(main_has_error.has_error());
REQUIRE(std::errc::bad_message == main_has_error.error());
}

TEST_CASE("test_result_int_propagate", "[error_handling][Result]") {
auto main_func = [&](bool is_error) -> Result<int> {
return YSTDLIB_ERROR_HANDLING_TRYX(cIntFunc(is_error));
};
auto const main_no_error{main_func(false)};
REQUIRE_FALSE(main_no_error.has_error());
REQUIRE(cTestInt == main_no_error.value());

auto const main_has_error{main_func(true)};
REQUIRE(main_has_error.has_error());
REQUIRE(std::errc::bad_message == main_has_error.error());
}

TEST_CASE("test_result_unique_ptr", "[error_handling][Result]") {
auto const result_no_error{cUniquePtrFunc(false)};
REQUIRE_FALSE(result_no_error.has_error());
REQUIRE(cTestInt == *(result_no_error.value()));

auto const result_has_error{cUniquePtrFunc(true)};
REQUIRE(result_has_error.has_error());
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == result_has_error.error()
);
}

TEST_CASE("test_result_unique_ptr_in_main", "[error_handling][Result]") {
auto main_func = [&](bool is_error) -> Result<void> {
YSTDLIB_ERROR_HANDLING_TRYV(cUniquePtrFunc(is_error));
return success();
};
auto const main_no_error{main_func(false)};
REQUIRE_FALSE(main_no_error.has_error());
REQUIRE(std::is_void_v<decltype(main_no_error.value())>);

auto const main_has_error{main_func(true)};
REQUIRE(main_has_error.has_error());
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == main_has_error.error());
}

TEST_CASE("test_result_unique_ptr_propagate", "[error_handling][Result]") {
auto main_func = [&](bool is_error) -> Result<std::unique_ptr<int>> {
return YSTDLIB_ERROR_HANDLING_TRYX(cUniquePtrFunc(is_error));
};
auto const main_no_error{main_func(false)};
REQUIRE_FALSE(main_no_error.has_error());
REQUIRE(cTestInt == *(main_no_error.value()));

auto const main_has_error{main_func(true)};
REQUIRE(main_has_error.has_error());
REQUIRE(AlwaysSuccessErrorCode{AlwaysSuccessErrorCodeEnum::Success} == main_has_error.error());
}
} // namespace ystdlib::error_handling::test