【问题标题】:Operator composition in c++C++ 中的运算符组合
【发布时间】:2018-10-05 16:44:52
【问题描述】:

我想知道在 C++ 中组合数学运算符是否有一个优雅的解决方案。对于操作员,我的意思类似于以下内容:

template<class H>
class ApplyOp {
    H h;
public:
    ApplyOp(){}
    ApplyOp(H h_i) : h(h_i) {}

    template<class argtype>
    double operator()(argtype f,double x){
        return h(x)*f(x);
    }
};

上面的类使用了一个“辅助函数”h(x)。例如,

struct Helper{
    Helper(){}
    double operator()(double x){return x*x;}
};

struct F{
    F(){}
    double operator()(double x){return exp(x);}
};

int main()
{
    Helper h;
    F f;
    ApplyOp<Helper> A(h);

    std::cout<<"A(f,2.0) = "<<A(f,2.0)<<std::endl; //Returns 2^2*exp(2) = 29.5562...

    return 0;
}

现在,我想组合运算符两次或更多次,即计算 A^2(f,2.0)。在上面的示例中,这将返回 h(x)*h(x)*f(x)。请注意,这是 not 函数组合,即我不想计算 A(A(f,2.0),2.0)。相反,考虑矩阵的计算能力:如果h(x) = M(矩阵),我想要M*M*...*M*x

我能够使用std::bind() 来达到我想要的A^2 的结果(但不是更高的权力!)如下:

auto g = std::bind(&ApplyOp<Helper>::operator()<F>,&A,f,std::placeholders::_1);

使用生成的g,我可以通过简单地调用A(g,2.0) 来应用A^2(f,2.0)。对于上面的例子,这将返回h(x)*h(x)*f(x) = x*x*x*x*exp(x)

我如何将其概括为迭代应用运算符A N 次?我真的很喜欢here 发布的答案,但它在这里不太适用。我尝试过嵌套std:binds,但很快就陷入了深层编译器错误。

有什么想法吗?

完整的工作示例:

#include<iostream>
#include<math.h>
#include<functional> //For std::bind

template<class H>
class ApplyOp {
    H h;
public:
    ApplyOp(){}
    ApplyOp(H h_i) : h(h_i) {}

    template<class argtype>
    double operator()(argtype f,double x){
        return h(x)*f(x);
    }
};

struct Helper{
    Helper(){}
    double operator()(double x){return x*x;}
};

struct F{
    F(){}
    double operator()(double x){return exp(x);}
};

int main()
{
    Helper h;
    F f;
    ApplyOp<Helper> A(h);

    std::cout<<"A(f,2.0) = "<<A(f,2.0)<<std::endl; //Returns 2^2*exp(2) = 29.5562...

    auto g = std::bind(&ApplyOp<Helper>::operator()<F>,&A,f,std::placeholders::_1);

    std::cout<<"A^2(f,2.0) = "<<A(g,2.0) <<std::endl; //Returns 2^4*exp(2) = 118.225... 

    return 0;
}

【问题讨论】:

  • 要拥有h(x)*h(x)*f(x),你必须改为拥有h1(x) = h(x)*h(x)并使用ApplyOp&lt;Helper1&gt; A(h1);
  • 使用模板和模板专业化
  • @Jarod42 这有点没抓住重点。我想要一个更通用的解决方案,它不依赖于运算符的特定形式。
  • @user9335240 我不确定我是否了解专业化有何帮助 - 你能再解释一下吗?
  • @Jarod42 您可以创建一个模板函数,其模板参数为 int,递归解析为相同的函数,但模板参数减一,并在此参数 == 时进行专门化2 做绑定

标签: c++ c++11 functional-programming


【解决方案1】:

试试这个,使用模板特化

#include<iostream>
#include<math.h>
#include<functional> //For std::bind

template<class H>
class ApplyOp {
    H h;
public:
    ApplyOp(){}
    ApplyOp(H h_i) : h(h_i) {}

    template<class argtype>
    double operator()(argtype f,double x){
        return h(x)*f(x);
    }
};

struct Helper{
    Helper(){}
    double operator()(double x){return x*x;}
};

struct F{
    F(){}
    double operator()(double x){return exp(x);}
};

// C++ doesn't permit recursive "partial specialization" in function
// So, make it a struct instead
template<typename T, typename U, typename W, int i>
struct Binder {
    auto binder(U b, W c) {
        // Recursively call it with subtracting i by one
        return [&](T x){ return b(Binder<T, U, W, i-1>().binder(b, c), x); };
    }
};

// Specialize this "struct", when i = 2
template<typename T, typename U, typename W>
struct Binder<T, U, W, 2> {
    auto binder(U b, W c) {
        return [&](T x){ return b(c, x); };
    }
};

// Helper function to call this struct (this is our goal, function template not
// struct)
template<int i, typename T, typename U, typename W>
auto binder(U b, W d) {
    return Binder<T, U, W, i>().binder(b, d);
}

