如何管理需要从递归 functors/lambdas 派生的模板参数的声明?

How do I manage declarations that require template parameters derived from recursive functors/lambdas?

我正在尝试构建一个干净整洁的具有递归能力的 lambda 自我作用域实现(它基本上是一个 Y 组合器,尽管我认为技术上不完全是)。这是一段带我去的旅程,其中包括 this thread and and this thread.

我已经尽可能清楚地归结了我的一个问题:我如何传递以 lambda 为模板参数的模板仿函数?

#include <string>
#include <iostream>
#define uint unsigned int

template <class F>
class Functor {
public:
    F m_f;

    template <class... Args>
    decltype(auto) operator()(Args&&... args) {
        return m_f(*this, std::forward<Args>(args)...);
    }
};
template <class F> Functor(F)->Functor<F>;

class B {
private:
    uint m_val;
public:
    B(uint val) : m_val(val) {}
    uint evaluate(Functor<decltype([](auto & self, uint val)->uint {})> func) const {
        return func(m_val);
    }
};

int main() {
    B b = B(5u);
    Functor f = Functor{[](auto& self, uint val) -> uint {
        return ((2u * val) + 1u);
    }};

    std::cout << "f applied to b is " << b.evaluate(f) << "." << std::endl;
}

上面的代码不起作用,Visual Studio 声称 f(在 b.evaluate(f) 调用中)与参数类型不匹配。

我的假设是 auto & self 不够聪明,无法完成这项工作。我该如何解决这个问题?当它们基本上无法定义时,我如何存储和传递这些东西?这就是为什么我见过的许多 Y 组合器实现都有奇怪的双重包装的原因吗?

任何帮助或解释将不胜感激。

我看到的唯一方法是使 evaluate() 成为模板方法;如果你想确保收到一个 Functor(但你可以简单地接受一个可调用的:见 ):

template <typename F>
uint evaluate(Functor<F> func) const {
    return func(m_val);
}

考虑到每个 lambda 都是不同的类型,您可以使用以下简单代码进行验证

auto l1 = []{};
auto l2 = []{};

static_assert( not std::is_same_v<decltype(l1), decltype(l2)> );

所以将特定的 lambda 类型强加给 evaluate() 是行不通的,因为如果您使用(显然)相同的 lambda 函数调用该方法,调用将不匹配,如下所示例子

auto l1 = []{};
auto l2 = []{};

void foo (decltype(l1))
 { }

int main ()
 {
   foo(l2); // compilation error: no matching function for call to 'foo'
 }

最简单的解决方案是:

uint evaluate(std::function<uint(uint)> func) const {
    return func(m_val);
}

更进一步是写 function_view

uint evaluate(function_view<uint(uint)> func) const {
    return func(m_val);
}

(网上有几十种实现,应该很容易找到)

最简单且运行时效率最高的是:

template<class F>
uint evaluate(F&& func) const {
    return func(m_val);
}

因为我们不关心 func 是什么,我们只希望它像鸭子一样嘎嘎叫。如果你想早点检查...

template<class F> requires (std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >)
uint evaluate(F&& func) const {
    return func(m_val);
}

使用, or using

template<class F,
  std::enable_if_t<(std::is_convertible_v< std::invoke_result_t< F&, uint >, uint >), bool> = true
>
uint evaluate(F&& func) const {
    return func(m_val);
}

相似只是更晦涩。

你可以写一个 fixes-signature type-erased Functor,但我认为这是个坏主意。看起来像:

template<class R, class...Args>
using FixedSignatureFunctor = Functor< std::function<R( std::function<R(Args...)>, Args...) > >;

或者效率稍微高一点

template<class R, class...Args>
using FixedSignatureFunctor = Functor< function_view<R( std::function<R(Args...)>, Args...) > >;

但这太疯狂了;你会想忘记 F 是什么,但你不能忘记 F!

为了使它完全“有用”,您必须将智能 copy/move/assign 操作添加到 Functor,如果每个操作中的 F 可以复制它可以复制。

template <class F>
class Functor {
public:
  // ...
  Functor(Functor&&)=default;
  Functor& operator=(Functor&&)=default;
  Functor(Functor const&)=default;
  Functor& operator=(Functor const&)=default;

  template<class O> requires (std::is_constructible_v<F, O&&>)
  Functor(Functor<O>&& o):m_f(std::move(o.m_f)){}
  template<class O> requires (std::is_constructible_v<F, O const&>)
  Functor(Functor<O> const& o):m_f(o.m_f){}
  template<class O> requires (std::is_assignable_v<F, O&&>)
  Functor& operator=(Functor<O>&& o){
    m_f = std::move(o.mf);
    return *this;
  }
  template<class O> requires (std::is_assignable_v<F, O const&>)
  Functor& operator=(Functor<O> const& o){
    m_f = o.mf;
    return *this;
  }
  // ...
};

version, replace requires clauses with std::enable_if_t SFINAE hack in 及之前)。

如何决定

这里要记住的核心是C++有不止一种多态性,使用错误的种类会让你浪费很多时间。

既有编译时多态,也有运行时多态。当你只需要编译时多态时使用运行时多态是一种浪费。

然后在每个类别中,还有更多的子类型。

std::function是运行时多态类型擦除正则对象。基于继承的虚函数是另一种运行时多态技术。

您的 Y 组合器正在执行编译时多态性。它改变了它存储的内容并公开了一个更统一的界面。

与该接口对话的事物不关心 Y 组合器的内部实现细节,将它们包含在它们的实现中是抽象失败。

evaluate 接受一个可调用的东西并将其传递给 uint 并期望 return 中有一个 uint。这就是它关心的。它 不关心 是否传递给 Functor<Chicken> 或函数指针。

让它关心它是一个错误。

如果取std::function,则执行运行时多态;如果它采用 template<class F>F&& 类型的参数,则它是编译时多态的。这是一个选择,他们是不同的。

任何类型的 Functor<F> 都将合同要求放入其 API 中,根本不应该关心。