Skip to content
Merged
13 changes: 13 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,23 @@ if(YSTDLIB_CPP_IS_TOP_LEVEL)
# installed, this file is not necessary.
include(build/deps/settings.cmake OPTIONAL)

# Use ystdlib-cpp's own path since there's no parent project.
# Note: the ending slash is needed, otherwise it gets appended to relative paths and make them
# into root paths, which is incorrect.
string(LENGTH "${CMAKE_CURRENT_LIST_DIR}/" YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE)

# If previously undefined, `BUILD_TESTING` will be set to ON.
include(CTest)
endif()

if(DEFINED YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE)
# Macro providing the length of the absolute source directory path of the project using
# ystdlib-cpp so we can output file name information with relative (rather than absolute) paths.
add_definitions(
"-DYSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE=${YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE}"
)
endif()

if(BUILD_TESTING AND YSTDLIB_CPP_BUILD_TESTING)
set(YSTDLIB_CPP_ENABLE_TESTS ON)
endif()
Expand Down
3 changes: 3 additions & 0 deletions src/ystdlib/error_handling/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ cpp_library(
NAMESPACE ystdlib
PUBLIC_HEADERS
ErrorCode.hpp
TraceableException.hpp
SourceLocation.hpp
TESTS_SOURCES
test/constants.hpp
test/test_ErrorCode.cpp
test/test_TraceableException.cpp
test/types.cpp
test/types.hpp
)
41 changes: 41 additions & 0 deletions src/ystdlib/error_handling/SourceLocation.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP
#define YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP

#include <iostream>
#include <source_location>
#include <sstream>
#include <string>

namespace ystdlib::error_handling {
class SourceLocation : public std::source_location {
public:
// Constructor
explicit SourceLocation(std::source_location const& where)
: std::source_location{where},
m_formatted_location{format_location(*this)} {}

#ifdef YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE
[[nodiscard]] auto file_name() const noexcept -> char const* {
Comment thread
Bill-hbrhbr marked this conversation as resolved.
Outdated
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
return std::source_location::file_name() + YSTDLIB_CPP_PROJECT_SOURCE_PATH_SIZE;
Comment thread
Bill-hbrhbr marked this conversation as resolved.
Outdated
}
#endif
Comment thread
Bill-hbrhbr marked this conversation as resolved.
Outdated

[[nodiscard]] auto str() const noexcept -> std::string const& { return m_formatted_location; }

friend auto operator<<(std::ostream& os, SourceLocation const& where) -> std::ostream& {
return os << where.str();
}

private:
static auto format_location(SourceLocation const& where) -> std::string {
std::ostringstream oss;
oss << where.file_name() << "(" << where.line() << ":" << where.column() << "), function `"
<< where.function_name() << "`";
return oss.str();
}

std::string m_formatted_location;
};
} // namespace ystdlib::error_handling
#endif // YSTDLIB_ERROR_HANDLING_SOURCELOCATION_HPP
84 changes: 84 additions & 0 deletions src/ystdlib/error_handling/TraceableException.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
#define YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP

#include <concepts>
#include <exception>
#include <source_location>
#include <string>
#include <system_error>
#include <utility>

#include "SourceLocation.hpp"

namespace ystdlib::error_handling {
/**
* Concept that defines a template parameter of an integer-based error code enumeration.
* @tparam Type
*/
template <typename Type>
concept ErrorCodeType
= std::same_as<Type, std::error_code> || std::convertible_to<Type, std::error_code>;

/**
* An exception class that is thrown with an `std::error_code`.
*
* This class extends `std::exception` and can be thrown with an `std::error_code` argument. It also
* provides additional information to aid in debugging by storing details in `std::source_location`,
* including the function name, file name, and line number of the throwing location.
*
* @see std::source_location::file_name()
* @see std::source_location::function_name()
* @see std::source_location::line()
*/
template <typename ErrorCodeType>
class TraceableException : public std::exception {
public:
// Constructors
explicit TraceableException(
ErrorCodeType error_code,
std::source_location const& where = std::source_location::current()
)
: m_error_code{std::move(error_code)},
m_where{where} {
m_what = m_where.str();
}

explicit TraceableException(
ErrorCodeType error_code,
std::string what,
Comment thread
davidlion marked this conversation as resolved.
Outdated
std::source_location const& where = std::source_location::current()
)
: m_error_code{std::move(error_code)},
m_what{std::move(what)},
m_where{where} {}

// Methods implementing std::exception
[[nodiscard]] auto what() const noexcept -> char const* override { return m_what.c_str(); }

// Methods
[[nodiscard]] auto error_code() const -> ErrorCodeType { return m_error_code; }

[[nodiscard]] auto what() -> std::string& { return m_what; }

[[nodiscard]] auto where() const noexcept -> SourceLocation const& { return m_where; }

private:
// Variables
ErrorCodeType m_error_code;
std::string m_what;
SourceLocation m_where;
};

} // namespace ystdlib::error_handling

