Skip to content
Merged
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