diff --git a/error/v2/CMakeLists.txt b/error/v2/CMakeLists.txt new file mode 100644 index 0000000..4680c38 --- /dev/null +++ b/error/v2/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.18) + +project(error LANGUAGES CXX) + +# Import third-party dependencies. +find_package(GTest REQUIRED CONFIG) +find_package(tl-expected REQUIRED CONFIG) + +# Enable some compiler warnings (supported by gcc & clang). +set(warnings + -Wall + -Wconversion + -Werror + -Wextra + -Wformat=2 + -Wold-style-cast + -Woverloaded-virtual + -Wshadow + -Wsign-conversion + -Wuninitialized + -Wunused + ) +string(REPLACE ";" " " warnings "${warnings}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${warnings}") + +add_library(error SHARED) +add_library(caesar::error ALIAS error) + +# Require C++17. +target_compile_features(error PUBLIC cxx_std_17) + +# Add sources. +set(sources + caesar/error/domain_error.cpp + caesar/error/error_code.cpp + caesar/error/error.cpp + caesar/error/expected.cpp + caesar/error/out_of_range.cpp + ) +target_sources(error PRIVATE ${sources}) + +# Add include dirs. +target_include_directories( + error + PUBLIC + $ + ) + +# Link to imported targets. +target_link_libraries( + error + PUBLIC + tl::expected + ) + +# Add test sources. +set(tests + test/error_code_test.cpp + test/error_test.cpp + test/expected_test.cpp + ) + +add_executable(error-test ${tests}) + +target_link_libraries( + error-test + PRIVATE + caesar::error + GTest::gmock_main + ) + +# Register tests with CTest. +enable_testing() +include(GoogleTest) +gtest_discover_tests(error-test) diff --git a/error/v2/caesar/error.hpp b/error/v2/caesar/error.hpp new file mode 100644 index 0000000..39da6e4 --- /dev/null +++ b/error/v2/caesar/error.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "error/domain_error.hpp" +#include "error/error_code.hpp" +#include "error/error.hpp" +#include "error/expected.hpp" +#include "error/out_of_range.hpp" diff --git a/error/v2/caesar/error/domain_error.cpp b/error/v2/caesar/error/domain_error.cpp new file mode 100644 index 0000000..eb23195 --- /dev/null +++ b/error/v2/caesar/error/domain_error.cpp @@ -0,0 +1,17 @@ +#include "domain_error.hpp" + +namespace caesar { + +const char* get_error_category(DomainError) noexcept { return "DomainError"; } + +const char* +get_error_string(DomainError e) noexcept +{ + switch (e) { + case DomainError::DivisionByZero: return "Division by zero"; + } + + return ""; +} + +} // namespace caesar diff --git a/error/v2/caesar/error/domain_error.hpp b/error/v2/caesar/error/domain_error.hpp new file mode 100644 index 0000000..d401ef2 --- /dev/null +++ b/error/v2/caesar/error/domain_error.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace caesar { + +/** + * Error code used to indicate domain errors, i.e. situations where the inputs + * are outside of the domain on which an operation is defined. + * + * \see ErrorCode + */ +enum class DomainError { + DivisionByZero = 1, +}; + +/** \private implements ErrorCode::category() */ +const char* +get_error_category(DomainError e) noexcept; + +/** \private implements ErrorCode::description() */ +const char* +get_error_string(DomainError e) noexcept; + +} // namespace caesar diff --git a/error/v2/caesar/error/error.cpp b/error/v2/caesar/error/error.cpp new file mode 100644 index 0000000..ad01126 --- /dev/null +++ b/error/v2/caesar/error/error.cpp @@ -0,0 +1 @@ +#include "error.hpp" diff --git a/error/v2/caesar/error/error.hpp b/error/v2/caesar/error/error.hpp new file mode 100644 index 0000000..144c162 --- /dev/null +++ b/error/v2/caesar/error/error.hpp @@ -0,0 +1,105 @@ +#pragma once + +#include "error_code.hpp" + +#include +#include + +namespace caesar { + +/** + * Describes an error encountered during processing + * + * The Error class stores an error code along with contextual information about + * where the error originated from in the source code (filename and line + * number). When constructed from an ErrorCode (or any supported error code + * enumeration type), by default, the source location information is populated + * based on the call site where the Error object was constructed. + * + * \see ErrorCode + * \see Expected + */ +class Error { + using source_location = std::experimental::source_location; + +public: + /** + * Construct a new Error object. + * + * \param[in] error_code error code + * \param[in] file source code filename where the error occurred + * \param[in] line source code line number where the error occurred + */ + constexpr Error(const ErrorCode& error_code, + const char* file, + int line) noexcept + : error_code_(error_code), file_(file), line_(line) + {} + + /** + * Construct a new Error object. + * + * The source location defaults to the call site where the Error object was + * constructed. + * + * \param[in] error_code error code + * \param[in] origin source code location where the error occurred + */ + constexpr Error( + const ErrorCode& error_code, + const source_location& origin = source_location::current()) noexcept + : Error(error_code, origin.file_name(), static_cast(origin.line())) + {} + + /** + * Construct a new Error object. + * + * The source location defaults to the call site where the Error object was + * constructed. + * + * This overload participates in overload resolution only if + * `std::is_constructible_v == true`. + * + * \tparam ErrorCodeEnum + * an error code enumeration type supported by ErrorCode + * + * \param[in] e error code enumeration object + * \param[in] origin source code location where the error occurred + */ + template>> + constexpr Error( + ErrorCodeEnum e, + const source_location& origin = source_location::current()) noexcept + : Error(ErrorCode(e), origin) + {} + + /** Return the error code. */ + constexpr const ErrorCode& + error_code() const noexcept + { + return error_code_; + } + + /** Return the source code filename where the error occurred. */ + constexpr const char* + file() const noexcept + { + return file_; + } + + /** Return the source code line number where the error occurred. */ + constexpr int + line() const noexcept + { + return line_; + } + +private: + ErrorCode error_code_; + const char* file_; + int line_; +}; + +} // namespace caesar diff --git a/error/v2/caesar/error/error_code.cpp b/error/v2/caesar/error/error_code.cpp new file mode 100644 index 0000000..c645fc2 --- /dev/null +++ b/error/v2/caesar/error/error_code.cpp @@ -0,0 +1 @@ +#include "error_code.hpp" diff --git a/error/v2/caesar/error/error_code.hpp b/error/v2/caesar/error/error_code.hpp new file mode 100644 index 0000000..7b8c01b --- /dev/null +++ b/error/v2/caesar/error/error_code.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include "domain_error.hpp" +#include "out_of_range.hpp" + +#include + +namespace caesar { + +/** + * A type-erased error code + * + * ErrorCode facilitates interoperability between any number of domain-specific + * error codes that an application and its third-party dependencies may + * need to support. It does so by providing a common type that can be used to + * represent error code enumerations of different types. + * + * Unlike + * [`std::error_code`](https://en.cppreference.com/w/cpp/error/error_code), + * ErrorCode uses a variant/visitor idiom rather than dynamic polymorphism to + * implement type erasure. This makes extending ErrorCode more intrusive (you + * need to actually modify the class definition in order to support additional + * error code enum types). However, ErrorCode boasts a number of beneficial + * properties compared to `std::error_code`: + * + * - ErrorCode objects can be constructed in device code and can be safely + * passed between the host and device + * - Supporting custom error codes requires significantly less boilerplate + * - ErrorCode is a *LiteralType* and most operations are `constexpr` + * + * ### Example + * + * ```c++ + * ErrorCode ec = DomainError::DivisionByZero; + * + * std::cout << ec.category() << "\n"; // "DomainError" + * std::cout << ec.description() << "\n"; // "Division by zero" + * ``` + * + * \see Error + */ +class ErrorCode : private std::variant { + using Base = std::variant; + +public: + /** Integral type that can be used to represent error code values */ + using value_type = int; + + using Base::Base; + using Base::operator=; + + ErrorCode() = delete; + + /** Return the error code value. */ + constexpr value_type + value() const noexcept + { + return std::visit([](auto arg) { return static_cast(arg); }, + base()); + } + + /** Return the associated error category name. */ + const char* + category() const noexcept + { + return std::visit([](auto arg) { return get_error_category(arg); }, + base()); + } + + /** Return a message describing the error code. */ + const char* + description() const noexcept + { + return std::visit([](auto arg) { return get_error_string(arg); }, + base()); + } + + /** Compare two ErrorCode objects */ + friend constexpr bool + operator==(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return lhs.base() == rhs.base(); + } + + /** \copydoc operator==(const ErrorCode&, const ErrorCode&) */ + friend constexpr bool + operator!=(const ErrorCode& lhs, const ErrorCode& rhs) noexcept + { + return not(lhs == rhs); + } + +private: + constexpr const Base& + base() const noexcept + { + return static_cast(*this); + } +}; + +} // namespace caesar diff --git a/error/v2/caesar/error/expected.cpp b/error/v2/caesar/error/expected.cpp new file mode 100644 index 0000000..869d274 --- /dev/null +++ b/error/v2/caesar/error/expected.cpp @@ -0,0 +1 @@ +#include "expected.hpp" diff --git a/error/v2/caesar/error/expected.hpp b/error/v2/caesar/error/expected.hpp new file mode 100644 index 0000000..47705ed --- /dev/null +++ b/error/v2/caesar/error/expected.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include "error.hpp" + +#include +#include + +namespace caesar { + +/** + * A wrapper that may contain an object of type T or an error + * + * The Expected class is intended to be used as a return type for operations + * that may fail. On success, the returned object contains the expected result. + * In case of failure, it instead contains an object that describes the error + * encountered. + * + * Compared to the use of exceptions, the Expected approach + * + * - can be used in device code and can be safely and easily propagated across + * thread boundaries + * - allows for inexpensive local handling of the "bad path" when an operation + * fails to return the expected value + * - improves error visibility in code review by making the possibility of error + * explicit in a function's return type + * + * Compared to error codes, Expected objects + * + * - do not monopolize the return channel + * - are not easily ignored (if the user wants to retrieve the contained value) + * + * The Expected class provides methods for safe and unsafe access to the + * underlying value. If an instance of the expected type was not stored, + * attempting to access it via the `Expected::value()` method causes an + * exception to be thrown. The indirection operators (`Expected::operator*()` + * and `Expected::operator->()`), however, provide unchecked access to the + * stored value. The behavior of these methods is undefined if the expected + * value is not present. (Similarly, attempting to access the error object via + * `Expected::error()` has undefined behavior if a value was stored instead.) + * + * When an Expected object is contextually converted to `bool`, the conversion + * returns `true` if the object contains a value and `false` if the object + * contains an error. + * + * Expected may not store a reference type. + * + * \tparam the expected value type + * + * ### Example + * + * ```c++ + * Expected safe_divide(int x, int y) + * { + * if (y == 0) { + * return DomainError::DivisionByZero; + * } + * return x / y; + * } + * + * auto res1 = safe_divide(6, 3); + * auto res2 = safe_divide(6, 0); + * + * std::cout << std::boolalpha << bool(res1) << "\n"; + * std::cout << std::boolalpha << bool(res2) << "\n"; + * + * std::cout << res1.value() << "\n"; + * std::cout << res2.error().error_code().description() << "\n"; + * + * // std::cout << res2.value() << "\n"; // causes an exception to be thrown + * ``` + * + * Possible output: + * + * ``` + * true + * false + * 2 + * Division by zero + * ``` + * + * \see Error + */ +template +class Expected : public tl::expected { + using Base = tl::expected; + using source_location = std::experimental::source_location; + +public: + using Base::Base; + using Base::operator=; + + Expected() = delete; + + /** Construct a new Expected object containing an error. */ + constexpr Expected(const Error& error) noexcept + : Base(tl::unexpected(error)) + {} + + /** + * Construct a new Expected object containing an error. + * + * The source location defaults to the call site where the Expected object + * was constructed. + * + * \param[in] error_code error code + * \param[in] origin source code location where the error occurred + */ + constexpr Expected( + const ErrorCode& error_code, + const source_location& origin = source_location::current()) noexcept + : Expected(Error(error_code, origin)) + {} + + /** + * Construct a new Expected object containing an error. + * + * The source location defaults to the call site where the Expected object + * was constructed. + * + * This overload participates in overload resolution only if + * `std::is_constructible_v == true`. + * + * \tparam ErrorCodeEnum + * an error code enumeration type supported by ErrorCode + * + * \param[in] e error code enumeration object + * \param[in] origin source code location where the error occurred + */ + template>> + constexpr Expected( + ErrorCodeEnum e, + const source_location& origin = source_location::current()) noexcept + : Expected(Error(e, origin)) + {} +}; + +} // namespace caesar diff --git a/error/v2/caesar/error/out_of_range.cpp b/error/v2/caesar/error/out_of_range.cpp new file mode 100644 index 0000000..8a3ddf8 --- /dev/null +++ b/error/v2/caesar/error/out_of_range.cpp @@ -0,0 +1,17 @@ +#include "out_of_range.hpp" + +namespace caesar { + +const char* get_error_category(OutOfRange) noexcept { return "OutOfRange"; } + +const char* +get_error_string(OutOfRange e) noexcept +{ + switch (e) { + case OutOfRange::OutOfBoundsAccess: return "Out of bounds access attempted"; + } + + return ""; +} + +} // namespace caesar diff --git a/error/v2/caesar/error/out_of_range.hpp b/error/v2/caesar/error/out_of_range.hpp new file mode 100644 index 0000000..bbf7a4b --- /dev/null +++ b/error/v2/caesar/error/out_of_range.hpp @@ -0,0 +1,23 @@ +#pragma once + +namespace caesar { + +/** + * Error code used to indicate errors that are consequence of attempt to access + * elements out of a defined range. + * + * \see ErrorCode + */ +enum class OutOfRange { + OutOfBoundsAccess = 1, +}; + +/** \private implements ErrorCode::category() */ +const char* +get_error_category(OutOfRange e) noexcept; + +/** \private implements ErrorCode::description() */ +const char* +get_error_string(OutOfRange e) noexcept; + +} // namespace caesar diff --git a/error/v2/test/error_code_test.cpp b/error/v2/test/error_code_test.cpp new file mode 100644 index 0000000..719a94e --- /dev/null +++ b/error/v2/test/error_code_test.cpp @@ -0,0 +1,50 @@ +#include + +#include +#include + +namespace cs = caesar; + +TEST(ErrorCodeTest, FromEnum) +{ + const auto e = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code = e; + + const auto value = static_cast(e); + EXPECT_EQ(error_code.value(), value); + + const std::string category = "DomainError"; + EXPECT_EQ(error_code.category(), category); +} + +TEST(ErrorCodeTest, AssignEnum) +{ + cs::ErrorCode error_code = cs::DomainError::DivisionByZero; + + const auto e = cs::OutOfRange::OutOfBoundsAccess; + error_code = e; + + const auto value = static_cast(e); + EXPECT_EQ(error_code.value(), value); + + const std::string category = "OutOfRange"; + EXPECT_EQ(error_code.category(), category); +} + +TEST(ErrorCodeTest, Description) +{ + const cs::ErrorCode error_code = cs::DomainError::DivisionByZero; + + const std::string description = "Division by zero"; + EXPECT_EQ(error_code.description(), description); +} + +TEST(ErrorCodeTest, Compare) +{ + const cs::ErrorCode error_code1 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code2 = cs::DomainError::DivisionByZero; + const cs::ErrorCode error_code3 = cs::OutOfRange::OutOfBoundsAccess; + + EXPECT_TRUE(error_code1 == error_code2); + EXPECT_TRUE(error_code1 != error_code3); +} diff --git a/error/v2/test/error_test.cpp b/error/v2/test/error_test.cpp new file mode 100644 index 0000000..27d3ae2 --- /dev/null +++ b/error/v2/test/error_test.cpp @@ -0,0 +1,27 @@ +#include + +#include + +namespace cs = caesar; + +TEST(ErrorTest, FromErrorCode) +{ + const auto error_code = cs::DomainError::DivisionByZero; + const cs::Error error = error_code; + + EXPECT_EQ(error.error_code(), error_code); + EXPECT_EQ(error.line(), 10); + EXPECT_THAT(error.file(), testing::EndsWith("error_test.cpp")); +} + +TEST(ErrorTest, AssignErrorCode) +{ + cs::Error error = cs::DomainError::DivisionByZero; + + const auto error_code = cs::OutOfRange::OutOfBoundsAccess; + error = error_code; + + EXPECT_EQ(error.error_code(), error_code); + EXPECT_EQ(error.line(), 22); + EXPECT_THAT(error.file(), testing::EndsWith("error_test.cpp")); +} diff --git a/error/v2/test/expected_test.cpp b/error/v2/test/expected_test.cpp new file mode 100644 index 0000000..64484df --- /dev/null +++ b/error/v2/test/expected_test.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include + +namespace cs = caesar; + +static cs::Expected +safe_divide(int x, int y) +{ + if (y == 0) { + return cs::DomainError::DivisionByZero; + } + return x / y; +} + +TEST(ExpectedTest, HasValue) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result.has_value()); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(ExpectedTest, Truthiness) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_TRUE(result); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_FALSE(result); + } +} + +TEST(ExpectedTest, Value) +{ + { + const auto result = safe_divide(6, 3); + EXPECT_EQ(result.value(), 2); + } + + { + const auto result = safe_divide(1, 0); + EXPECT_THROW({ result.value(); }, std::exception); + } +} + +TEST(ExpectedTest, Dereference) +{ + const auto result = safe_divide(6, 3); + EXPECT_EQ(*result, 2); +} + +TEST(ExpectedTest, DereferenceMember) +{ + struct Foo { + int bar; + }; + + const cs::Expected foo = Foo{123}; + EXPECT_EQ(foo->bar, 123); +} + +TEST(ExpectedTest, Error) +{ + const auto result = safe_divide(1, 0); + const auto error = result.error(); + + EXPECT_EQ(error.error_code(), cs::DomainError::DivisionByZero); + EXPECT_EQ(error.line(), 11); + EXPECT_THAT(error.file(), testing::EndsWith("expected_test.cpp")); +}