6
\$\begingroup\$

This is a C++17 Either implementation for error handling.

  • First intent is I want to improve myself.
  • Second intent is I want to create a more expressive solution than variant for exception free error handling.
  • Third intent is same type handling without extra variable.

to_left(...) and to_right(...) helper functions for specified side if both types are same in Result.

Left and Right are helper structs for avoid additional bool usage in Result.

result.hpp

#include <type_traits>
#include <variant>

namespace marklar::result
{
// Helper
template<typename Type>
struct Left;

template<typename Type>
struct Right;

template<typename>
struct is_left : std::false_type {};

template<typename Type>
struct is_left<Left<Type>> : std::true_type {};

template<typename Type>
inline constexpr bool is_left_v = is_left<Type>::value;

template<typename>
struct is_right : std::false_type {};

template<typename Type>
struct is_right<Right<Type>> : std::true_type {};

template<typename Type>
inline constexpr bool is_right_v = is_right<Type>::value;

template<typename Type>
inline constexpr Left<Type>
to_left(Type const & value) {
    return Left<Type>{ value };
}

template<typename Type, typename std::enable_if_t<std::is_move_constructible_v<Type>, bool> = true>
inline constexpr Left<Type>
to_left(Type && value) {
    return Left<Type>{ std::forward<Type>(value) };
}

template<typename Type>
inline constexpr Right<Type>
to_right(Type const & value) {
    return Right<Type>{ value };
}

template<typename Type, typename std::enable_if_t<std::is_move_constructible_v<Type>, bool> = true>
inline constexpr Right<Type>
to_right(Type && value) {
    return Right<Type>{ std::forward<Type>(value) };
}

template<typename Type>
struct Left {
    Type const value_;

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Left<Type>, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && std::is_convertible_v<ParamType &&, Type>
                 , bool> = true>
    constexpr Left(ParamType && value)
        : value_ { std::forward<ParamType>(value) }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Left<Type>, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && !std::is_convertible_v<ParamType &&, Type>
                 , bool> = false>
    constexpr explicit Left(ParamType && value)
        : value_ { std::forward<ParamType>(value) }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                     && std::is_constructible_v<Type, ParamType const &>
                     && !std::is_convertible_v<ParamType const &, Type>
                 , bool> = false>
    explicit constexpr Left(Left<ParamType> const & other)
        : value_ { other.value_ }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && std::is_convertible_v<ParamType &&, Type>
                 , bool> = true>
    constexpr Left(Left<ParamType> && other)
        : value_ { std::move(other).value_ }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && !std::is_convertible_v<ParamType &&, Type>
                 , bool> = false>
    explicit constexpr Left(Left<ParamType> && other)
        : value_ { std::move(other).value_ }
    {}
};

template<typename Type>
struct Right {
    Type const value_;

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Right<Type>, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && std::is_convertible_v<ParamType &&, Type>
                 , bool> = true>
    constexpr Right(ParamType && value)
        : value_ { std::forward<ParamType>(value) }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Right<Type>, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && !std::is_convertible_v<ParamType &&, Type>
                 , bool> = false>
    constexpr explicit Right(ParamType && value)
        : value_ { std::forward<ParamType>(value) }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                 && std::is_constructible_v<Type, ParamType const &>
                 && !std::is_convertible_v<ParamType const &, Type>
                 , bool> = false>
    explicit constexpr Right(Right<ParamType> const & other)
        : value_ { other.value_ }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && std::is_convertible_v<ParamType &&, Type>
                 , bool> = true>
    constexpr Right(Right<ParamType> && other)
        : value_ { std::move(other).value_ }
    {}

    template <typename ParamType = Type,
              typename std::enable_if_t<
                 !std::is_same_v<Type, ParamType>
                 && std::is_constructible_v<Type, ParamType &&>
                 && !std::is_convertible_v<ParamType &&, Type>
                 , bool> = false>
    explicit constexpr Right(Right<ParamType> && other)
        : value_ { std::move(other).value_ }
    {}
};

template<typename LeftType, typename RightType>
struct Result {
    static_assert(!(std::is_reference_v<LeftType> || std::is_reference_v<RightType>)
                      , "Result must have no reference alternative");
    static_assert(!(std::is_void_v<LeftType> || std::is_void_v<RightType>)
                      , "Result must have no void alternative");

