2

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

我已经尽可能清楚地总结了我的一个问题:如何传递以 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-combinator 实现都有奇怪的双重包装的原因吗?

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

4

2 回答 2

5

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

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'
 }
于 2021-02-05T17:10:50.927 回答
3

最简单的解决方案是:

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);
}

使用或使用

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);
}

这是相似的只是更模糊。

您可以编写 fix-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

为了使其完全“有用”,您必须将智能复制/移动/分配操作添加到Functor,如果可以复制其中F的每个 s ,则可以将其复制到其中。

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

及之前的std::enable_if_tSFINAE hack替换 requires 子句)。

如何决定

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

有编译时多态性和运行时多态性。当您只需要编译时多态时使用运行时多态是一种浪费。

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

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

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

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

evaluate接受一个可调用的东西并将其传递进去uint并期望一个uint作为回报。这就是它所关心的。它不关心是否传递了一个Functor<Chicken>或函数指针。

让它关心它是一个错误。

如果它需要 a std::function,它会执行运行时多态性;如果它需要 atemplate<class F>的参数类型为F&&,则它是编译时多态的。这是一个选择,它们是不同的。

采取Functor<F>任何一种方式都是将合同要求放在它根本不应该关心的 API 中。

于 2021-02-05T18:09:23.043 回答