【问题标题】:Generic multi-functor composition/pipelining in C++C ++中的通用多功能组合/流水线
【发布时间】:2021-06-17 13:05:32
【问题描述】:

是否可以在 C++ 20 中实现通用的多函子组合/流水线?

struct F{//1st multi-functor
  template<typename T> void operator()(const T& t){/*...*/}
};
struct G{//2nd multi-functor
  template<typename T> void operator()(const T& t){/*...*/}
};

F f;
G g;
auto pipe = f | g;//what magic should happen here to achieve g(f(...)) ? how exactly to overload the operator|()?

pipe(123);   //=> g(f(123);
pipe("text");//=> g(f("text");

编辑: 我尝试了这两个建议(来自@Some_programmer_dude 和@Jarod42),但我迷失在错误中:

  1. 像@Some_programmer_dude 建议的重载运算符|()
template<class Inp, class Out>
auto operator|(Inp inp, Out out){
    return [inp,out](const Inp& arg){
        out(inp(arg));
    };
}

生成:

2>main.cpp(71,13): error C3848: expression having type 'const Inp' would lose some const-volatile qualifiers in order to call 'void F::operator ()<F>(const T &)'
2>        with
2>        [
2>            Inp=F
2>        ]
2>        and
2>        [
2>            T=F
2>        ]
  1. 直接使用 lambda,而不是像 @Jarod42 建议的那样重载 operator|():
auto pipe = [=](const auto& arg){g(f(arg));};

生成:

2>main.cpp(86,52): error C3848: expression having type 'const F' would lose some const-volatile qualifiers in order to call 'void F::operator ()<_T1>(const T &)'
2>        with
2>        [
2>            _T1=int,
2>            T=int
2>        ]

【问题讨论】:

  • 您需要一个operator| 函数,它在左侧接受一个F 对象,在右侧接受一个G 对象。然后它可以返回一个 lambda,当被调用时做你想做的事。
  • auto pipe = [=](const auto&amp; arg){ g(f(arg)); }; :-)
  • lambda 的参数类型与您尝试调用它的类型不同。您确实需要对 lambda 参数使用自动类型推导。现在,lamda 的参数类型是const F&amp;(使用您当前拥有的代码)。请参阅@Jarod42 的评论。
  • “会丢失一些 const-volatile 限定符”是因为您的 operator()s 不是 const

标签: c++ templates pipeline c++20 functor


【解决方案1】:

你就快到了

template<class Inp, class Out>
auto operator|(Inp inp, Out out){
    return [inp,out](const Inp& arg){
        out(inp(arg));
    };
}

struct F{//1st multi-functor
  template<typename T> void operator()(const T& t){/*...*/}
};
struct G{//2nd multi-functor
  template<typename T> void operator()(const T& t){/*...*/}
};

F f;
G g;
auto pipe = f | g;

但是您忽略了一些细微差别。

首先,lambda 中的outinpconst,因为闭包类型有一个const 限定operator()。另外lambda的参数类型应该是auto,而不是Inp,否则只能pipe(f)

您想为某些x 调用g(f(x)),但f 的返回类型是void,这需要是别的东西。

您提供的| 与算术| 会产生歧义。

struct some_type {};

template<typename F>
concept unary_invocable = std::invocable<F, some_type>;

template <unary_invocable Inp, unary_invocable Out>
auto operator|(Inp inp, Out out){
    return [inp,out](auto&& arg){
        return out(inp(std::forward<decltype(arg)>(arg)));
    };
}

struct F{//1st multi-functor
  template<typename T> auto operator()(const T& t) const {/*... return something*/}
};
struct G{//2nd multi-functor
  template<typename T> auto operator()(const T& t) const {/*... return something*/}
};

【讨论】:

  • 谢谢@Caleth,你的方法也奏效了,它比Yakk的简单一点..但我已经标记了答案......无论如何,我肯定会带着我的另一个想法回来mind:多功能的推式管道
【解决方案2】:

所以这里有一个快速的小库。

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

namespace ops {
  template<class D>
  struct op_tag;

  template<class Second, class First>
  struct pipe_t;

  template<class D>
  struct op_tag {
    D const& self() const { return *static_cast<D const*>(this); }
    D& self() { return *static_cast<D*>(this); }

    auto operator()(auto&&...args) const
      RETURNS( self()(decltype(args)(args)...) )
    auto operator()(auto&&...args)
      RETURNS( self()(decltype(args)(args)...) )
  };
  
  template<class Second, class First>
  struct pipe_t:op_tag<pipe_t<Second, First>> {
    Second second;
    First first;
    pipe_t( Second second_, First first_ ):
      second(std::move(second_)),
      first(std::move(first_))
    {}
    auto operator()(auto&&...args)
      RETURNS( second(first(decltype(args)(args)...)) )
    auto operator()(auto&&...args) const
      RETURNS( second(first(decltype(args)(args)...)) )
  };
  template<class Second, class First>
  auto operator|(op_tag<First> const& first, op_tag<Second> const& second)
    RETURNS( pipe_t<Second, First>{ second.self(), first.self() } )
}

以贪婪的方式重载运算符被认为是不礼貌的。您只希望您的运算符重载参与您特别支持的类型。

这里我要求类型继承自 op_tag&lt;T&gt; 以表明它们有兴趣成为一个操作。

然后我们稍微修改您的代码:

struct F:ops::op_tag<F>{//1st multi-functor
  template<typename T>
  auto operator()(const T& t){
      std::cout << "f(" << t << ")";
      return -1;
  }
};
struct G:ops::op_tag<G>{//2nd multi-functor
  template<typename T> auto operator()(const T& t){
      std::cout << "g(" << t << ")";
      return 7;
  }
};

添加标签和返回值(否则 f(g(x)) 没有意义,除非 g 返回一些东西)。

你写的代码现在可以工作了。

如果您愿意,我们还可以添加对std::functions 甚至原始函数的支持。您可以在namespace ops 中添加合适的operator| 重载,并要求人们使用using ops::operator| 将运算符纳入范围(或将其与op_tag'd 类型一起使用)。

Live example.

输出:

f(123)g(-1)f(text)g(-1)

【讨论】:

  • 感谢@Yakk 提供了一个详细而完整的工作示例!它确实 f(g(...)) 而不是 g(f(...)) 但我看到了你的“魔法”:) 你也是对的:来自多功能函数的方法应该返回一些东西......管道中下一个仿函数的值
  • @solo 嗯,我想first | secondsecond | first 更有意义operator|。应该很容易换。我在考虑构图,而不是管道。
  • @Solo 将参数的顺序更改为operator|,并立即执行second | first 的顺序。
猜你喜欢
  • 2011-01-26
  • 1970-01-01
  • 1970-01-01
  • 2012-09-18
  • 1970-01-01
  • 2021-03-03
  • 1970-01-01
  • 2011-12-13
  • 2014-03-22
相关资源
最近更新 更多