模板类上的重载运算符

时间:2019-06-05 20:22:34

标签: c++ templates operator-overloading either

这是Result类的定义,该类旨在模拟Haskell中的Either单子逻辑(LeftFailureRight是{{1} }。

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; } }; 运算符组合两个结果时,一切都很好:

&&

但是,当尝试在结果之上链接计算时,会出现编译错误:

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;
}

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; } 中的错误如下:

clang++

关于如何解决此问题的任何想法?

3 个答案:

答案 0 :(得分:4)

这有效

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

我不能给出一个很好的简洁解释,仅是这么多:类型推导不考虑帐户转换,并且tripleresult<int,std::string>()(int)而不是std::function

您不必使用std::function,但是您可以接受任何可调用的内容,例如:

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

Live Demo

请注意,std::function会带来一些开销。它使用类型擦除来存储各种可调用对象。如果您只想传递一个可呼叫对象,则无需支付该费用。

对于第二行,@ Yksisarvinen的评论已经对其进行了总结。为了完整起见,我在这里简单引用它

  

auto res5b = res4 >>= triple >>= triple;将无法工作   用于两个函数指针或显式括号的附加运算符   在res4 >>= triple附近,因为operator >>=是从右到左的。   它将首先尝试在三重和三重上应用>>=

PS:我都不知道,您的代码比我以前使用的功能风格还要多,也许您可​​以从std::conditional中得到类似的东西?

答案 1 :(得分:2)

因此,在C ++中,std::function不是任何感兴趣的基类。您无法从函数或lambda推论std::function的类型。

所以您:

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

仅在通过实际的std::function时才能推断出。

现在,您真正的意思是“带有S并为result<T,F>类型返回T的东西。”

这不是您用C ++所说的话。

如上所述,>>=是右关联的。我可能会建议使用->*,它从左到右。

第二,您的failure静态函数无法正常工作,因为它经常返回错误的类型。

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

然后添加一个使用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};
  }
}

我现在正在仔细注意所涉及类型的r / lvalue值,如果可能的话,我们会采取行动。

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

使用

答案 2 :(得分:1)

干净有效地实施Result

C ++可以像haskell一样干净有效地表示Result类型。像Haskell一样,C ++具有真正的 sum类型,我们可以用标记的并集封装其全部功能。另外,通过利用隐式构造,我们可以将SuccessFailure表示为类型,而不是静态成员函数(这使事情更简洁)。

定义SuccessFailure

这些真的很简单。它们只是包装器类,因此我们可以将它们实现为聚合。另外,使用C ++ 17的模板推导指南,我们不必为FailureSuccess指定模板参数。相反,我们只能编写Success{10}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>; 

定义Result

Result是求和类型。这意味着它可以是成功或失败,但不能同时是两者。我们可以用并集来表示,并用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>(); 
        }
    }
};

解释Result(...)

从成功或失败构造结果。

  • 重载1到4仅处理基本副本并从SuccessFailure对象移动构造;
  • 通过5重载8来处理我们要进行隐式转换的情况(例如,将字符串文字转换为std::string.的情况
  • 重载9和10处理Result的移动和复制构造。

解释operator>>

这与您对operator>>=的实现非常相似,我将解释更改背后的原因。

    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; 
        }
    }

为什么不使用std::function std::function是一种类型擦除包装器。这意味着它在后台使用虚函数调用,这会使事情变慢。通过使用不受约束的模板,我们使编译器更容易优化内容。

为什么使用>>而不是>>=我使用>>是因为>>=的行为很奇怪,因为它是赋值运算符。语句a >>= b >>= c实际上是a >>= (b >>= c),这不是我们想要的。

class Ret = decltype(valueOf<Fun>()(success.value))做什么??它定义了一个模板参数,默认情况下为您传递的函数的返回类型。这使我们避免使用std::function,同时也允许我们使用lambda。

解释~Result()

由于Result包含一个并集,我们必须手动指定如何破坏它。 (包含Result 的类将不必执行此操作-一旦在Result中指定了该名称,一切都将像正常情况一样)。这很简单。如果它包含Success对象,我们将销毁该对象。否则,我们将销毁failure

   // Result class

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

为我的实施更新您的示例

现在我们已经编写了Result类,我们可以更新您对triplemain的定义。

triple的新定义

相当简单;我们只是将您的successfailure函数替换为SuccessFailure类型。

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

main的新定义

我添加了一个print函数,因此我们实际上可以看到一些输出。这只是一个lambda。完成了两次计算,一次用于ans,另一次用于ans2ans的那个打印18,因为triple的数字不会超过100,但是ans2的那个打印任何东西,因为它会导致失败。


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!