【问题标题】:Is there a std::function type or similar for lambda with auto parameter?带有 auto 参数的 lambda 是否有 std::function 类型或类似类型?
【发布时间】:2015-07-08 06:39:07
【问题描述】:

当我将 lambda 分配给显式类型的变量时(例如,当它是递归时,以捕获函数本身),我使用 std::function。

以这个愚蠢的“位计数”函数为例:

std::function<int(int)> f;
f = [&f](int x){ return x ? f(x/2)+1 : 0; };

如果我们使用自动参数来泛化x,就像在 C++14 泛型 lambda 中介绍的那样,情况如何?

std::function<int(???)> f;
f = [&f](auto x){ return x ? f(x/2)+1 : 0; };

显然,我不能将auto 放在function 类型参数中。

是否有可能定义一个足够通用的仿函数类以涵盖上述确切情况,但仍使用 lambda 进行函数定义?

(不要过度概括,只接受一个自动参数并对返回值进行硬编码。)用例将适用于上述场景:通过引用捕获函数本身以进行递归调用。

【问题讨论】:

  • 我认为这里的问题是,lamba 函数需要一个确定的类型——auto 只是让编译自己解决。这并不意味着您可以将 lambda 函数传递给其中具有任何类型的任何代码 - 需要在 lambda 函数的编译时知道该类型。
  • auto&amp; f = [&amp;f](auto x){return x ? f(x/2)+1 : 0;}; 怎么样?那样有用吗? (可能只是 auto 而不是 auto&amp;。)我手边没有支持 C++14 的编译器,所以我不能轻易测试它。
  • std::function 使用类型擦除。我相当确定根本不可能编写一个接受任意类型的类型擦除函数对象包装器,因为每组参数类型都需要创建一个新函数,只有当编译器可以看到包装的函数模板时才能做到这一点。解决方法包括使用一组固定的参数类型集(使用现有的重载集/在类型擦除之前预先计算所有重载),或类型擦除参数。
  • @dyp 您可以在参数类型或函数上创建类型擦除,但是在两者上都这样做是难以处理的没有将您的函数(如 boost phoenix)反映到数据中(基本上,在参数类型上重新编译你的函数)。举个具体的例子,想象一下用你的二进制文件发送 gcc,将函数存储为字符串。当传递参数时,您确定它们的类型,打印出该类型的函数,编译动态库,加载它并运行函数。并非完全不可能。并且需要将函数定义为简单的 C++ 代码以外的内容。
  • @dyp 是的。如果你使用 OpenCL 之类的东西,你的内核就是你在运行时编译的字符串。您可以在运行时使用“类型信息”输入来操作所述内核,生成一组无限可能的内核实现中的一个,编译它,然后调用它。当然,除了非常简单的情况外,安全地执行此操作非常困难,这是 C++ 样式模板元编程优于基于字符串的元编程的优势之一。

标签: c++ lambda c++14 auto generic-lambda


【解决方案1】:

您可以创建一个调用自身的 lambda,方法是将其作为参数传递给自身:

auto f = [](auto self, auto x) -> int {
    return x ? self(self, x / 2) + 1 : 0;
};

std::cout << f(f, 10);

然后您可以在另一个 lambda 中捕获该 lambda,因此您不必担心将其传递给自身:

auto f2 = [&f](auto x) {
    return f(f, x);
};

std::cout << f2(10);

【讨论】:

  • 虽然你没有回答实际问题(我想为函子输入一个类型名称),但我想我可以用它来解决我的实际问题(解释太长了 - 我以为我需要明确地给函子一个类型,但我想我可以在我的具体场景中使用你的解决方案来解决这个问题)。
【解决方案2】:

这是一个基于 y-combinator 的快速递归引擎:

template<class F>
struct recursive_t {
  F f;

  // note Self must be an lvalue reference.  Things get
  // strange if it is an rvalue:
  // invoke makes recursive ADL work a touch better.
  template<class Self, class...Args>
  friend auto invoke( Self& self, Args&&...args )
  -> decltype( self.f( self, std::declval<Args>()... ) )
  {
    return self.f( self, std::forward<Args>(args)... );
  }
  // calculate return type using `invoke` above:
  template<class Self, class...Args>
  using R = decltype( invoke( std::declval<Self>(), std::declval<Args>()... ) );

  template<class...Args>
  R<recursive_t&, Args...> operator()(Args&&...args)
  {
    return invoke( *this, std::forward<Args>(args)... );
  }
  template<class...Args>
  R<recursive_t const&, Args...> operator()(Args&&...args)const
  {
    return invoke( *this, std::forward<Args>(args)... );
  }
};

template<class F>
recursive_t< std::decay_t<F> > recurse( F&& f )
{
  return {std::forward<F>(f)};
}

现在你可以这样做了:

auto f = recurse( [](auto&& f, auto x){ return x ? f(x/2)+1 : 0; } );

你会得到一个没有 &amp; 捕获的递归 lambda(这将它的使用限制在当前范围内)。

通过引用捕获 std::function 意味着您的 lambda 的生命周期是当前范围,并且每个递归调用都需要遍历类型擦除(阻止任何可能的优化,如尾递归,通过递归调用)。其他类似的解决方案也是如此。

需要使用recursive_t 而不是使用 lambda,因为 lambda 不能在其自身内命名。

Live example.

基于 lambda 的版本在实现上稍微简单一些。请注意,对于可变和不可变 lambda,您需要不同的类型函数:

template<class F>
auto recurse( F&& f ) {
  return [f=std::forward<F>(f)](auto&&...args){
    return f(f, decltype(args)(args)...);
  };
};

recursive_t 的工作方式如下:

auto fib = recurse( [](auto&& fib, int x){ if (x<2) return 1; return fib(x-1)+fib(x-2); } );

lambda 版本的工作方式如下:

auto fib = recurse( [](auto&& self, int x){ if (x<2) return 1; return self(self, x-1)+self(self,x-2); } );

我个人觉得更尴尬。

recurse 的类型也更难描述。对于recursive_t 版本,recurse 的类型为:

((A->B)->A->B)->(A->B)

这很尴尬,但却是一种有限的类型。

lambda 版本更复杂。 recursive 的函数参数的类型是:

F:= F->A->B

这是令人讨厌的无限,然后recurse 是类型

F->A->(A->B)

继承了无穷大。

无论如何,recurse 返回值可以存储在普通的std::function 中,或者不存储在任何类型擦除的容器中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-21
    • 2012-05-30
    • 2020-12-25
    • 1970-01-01
    相关资源
    最近更新 更多