2
\$\begingroup\$

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;
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Unnecessary code

Your classes are actually just very thin wrappers over std::expected and std::unexpected. With some minor changes (like replacing get_value() with value(), has_error() with !has_value()), you can just do:

namespace outcome {
    template<typename T>
    using failure = std::unexpected<T>;

    template<typename T>
    using result = std::expected<T, error>;
}

And the rest of your code will compile and do the same thing!

Unnecessary templating of error::emit()

I don't see any point in passing error_code as a template parameter to error::emit(). Nothing is constexpr here. If the compiler can somehow specialize and optimize based on the error_code, it can still do that if it's passed as a regular function parameter.

If you really want to make use of the template parameter, then instead of storing the error messages in a std::unordered_map, use variable templates and template specialization instead:

class error {
    template<u64 error_code>
    static const char* error_template{};
    …
public:
    template<u64 error_code, …>
    static error emit(…) {
        return {error_code, std::vformat(error_template<error_code>, …)};
    }
    …
};

template<> const char* error::error_template<100> = "error occured while creating 'Hello', today's date is: {:%Y-%m-%d}";
template<> const char* error::error_template<200> = "something messed up in the 'world!' creation process, system has {} threads";

Consider using std::error_code instead of class error

Your class error looks very similar to std::error_code. I would use the latter instead of reinventing the wheel. You can create a custom error category (derived from std::error_category) for your own errors.

Avoid creating aliases for built-in types

It might save a little bit of typing, but I recommend you don't create u64 and i32 aliases. In particular, don't declare those aliases in a header file in the global namespace. Just write std::uint64_t and std::int32_t. If you have a project that includes multiple libraries, it's easy to get lots of different aliases for the same type (making grepping harder), and adds potential for conflicts.

main() returns int, not i32

There is only one valid return type for main(), and that is int. While i32 might be an alias for int on some platforms, there are other platforms where this is not the case, and thus would probably cause a compile error.

Don't use const references to hold returned values

In main() you wrote:

const auto& result = test();

However, test() returns an outcome by value. The const auto reference will cause the temporary return value's lifetime to be extended, so it still works fine, but I would just write:

auto result = test();

You wouldn't write this either, even though it compiles:

const auto& pi = 3.1415;
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.