简体   繁体   English

模板类上的重载运算符

[英]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 ). 这是一个Result类的定义,该类旨在模拟Haskell中的Either monad的逻辑( LeftFailureRightSuccess )。

#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: 来自clang++的错误如下:

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? 关于如何解决此问题的任何想法吗?

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 . 我不能给出一个很好的简洁解释,仅是这么多:类型推导不考虑计数转换,而triple是一个result<int,std::string>()(int)而不是std::function

You dont have to use std::function but you can accept any callable with something like: 您不必使用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 现场演示

Note that std::function comes with some overhead. 注意std::function带有一些开销。 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. 对于第二行,@ Yksisarvinen的评论已经对其进行了总结。 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. 没有两个函数指针的附加运算符或res4 >>= triple周围的括号,将无法工作,因为operator >>=是从右到左的。 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 ? PS:我都不知道,而且您的代码比我以前使用的代码更具功能性,也许您可​​以从std::conditional得到类似的信息?

So, in C++, a std::function is not the base class of anything of interest. 因此,在C ++中, std::function不是任何感兴趣的基类。 You cannot deduce the type of a std::function from a function or a lambda. 您不能从函数或lambda推导std::function的类型。

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 . 仅在通过实际的std::function时才推断出来。

Now, what you really mean is "something that takes an S and returns a result<T,F> for some type T ". 现在,您真正的意思是“带有S并为某些T类型返回result<T,F>的东西”。

This isn't how you say it in C++. 这不是您在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. 其次, failure静态函数无法正常运行,因为它经常返回错误的类型。

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

then add a constructor taking a 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};
  }
}

I am now carefully paying attention to r/lvalue ness of the types involved, and will move if possible. 我现在正在仔细注意所涉及类型的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 . 现场例子

Uses . 使用

Implementing Result cleanly and efficently 干净,有效地执行Result

C++ can represent a Result type just as cleanly and efficiently as haskell. C ++可以像haskell一样干净高效地表示Result类型。 Like Haskell, C++ has true sum types , and we can encapsulate the entirety of their functionality with a tagged union. 像Haskell一样,C ++具有真正的sum类型 ,我们可以使用加标签的并集封装其全部功能。 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). 另外,通过利用隐式构造,我们可以将SuccessFailure表示为类型,而不是静态成员函数(这使事情更加清晰)。

Defining Success and Failure 定义SuccessFailure

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 . 此外,使用C ++ 17的模板推导指南,我们不必为FailureSuccess指定模板参数。 Instead, we'll just be able to write Success{10} , or Failure{"Bad arg"} . 相反,我们只能编写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>; 

Defining Result 定义Result

Result is a sum type. Result是求和类型。 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. 我们可以用并集来表示,并用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(...) 解释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; 重载1到4仅处理基本副本,并从SuccessFailure对象移动构造。
  • 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. 重载5到8将处理我们要进行隐式转换的情况(例如,将字符串文字转换为std::string.
  • Overloads 9 and 10 handle move and copy construction of Result . 重载9和10处理Result移动和复制结构。

Explaining operator>> 解释operator>>

This is pretty similar to your implementation of operator>>= , and i'll explain the reasoning behind my changes. 这与您对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; 
        }
    }

Why not use std::function ? 为什么不使用std::function std::function is a type-erasing wrapper. std::function是类型擦除包装器。 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. 语句a >>= b >>= c实际上a >>= (b >>= c) ,这不是我们想要的。

What does class Ret = decltype(valueOf<Fun>()(success.value)) do? class Ret = decltype(valueOf<Fun>()(success.value))什么作用? 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. 这使我们能够避免使用std::function同时还允许我们使用lambda。

Explaining ~Result() 解释〜Result ~Result()

Because Result contains a union, we have to manually specify how to destruct it. 由于Result包含一个并集,因此我们必须手动指定如何对其进行销毁。 (Classes containing Result won't have to do this - once we specify it inside Result , everything behaves like normal). (包含Result将不必执行此操作-一旦在Result指定了Result ,则所有行为都将像正常情况一样)。 This is pretty straight-forward. 这很简单。 If it contains the Success object, we destroy that one. 如果它包含Success对象,我们将销毁该对象。 Otherwise, we destroy the failure one. 否则,我们破坏failure之一。

   // 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 . 既然我们已经编写了一个Result类,我们就可以更新您对triplemain定义。

New definition of triple triple新定义

Pretty straight-forward; 很简单 we just replaced your success and failure functions with the Success and Failure types. 我们只是将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"};
    }
}

New definition of main main新定义

I added a print function so we can actually see some output. 我添加了print功能,因此我们实际上可以看到一些输出。 It's just a lambda. 这只是一个lambda。 There are two computations done, one for ans and the other for ans2 . 完成了两个计算,一个用于ans ,另一个用于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. 一个用于ans打印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! 您可以在此处使用代码!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM