【问题标题】:Overloading operator on a templated class模板类上的重载运算符
【发布时间】:2019-06-05 20:22:34
【问题描述】:

这是一个 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;
    }
};

当尝试使用 &amp;&amp; 运算符组合两个结果时,一切正常:

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++的错误如下:

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;

知道如何解决这个问题吗?

【问题讨论】:

  • 在我看来,triple 是一个函数。您是否尝试过调用它,就像使用 result::success 一样?投票结束是一个错字。
  • @AlgirdasPreidžius 这个想法是将函数作为参数提供给&gt;&gt;= 运算符。有什么错别字?
  • 在以前已知的_463035818 的答案之上,如果没有两个函数指针的附加运算符或res4 &gt;&gt;= triple 周围的显式括号,auto res5b = res4 &gt;&gt;= triple &gt;&gt;= triple; 将无法工作,因为operator &gt;&gt;= 是从右到左的。它将首先尝试在tripletriple 上应用&gt;&gt;=
  • 我写了一篇详细的帖子,解释了如何干净高效地实现Result,使其成为 sum 类型。它解决了您的问题,并且避免使用std::function,以便编译器可以内联所有内容。语法最终更接近 Haskell 的

标签: c++ templates operator-overloading either


【解决方案1】:

这行得通

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

我无法给出一个简洁的解释,仅此而已:类型推导不考虑转换,tripleresult&lt;int,std::string&gt;()(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 &gt;&gt;= triple &gt;&gt;= triple; 将无法工作 两个函数指针或显式括号的附加运算符 围绕res4 &gt;&gt;= triple,因为operator &gt;&gt;= 是从右到左的。 它将首先尝试在三重和三重上应用&gt;&gt;=

PS:我不知道,而且你的代码比我习惯的更具功能性,也许你可以从std::conditional得到类似的东西?

【讨论】:

    【解决方案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 并为某些类型 T 返回 result&lt;T,F&gt; 的东西”。

    这不是你在 C++ 中的说法。

    如前所述,&gt;&gt;= 是右关联的。我可能会建议-&gt;*,它是从左到右的。

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

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

    然后添加一个采用failure&lt;F&gt; 的构造函数。

    /// 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.

    使用

    【讨论】:

      【解决方案3】:

      干净高效地实施Result

      C++ 可以像 haskell 一样干净有效地表示 Result 类型。和 Haskell 一样,C++ 也有真正的 sum 类型,我们可以用标记的 union 封装它们的全部功能。此外,通过利用隐式构造,我们可以将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 是 sum 类型。这意味着它可以是成功或失败,但不能两者兼而有之。我们可以用一个联合来表示它,我们将用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&gt;&gt;

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

          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 是一个类型擦除包装器。这意味着它在后台使用虚函数调用,这会减慢速度。通过使用不受约束的模板,我们使编译器更容易优化内容。

      为什么使用&gt;&gt; 而不是&gt;&gt;= 我使用&gt;&gt; 因为&gt;&gt;= 具有奇怪的行为,因为它是一个赋值运算符。声明a &gt;&gt;= b &gt;&gt;= c 实际上是a &gt;&gt;= (b &gt;&gt;= c),这不是我们想要的。

      class Ret = decltype(valueOf&lt;Fun&gt;()(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!

      【讨论】:

        猜你喜欢
        • 2011-04-30
        • 2012-02-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-01
        • 2013-02-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多