int main()
{
    Helper h;
    F f;
    ApplyOp<Helper> A(h);

    std::cout<<"A(f,2.0) = "<<A(f,2.0)<<std::endl; //Returns 2^2*exp(2) = 29.5562...

    // We don't need to give all the template parameters, C++ will infer the rest
    auto g = binder<2, double>(A, f);

    std::cout<<"A^2(f,2.0) = "<<A(g,2.0) <<std::endl; //Returns 2^4*exp(2) = 118.225... 

    auto g1 = binder<3, double>(A, f);

    std::cout<<"A^3(f,2.0) = "<<A(g1,2.0) <<std::endl; //Returns 2^6*exp(2) = 472.2

    auto g2 = binder<4, double>(A, f);

    std::cout<<"A^4(f,2.0) = "<<A(g2,2.0) <<std::endl; //Returns 2^8*exp(2) = 1891.598... 

    return 0;
}

【讨论】:

  • 非常接近我的需要。我试图在我的项目中实现它,但它并不能很好地工作 - 由于某种原因,binder 返回的 lambda 没有捕获正确的行为。如果我做auto g = [&amp;] (double x){return A(f,x);}; 然后评估A(g,x),我会得到正确的结果,但如果我使用auto g = binder&lt;2,double&gt;(A,f) 则不会。对于我给出的示例(您的代码有效),它非常有效,但是当我对ApplyOp 进行更改时,它会中断。我可能会发布一个单独的问题,因为很难在评论中解释。谢谢!
  • 好的,这正是我所需要的。我需要在返回 lambda 表达式时按值捕获(即使用[=]),并确保我所有的operator() 都是const。很好的解决方案,再次感谢!
【解决方案2】:

从我能从你的问题中了解到,你实际上是在尝试定义

A^1(h, f, x) = h(x) * f(x)
A^n(h, f, x) = h(x) * A^(n-1)(h, f, x)

如果您愿意使用 C++17,以下是您可以构建的基础

#include <iostream>
#include <math.h>

template <int N>
struct apply_n_helper {
  template <typename H, typename F>
  auto operator()(H h, F f, double x) const {
    if constexpr(N == 0) {
      return f(x);
    } else {
      return h(x) * apply_n_helper<N - 1>()(h, f, x);
    }
  }
};

template <int N>
constexpr auto apply_n = apply_n_helper<N>();

int main() {
  auto sqr = [](double x) { return x * x; };
  auto exp_ = [](double x) { return exp(x); };

  std::cout << apply_n<100>(sqr, exp_, 2.0) << '\n';
  std::cout << apply_n<200>(sqr, exp_, 2.0) << '\n';
  return 0;
}

如果 C++17 不是一个选项,您可以轻松地重写它以使用模板特化而不是 constexpr-if。我将把它留作练习。以下是包含此代码的编译器资源管理器的链接:https://godbolt.org/z/5ZMw-W

EDIT 回顾这个问题,我发现您实际上是在尝试以某种方式计算 (h(x))^n * f(x),这样您就不必在运行时和生成的代码中实际执行任何循环相当于:

auto y = h(x);
auto result = y * y * ... * y * f(x)
              \_____________/
                  n times
return result;

实现此目的的另一种方法是如下所示

#include <cmath>
#include <iostream>

template <size_t N, typename T>
T pow(const T& x) {
    if constexpr(N == 0) {
        return 1;
    } else if (N == 1) {
        return x;
    } else {
        return pow<N/2>(x) * pow<N - N/2>(x);
    }
}

template <int N>
struct apply_n_helper {
    template <typename H, typename F>
    auto operator()(H h, F f, double x) const {
        auto tmp = pow<N>(h(x));
        return tmp * f(x);
    }
};

template <int N>
constexpr auto apply_n = apply_n_helper<N>();

int main()
{
    auto sqr = [](double x) { return x * x; };
    auto exp_ = [](double x) { return exp(x); };

    std::cout << apply_n<100>(sqr, exp_, 2.0) << '\n';
    std::cout << apply_n<200>(sqr, exp_, 2.0) << '\n';
    return 0;
}

在这里,pow 函数的使用使我们免于多次评估h(x)