/**
* The macro to define a `TraceableException` class with the given class name T.
*/
// NOLINTBEGIN(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
Comment thread
Bill-hbrhbr marked this conversation as resolved.
Outdated
#define YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(T, E) \
class T : public ystdlib::error_handling::TraceableException<E> { \
using ystdlib::error_handling::TraceableException<E>::TraceableException; \
}
// NOLINTEND(bugprone-macro-parentheses, cppcoreguidelines-macro-usage)
Comment thread
Bill-hbrhbr marked this conversation as resolved.
Outdated

#endif // YSTDLIB_ERROR_HANDLING_TRACEABLEEXCEPTION_HPP
68 changes: 68 additions & 0 deletions src/ystdlib/error_handling/test/test_TraceableException.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#include <cassert>
#include <cstring>

#include <ystdlib/error_handling/TraceableException.hpp>

#include <catch2/catch_test_macros.hpp>

#include "types.hpp"

using ystdlib::error_handling::ErrorCodeType;
using ystdlib::error_handling::TraceableException;

namespace {
constexpr auto cCustomFailureDescription{"This operation failed due to invalid args."};
constexpr auto cCurrentFileName{"src/ystdlib/error_handling/test/test_TraceableException.cpp"};
constexpr auto cSuccessFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_success()"
};
constexpr auto cFailureFuncName{
"static void ystdlib::error_handling::test::Worker::execute_with_failure()"
};
} // namespace

namespace ystdlib::error_handling::test {
class Worker {
public:
YSTDLIB_ERROR_HANDLING_DEFINE_TRACEABLE_EXCEPTION(OperationFailed, BinaryErrorCode);

static auto execute_with_success() -> void {
throw OperationFailed(BinaryErrorCode{BinaryErrorCodeEnum::Success});
}

static auto execute_with_failure() -> void {
throw OperationFailed(
BinaryErrorCode{BinaryErrorCodeEnum::Failure},
cCustomFailureDescription
);
}
};
} // namespace ystdlib::error_handling::test

namespace {
template <ErrorCodeType E, typename Callable>
[[nodiscard]] auto capture_exception(Callable&& f) -> TraceableException<E>;

template <ErrorCodeType E, typename Callable>
auto capture_exception(Callable&& f) -> TraceableException<E> {
try {
std::forward<Callable>(f)();
} catch (TraceableException<E>& e) {
return e;
}
assert(false && "The function is expected to throw.");
}
Comment on lines +41 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety with template constraints.
The capture_exception template returns a TraceableException and expects that exact type to be thrown. If a different type of exception is actually thrown, the code falls through to assert(false). Consider constraining your template to accept callables that are guaranteed to throw TraceableException or handle unexpected exception types more gracefully.

} // namespace

namespace ystdlib::error_handling::test {
TEST_CASE("test_traceable_exception", "[error_handling][TraceableException]") {
auto const ex_success{capture_exception<BinaryErrorCode>(Worker::execute_with_success)};
REQUIRE((0 == std::strcmp(ex_success.where().file_name(), cCurrentFileName)));
REQUIRE((0 == std::strcmp(ex_success.where().function_name(), cSuccessFuncName)));

auto const ex_failure{capture_exception<BinaryErrorCode>(Worker::execute_with_failure)};
REQUIRE((0 == std::strcmp(ex_failure.what(), cCustomFailureDescription)));
REQUIRE((0 == std::strcmp(ex_failure.where().file_name(), cCurrentFileName)));
REQUIRE((0 == std::strcmp(ex_failure.where().function_name(), cFailureFuncName)));
}
} // namespace ystdlib::error_handling::test