    using LeftValue = Left<LeftType>;
    using RightValue = Right<RightType>;

    static constexpr size_t index_left_ = 0;
    static constexpr size_t index_right_ = 1;

    const std::variant<const LeftValue, const RightValue> variant_;

    constexpr explicit Result(Result<LeftType, RightType> && other)
        : variant_ { std::forward<Result<LeftType, RightType>>( other ).variant_ }
    {}

    template<typename ParamType>
    constexpr explicit Result(ParamType const & value)
        : variant_ {
            []() -> auto {
                if constexpr (std::is_same_v<LeftType, ParamType> || is_left_v<ParamType>) {
                    return std::in_place_index<index_left_>;
                } else if constexpr (std::is_same_v<RightType, ParamType> || is_right_v<ParamType>) {
                    return std::in_place_index<index_right_>;
                }
            }()
            , [](ParamType const & value) -> auto {
                if constexpr (std::is_same_v<LeftType, ParamType>) {
                    return to_left(value);
                } else if constexpr (std::is_same_v<RightType, ParamType>) {
                    return to_right(value);
                } else if constexpr (is_left_v<ParamType> || is_right_v<ParamType>) {
                    return value;
                }
            }(value)
        }
    {
        static_assert((is_left_v<ParamType> || is_right_v<ParamType> || std::is_same_v<LeftType, ParamType> || std::is_same_v<RightType, ParamType>)
                      , "Result only setted alternatives can use");

        if constexpr (!(is_left_v<ParamType> || is_right_v<ParamType>)) {
            static_assert(!std::is_same_v<LeftType, RightType>
                          , "Result must have distinguish between alternatives");
        }
    }

    template<typename ParamType>
    constexpr explicit Result(ParamType && value) noexcept
        : variant_ {
            []() -> auto {
                if constexpr (std::is_same_v<LeftType, ParamType> || is_left_v<ParamType>) {
                    return std::in_place_index<index_left_>;
                } else if constexpr (std::is_same_v<RightType, ParamType> || is_right_v<ParamType>) {
                    return std::in_place_index<index_right_>;
                }
            }()
            , [](ParamType && value) -> auto {
                if constexpr (std::is_same_v<LeftType, ParamType>) {
                    return to_left(std::forward<ParamType>(value));
                } else if constexpr (std::is_same_v<RightType, ParamType>) {
                    return to_right(std::forward<ParamType>(value));
                } else if constexpr (is_left_v<ParamType> || is_right_v<ParamType>) {
                    return std::forward<ParamType>(value);
                }
            }(std::forward<ParamType>(value))
        }
    {
        static_assert((is_left_v<ParamType> || is_right_v<ParamType> || std::is_same_v<LeftType, ParamType> || std::is_same_v<RightType, ParamType>)
                          , "Result only setted alternatives can use");

        if constexpr (!(is_left_v<ParamType> || is_right_v<ParamType>)) {
            static_assert(!std::is_same_v<LeftType, RightType>
                          , "Result must have distinguish between alternatives");
        }
    }

    template<typename TempType = LeftType>
    inline constexpr TempType const &
    left() const &
    {
        static_assert(std::is_convertible_v<TempType, LeftType>);

        return std::get<index_left_>(variant_).value_;
    }

    template<typename TempType = LeftType>
    constexpr TempType &&
    left() &&
    {
        static_assert(std::is_convertible_v<TempType &&, LeftType>);

        return std::move(std::get<index_left_>(variant_).value_);
    }

    template<typename TempType = LeftType>
    constexpr LeftType
    left_or(TempType && substitute) const &
    {
        static_assert(std::is_convertible_v<TempType &&, LeftType>);

        return std::holds_alternative<const LeftValue>(variant_)
                   ? this->left()
                   : static_cast<LeftType>(std::forward<TempType>(substitute));
    }

    template<typename TempType = LeftType>
    constexpr LeftType &&
    left_or(TempType && substitute) &&
    {
        static_assert(std::is_convertible_v<TempType &&, LeftType>);

        return std::holds_alternative<const LeftValue>(variant_)
                   ? std::move(this->left())
                   : static_cast<LeftType>(std::forward<TempType>(substitute));
    }