【讨论】:

    【解决方案3】:

    我的意思是你可以使用其他类:

    template <typename T, std::size_t N>
    struct Pow
    {
        Pow(T t) : t(t) {}
    
        double operator()(double x) const
        {
            double res = 1.;
    
            for (int i = 0; i != N; ++i) {
                res *= t(x);
            }
            return res;
        }
    
        T t;  
    };
    

    并使用

    ApplyOp&lt;Pow&lt;Helper, 2&gt;&gt; B(h); 而不是ApplyOp&lt;Helper&gt; A(h);

    Demo

    【讨论】:

      【解决方案4】:

      所以你希望能够乘以函数。嗯,听起来不错。为什么不+-/ 而我们在那里呢?

      template<class F>
      struct alg_fun;
      
      template<class F>
      alg_fun<F> make_alg_fun( F f );
      
      template<class F>
      struct alg_fun:F {
        alg_fun(F f):F(std::move(f)){}
        alg_fun(alg_fun const&)=default;
        alg_fun(alg_fun &&)=default;
        alg_fun& operator=(alg_fun const&)=default;
        alg_fun& operator=(alg_fun &&)=default;
      
        template<class G, class Op>
        friend auto bin_op( alg_fun<F> f, alg_fun<G> g, Op op ) {
          return make_alg_fun(
            [f=std::move(f), g=std::move(g), op=std::move(op)](auto&&...args){
              return op( f(decltype(args)(args)...), g(decltype(args)(args)...) );
            }
          );
        }
      
        template<class G>
        friend auto operator+( alg_fun<F> f, alg_fun<G> g ) {
          return bin_op( std::move(f), std::move(g), std::plus<>{} );
        }
        template<class G>
        friend auto operator-( alg_fun<F> f, alg_fun<G> g ) {
          return bin_op( std::move(f), std::move(g), std::minus<>{} );
        }
        template<class G>
        friend auto operator*( alg_fun<F> f, alg_fun<G> g ) {
          return bin_op( std::move(f), std::move(g),
            std::multiplies<>{} );
        }
        template<class G>
        friend auto operator/( alg_fun<F> f, alg_fun<G> g ) {
          return bin_op( std::move(f), std::move(g),
            std::divides<>{} );
        }
      
        template<class Rhs,
          std::enable_if_t< std::is_convertible<alg_fun<Rhs>, F>{}, bool> = true
        >
        alg_fun( alg_fun<Rhs> rhs ):
          F(std::move(rhs))
        {}
      
        // often doesn't compile:
        template<class G>
        alg_fun& operator-=( alg_fun<G> rhs )& {
          *this = std::move(*this)-std::move(rhs);
          return *this;
        }
        template<class G>
        alg_fun& operator+=( alg_fun<G> rhs )& {
          *this = std::move(*this)+std::move(rhs);
          return *this;
        }
        template<class G>
        alg_fun& operator*=( alg_fun<G> rhs )& {
          *this = std::move(*this)*std::move(rhs);
          return *this;
        }
        template<class G>
        alg_fun& operator/=( alg_fun<G> rhs )& {
          *this = std::move(*this)/std::move(rhs);
          return *this;
        }
      };
      template<class F>
      alg_fun<F> make_alg_fun( F f ) { return {std::move(f)}; }
      
      auto identity = make_alg_fun([](auto&& x){ return decltype(x)(x); });
      template<class X>
      auto always_return( X&& x ) {
        return make_alg_fun([x=std::forward<X>(x)](auto&&... /* ignored */) {
          return x;
        });
      }
      

      我觉得差不多就行了。

      auto square = identity*identity;
      

      我们也可以有类型擦除算法。

      template<class Out, class...In>
      using alg_map = alg_fun< std::function<Out(In...)> >;
      

      这些是支持*= 之类的东西。 alg_fun 通常不要键入擦除足够的内容。

      template<class Out, class... In>
      alg_map<Out, In...> pow( alg_map<Out, In...> f, std::size_t n ) {
        if (n==0) return always_return(Out(1));
        auto r = f;
        for (std::size_t i = 1; i < n; ++i) {
          r *= f;
        }
        return r;
      }
      

      可以更有效地完成。

      测试代码:

      auto add_3 = make_alg_fun( [](auto&& x){ return x+3; } );
      std::cout << (square * add_3)(3)  << "\n";; // Prints 54, aka 3*3 * (3+3)
      
      alg_map<int, int> f = identity;
      std::cout << pow(f, 10)(2) << "\n"; // prints 1024
      

      Live example.

      这是一个更高效的 p​​ow,无需类型擦除:

      inline auto raise(std::size_t n) {
        return make_alg_fun([n](auto&&x)
          -> std::decay_t<decltype(x)>
        {
          std::decay_t<decltype(x)> r = 1;
          auto tmp = decltype(x)(x);
      
          std::size_t bit = 0;
          auto mask = n;
          while(mask) {
            if ( mask & (1<<bit))
              r *= tmp;
            mask = mask & ~(1<<bit);
            tmp *= tmp;
            ++bit;
          }
          return r;
        });
      }
      template<class F>
      auto pow( alg_fun<F> f, std::size_t n ) {
        return compose( raise(n), std::move(f) );
      }
      

      Live example。它在alg_fun 中使用了一个新函数compose

        template<class G>
        friend auto compose( alg_fun lhs, alg_fun<G> rhs ) {
          return make_alg_fun( [lhs=std::move(lhs), rhs=std::move(rhs)](auto&&...args){
              return lhs(rhs(decltype(args)(args)...));
          });
        }
      

      这是compose(f,g)(x) := f(g(x))

      你的代码现在变成了

      alg_fun<Helper> h;
      alg_fun<F> f;
      
      auto result = pow( h, 10 )*f;
      

      这是h(x)*h(x)*h(x)*h(x)*h(x)*h(x)*h(x)*h(x)*h(x)*h(x)*f(x)。除了(使用高效版本)我只调用一次 h 并将结果提高到 10 次方。

      【讨论】:

        猜你喜欢
        • 2011-02-13
        • 1970-01-01
        • 2011-04-24
        • 1970-01-01
        • 2022-07-13
        • 1970-01-01
        • 2011-05-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多