I'm creating a little library for my pet project that is inspired by Boost's outcome namespace. The main reason why I didn't want to use the Boost version is that I don't like the overhead of including something so massive just for something small like this.
Source code:
using u64 = uint64_t;
using i32 = int32_t;
/**
* \brief Base error class
*/
class error {
static inline std::unordered_map<u64, std::string> m_error_templates = {
{ 100, "error occurred while creating 'Hello', today's date is: {:%Y-%m-%d}"},
{ 200, "something messed up in the 'world!' creation process, system has {} threads"}
};
error() = delete;
error(
u64 error_code,
std::string message
) : m_code(error_code),
m_message(std::move(message)) {}
public:
/**
* \brief Emits a new error using the given \a error code.
* \tparam error_code Error code of the error to emit
* \tparam argument_types Argument list types
* \param arguments Argument list that will be passed to the error template
* \return \a Error with the given error code and generated error message.
*/
template<u64 error_code, typename... argument_types>
static error emit(
argument_types&&... arguments
) {
const auto iterator = m_error_templates.find(error_code);
if (iterator == m_error_templates.end()) {
throw std::invalid_argument(std::format("invalid error code used ({})", error_code));
}
return error(error_code, std::vformat(
iterator->second,
std::make_format_args(std::forward<argument_types>(arguments)...)
));
}
/**
* \brief Prints the given error.
*/
void print() const {
std::cout << "error (" << m_code << "): " << m_message << '\n';
}
private:
u64 m_code;
std::string m_message;
};
/**
* \brief Outcome namespace, contains containers for handling and propagating errors.
*/
namespace outcome {
/**
* \brief Base \a failure class, can be emitted with an \a error.
*/
class failure {
public:
failure() = delete;
/**
* \brief Creates a new failure case containing the given \a error.
* \param error Error to use as the reason for failure
*/
failure(
const error& error
) : m_error(error) {}
/**
* \brief Returns the contained error.
*/
const error& get_error() const {
return m_error;
}
private:
error m_error;
};
/**
* \brief Base success class, used for handling success cases with no success return types.
*/
class success {};
/**
* \brief Base result class, contains information about the outcome of an operation.
* \tparam type Type of the successful outcome
*/
template<typename type>
class result {
public:
/**
* \brief Move constructor.
*/
constexpr result(
result&& other
) noexcept {
m_value = other.m_value;
}
/**
* \brief Constructs a result from a failure.
*/
constexpr result(
failure&& failure
) : m_value(std::unexpected(failure.get_error())) {}
/**
* \brief Constructs a result from a success.
*/
constexpr result(
success&& success
) : m_value() {}
/**
* \brief Constructs a result from a given value.
*/
template<typename current_type>
constexpr result(
current_type&& value
) : m_value(std::forward<current_type>(value)) {}
/**
* \brief Checks if the result contains an error.
*/
bool has_error() const {
return !m_value.has_value();
}
/**
* \brief Checks if the result contains a value.
*/
bool has_value() const {
return m_value.has_value();
}
/**
* \brief Returns the encapsulated value.
*/
const type& get_value() const {
return m_value.value();
}
/**
* \brief Returns the encapsulated error.
*/
const error& get_error() const {
return m_value.error();
}
private:
std::expected<type, error> m_value;
};
/**
* \brief Specialization of the \a result class for void type.
*/
template<>
class result<void> {
public:
constexpr result(
result&& other
) noexcept {
m_value = other.m_value;
}
constexpr result(
failure&& failure
) : m_value(std::unexpected(failure.get_error())) {}
constexpr result(
success&& success
) : m_value() {}
bool has_error() const {
return !m_value.has_value();
}
bool has_value() const {
return m_value.has_value();
}
const error& get_error() const {
return m_value.error();
}
private:
std::expected<void, error> m_value;
};
}
#define CONCATENATE(x, y) _CONCATENATE(x, y)
#define _CONCATENATE(x, y) x ## y
/**
* \brief Attempts to call the given \a __function, if the outcome of the
* function call is erroneous immediately returns from the parent function/method.
* \param __success Variable declaration used for storing the successful result
* \param __function Function to execute
*/
#define OUTCOME_TRY(__success, __function) \
auto CONCATENATE(result, __LINE__) = __function; \
if(CONCATENATE(result, __LINE__).has_error()) \
return outcome::failure((CONCATENATE(result, __LINE__)).get_error()); \
__success = CONCATENATE(result, __LINE__).get_value()
Example usage:
outcome::result<std::string> generate_hello(
bool emit_error
) {
if(emit_error) {
return outcome::failure(
error::emit<100>(std::chrono::system_clock::now())
);
}
return "Hello";
}
outcome::result<std::string> generate_world(
bool emit_error
) {
if (emit_error) {
return outcome::failure(
error::emit<200>(std::thread::hardware_concurrency())
);
}
return " world!";
}
outcome::result<std::string> test() {
OUTCOME_TRY(const auto& hello, generate_hello(false));
OUTCOME_TRY(const auto& world, generate_world(false));
return hello + world;
}
i32 main() {
// capture propagated errors
const auto& result = test();
if(result.has_value()) {
std::cout << "value: " << result.get_value() << '\n';
}
else {
result.get_error().print();
}
return 0;
}