Overloading operator on a templated class

Viewed 148

Here is a definition of a Result class that aims to simulate the logic of the Either monad from Haskell (Left is Failure; Right is Success).

#include <string>
#include <functional>
#include <iostream>

template <typename S, typename F>
class result
{
  private:
    S succ;
    F fail;
    bool pick;

  public:
    /// Chain results of two computations.
    template <typename T>
    result<T,F> operator&&(result<T,F> _res) {
      if (pick == true) {
        return _res;
      } else {
        return failure(fail);
      }
    }

    /// Chain two computations.
    template <typename T>
    result<T,F> operator>>=(std::function<result<T,F>(S)> func) {
      if (pick == true) {
        return func(succ);
      } else {
        return failure(fail);
      }
    }

    /// Create a result that represents success.
    static result success(S _succ) {
      result res;
      res.succ = _succ;
      res.pick = true;

      return res;
    }

    /// Create a result that represents failure.
    static result failure(F _fail) {
      result res;
      res.fail = _fail;
      res.pick = false;

      return res;
    }
};

When trying to compose two results using the && operator, all is well:

int
main(int argc, char* argv[])
{
  // Works!
  auto res1 = result<int, std::string>::success(2);
  auto res2 = result<int, std::string>::success(3);
  auto res3 = res1 && res2;
}

But when attempting to chain computations on top of the result, a compilation error appears:

result<int, std::string>
triple(int val)
{
  if (val < 100) {
    return result<int, std::string>::success(val * 3);
  } else {
    return result<int, std::string>::failure("can't go over 100!");
  }
}

int
main(int argc, char* argv[])
{
  // Does not compile!
  auto res4 = result<int, std::string>::success(2);
  auto res5a = res4 >>= triple;
  auto res5b = res4 >>= triple >>= triple;
}

The error from clang++ is as follows:

minimal.cpp:82:21: error: no viable overloaded '>>='
  auto res5a = res4 >>= triple;
               ~~~~ ^   ~~~~~~