    template<typename TempType = RightType>
    inline constexpr TempType const &
    right() const &
    {
        static_assert(std::is_convertible_v<TempType, RightType>);

        return std::get<index_right_>(variant_).value_;
    }

    template<typename TempType = RightType>
    constexpr TempType &&
    right() &&
    {
        static_assert(std::is_convertible_v<TempType &&, RightType>);

        return std::move(std::get<index_right_>(variant_).value_);
    }

    template<typename TempType = RightType>
    constexpr RightType
    right_or(TempType && substitute) const &
    {
        static_assert(std::is_convertible_v<TempType &&, RightType>);

        return std::holds_alternative<const LeftValue>(variant_)
                   ? static_cast<RightType>(std::forward<TempType>(substitute))
                   : this->right();
    }

    template<typename TempType = RightType>
    constexpr RightType &&
    right_or(TempType && substitute) &&
    {
        static_assert(std::is_convertible_v<TempType &&, RightType>);

        return std::holds_alternative<const LeftValue>(variant_)
                   ? static_cast<RightType>(std::forward<TempType>(substitute))
                   : std::move(this->right());
    }

    template<typename Function>
    inline constexpr auto left_map(Function const & function) &&
        -> Result<decltype(function(std::get<index_left_>(variant_).value_)), RightType>
    {
        return std::holds_alternative<const LeftValue>(variant_)
                   ? Result{ to_left(function(this->left())) }
                   : Result{ std::get<index_right_>(variant_) };
    }

    template<typename Function>
    inline constexpr auto
    right_map(Function const & function) const
        -> Result<LeftType, decltype(function(std::get<index_right_>(variant_).value_))>
    {
        return std::holds_alternative<const LeftValue>(variant_)
                   ? Result{ std::get<index_left_>(variant_) }
                   : Result{ to_right(function(this->right())) };
    }

    template<typename LeftLocal = LeftType, typename RightLocal = RightType>
    inline constexpr auto
    join() const
        -> std::common_type_t<const LeftLocal, const RightLocal>
    {
        return std::holds_alternative<const LeftValue>(variant_)
                   ? this->left()
                   : this->right();
    }

    inline constexpr operator bool() const noexcept
    {
        return std::holds_alternative<const LeftValue>(variant_);
    }
};
} // namespace marklar::result

main.cpp

#include <iostream>
#include <string>

#include "result.hpp"

// Tester function
auto tester(int result) {
    using R = marklar::result::Result<int, std::string>;

    return
        (result < 0)
            ? R( marklar::result::to_right<std::string>("It is a negative number") )
            : R( marklar::result::to_left<int>(result) )
    ;
}

int main()
{
    std::cout << std::boolalpha;

    std::cout << "Positive test\n";
    auto resOk = tester(42);
    if(resOk) {
        std::cout << "data : " << std::to_string(resOk.left()) << "\n";
    } else {
        std::cout << "error : " << resOk.right() << "\n";
    }
    std::cout << std::endl;


    std::cout << "Negative test\n";
    auto resErr = tester(-1);
    if(resErr) {
        std::cout << "data : " << std::to_string(resErr.left()) << "\n";
    } else {
        std::cout << "error : " << resErr.right() << "\n";
    }
    std::cout << std::endl;

    std::cout << "Same type test - lef side\n";
    marklar::result::Result<int, int> resSameLeft(marklar::result::to_left(42));
    std::cout << "Is store left data? : " << static_cast<bool>(resSameLeft) << "\n";
    std::cout << "data : " << std::to_string(resSameLeft.left()) << "\n";

    std::cout << std::endl;

    std::cout << "Same type test - right side\n";
    marklar::result::Result<int, int> resSameRight(marklar::result::to_right(24));
    std::cout << "Is store left data? : " << static_cast<bool>(resSameRight) << "\n";
    std::cout << "data : " << std::to_string(resSameRight.right()) << "\n";

    std::cout << std::endl;

    return 0;
}

An working example

My questions:

  • Any suggestion for better implementation?
  • Is the perfect forwarding correctly used?
  • Can be improve the usability?
\$\endgroup\$

