【问题标题】:Function to Lambda函数到 Lambda
【发布时间】:2016-03-03 21:53:06
【问题描述】:

在 C++ 中使用 lambda 和函数对象而不是函数指针通常很有用。原因是 lambda 或函数对象的类型完全编码了被调用的内容(因此允许内联),而函数指针的类型仅编码签名。我有一个有趣的想法,即在使用泛型代码创建调用给定函数的 lambda 时它可能很有用。换句话说,给定指定函数的高阶函数返回调用它的 lambda。为了避免任何间接,必须使用函数指针非类型模板参数。问题是要指定这样一个非类型模板参数,你必须知道函数的签名。我无法绕过需要两次传递有问题的函数:一次是推断类型,一次是实际专注于函数。我最好的尝试是这样的:

template <class T>
struct makeLambdaHelper;

template <class R, class ... Args>
struct makeLambdaHelper<R(*)(Args...)>
{
  template <void(*F)(Args...)>
  static auto blah() {
    return [] (Args && ... args) {
      return F(std::forward<Args>(args)...);
    };
  }
};

要使用它,你会这样做:

void f(int, int) { std::cerr << "f\n"; };
...
auto lam = makeLambdaHelper<decltype(&f)>::blah<f>();
lam(0,0);

当 f 的类型作为模板参数传递给 makeLambdaHelper 时,首先推导出它的签名。然后结构体有一个静态函数,声明了一个非类型模板参数;所以我们通过 f 本身,并得到一个 lambda。像这样必须通过两次 f 非常难看;是否有可能做得更好,也许通过使用默认模板参数做一些聪明的事情?

【问题讨论】:

  • 嗯...你的意思是像std::bind?
  • 绑定某处是否有函数指针模板参数的特殊情况?如果没有,那么 lambda 内部会有一层间接,这对我没有帮助。我从这次谈话 (youtube.com/watch?v=zt7ThwVfap0) 中的理解是通常避免绑定。
  • bind 专门用于函数指针、成员函数指针、绑定到 shared_pointers 的成员函数等。为什么要避免绑定?它是标准库的一部分,因此能够使用您的标准兼容代码无法使用的魔术技巧。
  • 如您所见,bind 在移除间接性方面并不成功,这与我的版本不同:goo.gl/hDk4mZ。此外,使用 bind 你必须指定笨重的 _1、_2 等。
  • 并且您应该明确声明您这样做只是为了解决一些编译器使用 lambda 优化代码比使用仿函数的代码略好一些的问题。

标签: c++ templates c++11 c++14


【解决方案1】:

如果你只是想在函数对象中封装一个函数指针,我推荐一个宏:

#define WRAP_FN(f) [](auto&&... args) -> decltype(auto) { \
    return f(std::forward<decltype(args)>(args)...); };

用法如下:

auto lam = WRAP_FN(foo);

宏的优点是它会额外处理 重载 名称 - 这样您就可以将重载函数传递给 std 算法,并让它做你想做的事情。

【讨论】:

  • 在不支持泛型 lambda 的 C++11 中不起作用。
  • 好吧,如果我们使用宏,我的解决方案也可以包含在宏中。您的解决方案允许重载,这很好,但我的解决方案保留了正确的签名,并且不是具有优势的多态 lambda。不过,这至少是一个不错的选择。
  • @NirFriedman 函数指针已经保留了正确的签名。如果不帮助重载名称,为什么需要函数对象?
  • @Barry 性能原因一般。如果您将函数指针传递给例如std::sort 作为比较器,它不会被内联(除非整个排序被内联,这不太可能)。如果您首先使用我或您的技术将其转换为 lambda,它将是。
  • @Barry 不知道你为什么会这样,我怀疑这只是一个误传。当您使用 f 之类的函数调用典型的模板化高阶函数(例如 std::sort)时,它为 Comparator 推导出的类型将是 bool(*)(double, double) 或类似的东西。因此,如果 sort 本身没有被内联,它没有足够的信息来内联 f,它必须在运行时依赖传递的地址。但是,如果您传递一个调用 f 的 lambda,则 f 将内联到当前范围的 lambda 中。 Sort 为 lambda 推导出一个独特的类型,为它提供足够的信息来内联
【解决方案2】:

鉴于您的代码的示例用法;

makeLambdaHelper<decltype(&f)>::blah<f>();

您声明一个类,使用函数f 的签名(使用decltype),然后在对函数blah 的调用中再次引用函数f - 您引用f 两次,一次用于值(指针),一次用于类型(签名)。我认为您可以将此处的语法最小化并将它们组合起来(使用模板类型推导)。

void f(int, int) { std::cout << "f\n"; }

template <class R, class ... Args>
auto make_lambda(R(*F)(Args...))
{
    return [F] (Args && ... args) {// F pointer captured by value
      return F(std::forward<Args>(args)...);
    };
}

int main()
{
    auto lam = make_lambda(&f);
    lam(0,0);
}

Demo code.

函数make_lambda将指针指向函数f一次,并使用模板推导得到所需的签名。


正如 cmets 中所述,问题的合理部分是在生成的代码中没有间接(包括尾调用)的问题;间接性取决于许多因素,包括使用的编译器、应用的优化级别以及函数f 的内联能力。有关change in compiler、the complexity of the function 的更改和to the "inline-ability" of f 的更改,请参阅这些示例;所有这些都会产生不同的间接结果、尾调用和内联代码。正如您在 cmets 中所指出的,重要的是 f 可以(并且是)内联(这通常取决于编译器优化)开始;否则,所有选项之间几乎没有区别。

在语法上,除了宏之外,当模板参数需要类型和值(这里的指针)时,类模板似乎几乎没有什么可以做的;函数模板可以根据值推断类型。 请记住,如果间接的性能在应用程序中是可测量的并且显着,那么您可能需要硬着头皮使用较长的语法形式,否则倾向于更易于维护的较短和明显的语法(任何人其中).


鉴于原来的语法,可以稍微清理一下;并不是说它改善了任何基本问题,只是在模板语法中将类型和值“靠近”放置有助于代码的维护(当然,您的结果可能会有所不同);

template <class F, F>
struct lambda_maker;

template <class R, class ... Args, R(*F)(Args...)>
struct lambda_maker<R(Args...), F>
{
  static auto make() {
    return [] (Args && ... args) {
      return F(std::forward<Args>(args)...);
    };
  }
};
// ...
auto lam = lambda_maker<decltype(f), f>::make();
lam(2, 3);

【讨论】:

  • 无法删除间接性,将您的方法的程序集与我的方法进行比较:goo.gl/IoxcW2。
  • 我明白了,但是编译器的改变也会改变。 goo.gl/37zsIl。其中一些将取决于编译器及其优化。答案中的运行时参数确实有所变化。我会考虑一些替代方案,然后再讨论这个问题。
  • 酷,谢谢。各种版本的 gcc 生成的代码与 clang 有很大的不同(我发现它的结果更直观),所以这很有趣。
  • 缺少宏,当模板参数需要类型和值(此处的指针)时,模板似乎几乎没有什么可以做的。请记住,如果间接的性能在应用程序中是可测量的并且很重要,那么您可能需要咬紧牙关并使用较长的语法形式,否则倾向于更易于维护的较短和明显的语法(其中任何一个) .
  • 无论哪种方式,有趣的是,如果您将被调用的函数修改为更复杂,goo.gl/KBJd5v,那么所有变体都会产生间接代码和内联代码的混合。如果我想到或找到别的东西,我会告诉你...
猜你喜欢
  • 2020-09-23
  • 1970-01-01
  • 1970-01-01
  • 2021-11-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多