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\$
6
  • 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\$CommentedMay 13, 2019 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.
    CommentedMay 13, 2019 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\$CommentedMay 13, 2019 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\$CommentedMay 13, 2019 at 13:52
  • \$\begingroup\$Why do you think it is improve the usability? Thanks\$\endgroup\$CommentedMay 13, 2019 at 13:59

1 Answer 1

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\$
3
  • \$\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\$CommentedMay 14, 2019 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\$CommentedMay 14, 2019 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
    CommentedMay 14, 2019 at 12:45

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.