Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overloading operator on a templated class

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?

like image 445
Daniel Lovasko Avatar asked Jan 25 '26 15:01

Daniel Lovasko


2 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?

like image 97
463035818_is_not_a_number Avatar answered Jan 28 '26 04:01

463035818_is_not_a_number


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 c++17.

like image 31
Yakk - Adam Nevraumont Avatar answered Jan 28 '26 05:01

Yakk - Adam Nevraumont