closed as unclear what you're asking by yuri, t3chb0t, Mast, 1201ProgramAlarm, IEatBagels May 21 at 15:09

Please clarify your specific problem or add additional details to highlight exactly what you need. As it's currently written, it’s hard to tell exactly what you're asking. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 2
    \$\begingroup\$ Title says “Result”, question body says “Either”, which is it? And what is it supposed to do? A usage example would be useful, ideally in a main so we can run your code without making changes. You are also missing #include statements. \$\endgroup\$ – Cris Luengo May 13 at 1:35
  • 1
    \$\begingroup\$ You code seems good, but what is the intent? In particular, what's wrong with variant? What's wrong with exceptions? \$\endgroup\$ – L. F. May 13 at 10:19
  • \$\begingroup\$ - First intent is I want to improve myself. - Second intent is I want to create a more expressive solution than variant for exception free error handling. - Third intent is same type handling without extra variable. \$\endgroup\$ – István Simon May 13 at 11:13
  • 1
    \$\begingroup\$ You could significantly improve usability by foregoing a new type for Result, and making Left and Right more widely applicable. Simply use std::variant directly, and add a template encoding a statically chosen option from a std::variant. Perhaps a few convenience aliases and/or functions, and you are done. \$\endgroup\$ – Deduplicator May 13 at 13:52
  • \$\begingroup\$ Why do you think it is improve the usability? Thanks \$\endgroup\$ – István Simon May 13 at 13:59
3
\$\begingroup\$

It seems to me that you go a very long and very complicated way to do exactly what std::variant does; since you're tagging your question with reinvent-the-wheel it could be perfectly legitimate, but then you can't use std::variant inside your code, because you can't use a wheel to reinvent it.

What is the Either monad? It's not necessarily about error handling, even if it indeed is often used as beefed-up version of Maybe. It only is a type that can hold one of two arbitrary types. Generalizing it into a AnyTypeOf monad, it would become a type that can hold one of several arbitrary types. That is to say, a std::variant. At least conceptually, you rely on a more powerful type (std::variant) to implement a less powerful one (Either) and need 350 lines of very complex code to do it.

Here's my version of the Either monad:

template <typename T, typename U>
using Either = std::variant<T, U>;

I confess that it is a bit rudimentary, but it isn't very difficult to derive the whole monadic interface from it. But let's precise the semantics a bit, since we're looking for exception-free error handling:

template <typename T>
using SafeType = Either<std::string, T>;

Note that the convention is for the right type to hold the correct value, and the left type the error. Now we can write simple constructors-like functions:

using SafeInteger = Either<std::string, int>; /

SafeInteger left(std::string error_message) { return SafeInteger(error_message); }
SafeInteger right(int i)                    { return SafeInteger(i); }

If the type of the error message and of the value are the same, it's just a few characters longer:

using SafeString = Either<std::string, std::string>;

SafeString left(std::string error_message) { return SafeString(std::in_place_index_t<0>(), error_message); }
SafeString right(std::string str)          { return SafeString(std::in_place_index_t<1>(), std::move(str)); }

The monadic scaffolding is also just a few lines long (I implemented it around return and bind, but join wouldn't have been more complex):

auto monadic_return(std::string str) {
    return right(str);
}

template <typename Function>
auto monadic_bind(const SafeString& str, Function func) {
    if (std::get_if<0>(&str)) return str;
    return func(std::get<1>(str));
}

Complete example here: https://wandbox.org/permlink/Sj61MC1jbEO20T5B

\$\endgroup\$
  • \$\begingroup\$ Thanks for the answer. Yes, the solution is somewhere bloated. I tried to put everything to a class/struct. In your solution always right side is the intended result. I tried also this part to generalize, but after rethink this was unnecessary. \$\endgroup\$ – István Simon May 14 at 12:31
  • \$\begingroup\$ Of course, std::string can throw an exception on assignment / initialisation, which might throw a spanner in the works of that example. \$\endgroup\$ – Deduplicator May 14 at 12:34
  • \$\begingroup\$ @Deduplicator: a spanner? / right, it'd be more sensible to allocate a buffer on the stack but I feel lazy. \$\endgroup\$ – papagaga May 14 at 12:45

Not the answer you're looking for? Browse other questions tagged or ask your own question.