minimal.cpp:26:17: note: candidate template ignored: could not match
      'function<result<type-parameter-0-0, std::__1::basic_string<char,
      std::__1::char_traits<char>, std::__1::allocator<char> > > (int)>' against
      'result<int, std::__1::basic_string<char, std::__1::char_traits<char>,
      std::__1::allocator<char> > > (*)(int)'
    result<T,F> operator>>=(std::function<result<T,F>(S)> func) {
                ^
minimal.cpp:83:32: error: invalid operands to binary expression ('result<int,
      std::string> (int)' and 'result<int, std::string> (*)(int)')
  auto res5b = res4 >>= triple >>= triple;

Any idea as to how to fix this issue?

3 Answers

This works

auto f = std::function< result<int, std::string>(int)>(triple);
auto res5a = res4 >>= f;

I cannot give a good concise explanation, only that much: Type deduction does not take into acount conversions and triple is a result<int,std::string>()(int) not a std::function.

You dont have to use std::function but you can accept any callable with something like:

template <typename G>
auto operator>>=(G func) -> decltype(func(std::declval<S>())) {
    if (pick == true) {
        return func(succ);
    } else {
        return failure(fail);
    }
}

Live Demo

Note that std::function comes with some overhead. It uses type erasure to be able to store all kinds of callables. When you want to pass only one callable there is no need to pay that cost.

For the second line @Yksisarvinen's comment already summarizes it. For the sake of completeness I simply quote it here

auto res5b = res4 >>= triple >>= triple; will not work without additional operator for two function pointers or an explicit brackets around res4 >>= triple, because operator >>= is a right-to-left one. It will try first to apply >>= on triple and triple.

PS: I dont know Either and your code is a bit more functional style than what I am used to, maybe you can get similar out of std::conditional?

So, in C++, a std::function is not the base class of anything of interest. You cannot deduce the type of a std::function from a function or a lambda.

So your:

/// Chain two computations.
template <typename T>
result<T,F> operator>>=(std::function<result<T,F>(S)> func)

will only deduce when passed an actual std::function.

Now, what you really mean is "something that takes an S and returns a result<T,F> for some type T".

This isn't how you say it in C++.

As noted, >>= is right-associative. I might propose ->* instead, which is left-to-right.

Second, your failure static function won't work right, as it returns the wrong type often.

template<class F>
struct failure {
  F t;
};
template<class F>
failure(F)->failure{F};

then add a constructor taking a failure<F>.

/// Chain two computations.
template<class Self, class Rhs,
  std::enable_if_t<std::is_same<result, std::decay_t<Self>>{}, bool> = true
>
auto operator->*( Self&& self, Rhs&& rhs )
-> decltype( std::declval<Rhs>()( std::declval<Self>().succ ) )
{
  if (self.pick == true) {
    return std::forward<Rhs>(rhs)(std::forward<Self>(self).succ);
  } else {
    return failure{std::forward<Self>(self).fail};
  }
}

I am now carefully paying attention to r/lvalue ness of the types involved, and will move if possible.

template<class F>
struct failure {
    F f;
};
template<class F>
failure(F&&)->failure<std::decay_t<F>>;

template<class S>
struct success {
    S s;
};
template<class S>
success(S&&)->success<std::decay_t<S>>;


template <class S, class F>
class result
{
  private:
    std::variant<S, F> state;

  public:
    bool successful() const {
      return state.index() == 0;
    }

    template<class Self,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend decltype(auto) s( Self&& self ) {
        return std::get<0>(std::forward<Self>(self).state);
    }
    template<class Self,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend decltype(auto) f( Self&& self ) {
        return std::get<1>(std::forward<Self>(self).state);
    }

    /// Chain results of two computations.
    template<class Self, class Rhs,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >
    friend std::decay_t<Rhs> operator&&(Self&& self, Rhs&& rhs) {
      if (self.successful()) {
        return success{s(std::forward<Rhs>(rhs))};
      } else {
        return failure{f(std::forward<Self>(self))};
      }
    }

    /// Chain two computations.
    template<class Self, class Rhs,
        std::enable_if_t< std::is_same<result, std::decay_t<Self>>{}, bool> = true
    >        
    friend auto operator->*(Self&&self, Rhs&& rhs)
    -> decltype( std::declval<Rhs>()( s( std::declval<Self>() ) ) )
    {
      if (self.successful()) {
        return std::forward<Rhs>(rhs)(s(std::forward<Self>(self)));
      } else {
        return failure{f(std::forward<Self>(self))};
      }
    }

    template<class T>
    result( success<T> s ):
      state(std::forward<T>(s.s))
    {}
    template<class T>
    result( failure<T> f ):
      state(std::forward<T>(f.f))
    {}
    explicit operator bool() const { return successful(); }
};

live example.

Uses .

Implementing Result cleanly and efficently

C++ can represent a Result type just as cleanly and efficiently as haskell. Like Haskell, C++ has true sum types, and we can encapsulate the entirety of their functionality with a tagged union. In addition, by taking advantage of implicit construction, we can represent Success and Failure as types instead of static member functions (this makes things a lot cleaner).

Defining Success and Failure

These are really simple. They're just wrapper classes, so we can implement them as aggregates. In addition, using C++17's template deduction guides, we won't have to specify the template parameters for Failure and Success. Instead, we'll just be able to write Success{10}, or Failure{"Bad arg"}.

template <class F>
class Failure {
   public: 
    F value;
};
template<class F>
Failure(F) -> Failure<F>; 

template <class S>
class Success {
   public:
    S value;

    // This allows chaining from an initial Success
    template<class Fun>
    auto operator>>(Fun&& func) const {
        return func(value); 
    }
};
template <class S>
Success(S) -> Success<S>; 

Defining Result

Result is a sum type. That means it can be either a success or a failure, but not both. We can represent this with a union, which we'll tag with a was_success bool.

template < class S, class F>
class Result {
    union {
        Success<S> success; 
        Failure<F> failure; 
    };
    bool was_success = false; // We set this just to ensure it's in a well-defined state
   public:
    // Result overloads 1 through 4
    Result(Success<S> const& s) : success(s), was_success(true) {}
    Result(Failure<F> const& f) : failure(f), was_success(false) {}
    Result(Success<S>&& s) : success(std::move(s)), was_success(true) {}
    Result(Failure<F>&& f) : failure(std::move(f)), was_success(false) {}

    // Result overloads 5 through 8
    template<class S2>
    Result(Success<S2> const& s) : success{S(s.value)}, was_success(true) {}
    template<class F2>
    Result(Failure<F2> const& f) : failure{F(f.value)}, was_success(false) {}
    template<class S2>
    Result(Success<S2>&& s) : success{S(std::move(s.value))}, was_success(true) {}
    template<class F2>
    Result(Failure<F2>&& f) : failure{F(std::move(f.value))}, was_success(false) {}

    // Result overloads 9 through 10
    Result(Result const&) = default;
    Result(Result&&) = default; 

    template<class S2> 
    Result<S2, F> operator&&(Result<S2, F> const& res) {
        if(was_success) {
            return res; 
        } else {
            return Failure{failure}; 
        }
    }

    template<class Fun, class Ret = decltype(valueOf<Fun>()(success.value))>
    auto operator>>(Fun&& func) const
        -> Ret
    {
        if(was_success) {
            return func(success.value); 
        } else {
            return failure; 
        }
    }

    ~Result() {
        if(was_success) {
            success.~Success<S>(); 
        } else {
            failure.~Failure<F>(); 
        }
    }
};

Explaining Result(...)

A result is either constructed from a success or a failure.

  • Overloads 1 through 4 just handle the basic copy and move construction from Success and Failure objects;
  • Overloads 5 trhough 8 handle the case where we want to do an implicit conversion (as in the case of a string literal to a std::string.
  • Overloads 9 and 10 handle move and copy construction of Result.

Explaining operator>>

This is pretty similar to your implementation of operator>>=, and i'll explain the reasoning behind my changes.

    template<class Fun, class Ret = decltype(valueOf<Fun>()(success.value))>
    auto operator>>(Fun&& func) const
        -> Ret
    {
        if(was_success) {
            return func(success.value); 
        } else {
            return failure; 
        }
    }

Why not use std::function? std::function is a type-erasing wrapper. That means that it uses virtual function calls under the hood, which slow things down. By using an unconstrained template, we make it much easier for the compiler to optimize stuff.

Why use >> instead of >>=? I used >> because >>= has weird behavior since it's an assignment operator. The statement a >>= b >>= c is actually a >>= (b >>= c), which is not what we intended.

What does class Ret = decltype(valueOf<Fun>()(success.value)) do? That defines a template parameter which is defaulted to the return type of the function you pass. This enables us to avoid using std::function while also allowing us to use lambdas.

Explaining ~Result()

Because Result contains a union, we have to manually specify how to destruct it. (Classes containing Result won't have to do this - once we specify it inside Result, everything behaves like normal). This is pretty straight-forward. If it contains the Success object, we destroy that one. Otherwise, we destroy the failure one.

   // Result class

   ~Result() {
        if(was_success) {
            success.~Success<S>(); 
        } else {
            failure.~Failure<F>(); 
        }
    }

Updating your example for my implementation

Now that we've written a Result class, we can update your definitions of triple and main.

New definition of triple

Pretty straight-forward; we just replaced your success and failure functions with the Success and Failure types.

auto triple(int val) -> Result<int, std::string>
{
    if (val < 100) {
      return Success{val * 3};
    } else {
        return Failure{"can't go over 100"};
    }
}

New definition of main

I added a print function so we can actually see some output. It's just a lambda. There are two computations done, one for ans and the other for ans2. The one for ans prints 18, because triple doesn't push the number over 100, but the one for ans2 doesn't print anything because it results in a failure.


int main(int argc, char* argv[])
{
    auto print = [](auto value) -> Result<decltype(value), std::string> {
        std::cout << "Obtained value: " << value << '\n';
        return Success{value}; 
    };
    auto ans = Success{2} >> triple >> triple >> print;
    auto ans2 = Success{2} >> triple >> triple >> triple >> triple >> triple >> print;
}

You can play with the code here!

Related