【问题标题】:Does the compiler generate a different type for each lambda?编译器是否为每个 lambda 生成不同的类型?
【发布时间】:2017-05-15 02:54:01
【问题描述】:

免责声明:请勿在此问题中使用代码。它调用未定义的行为。问题的核心陈述,编译器是否为每个lambda生成一个新的类型,对应的答案仍然有效。

为了通过捕获获取指向 lambda 的函数指针,我想出了以下技巧:

auto f = [&a] (double x) { return a*x; };
static auto proxy = f;
double(*ptr)(double) = [] (double x) { return proxy(x); };
// do something with ptr

我将带有捕获(可能是函数参数)的 lambda 分配给函数内部的静态变量,因此在另一个 lambda 中使用它时我不必捕获它。然后,另一个无捕获 lambda 可以愉快地衰减为一个函数指针,我可以将它传递给某个共享库。

现在可以尝试概括一下:

template < typename F >
decltype(auto) get_ptr(F f)
{
  static auto proxy = f;
  return [] (auto ... args) { return proxy(args...); };
}

但是,由于proxy 是一个静态变量,它会在每次调用该函数时被覆盖。因为它是一个模板,我认为只有当我调用相同的实例化时才会被覆盖,即每个实例化都有自己的静态proxy

但是,以下工作:

#include <cassert>

template < typename F >
decltype(auto) get_ptr(F f)
{
  static auto proxy = f;
  return [] (auto ... args) { return proxy(args...); };
}

int main()
{
  auto f1 = [ ](double,double) { return 1; };
  auto f2 = [=](double,double) { return 2; };
  auto f3 = [&](double,double) { return 3; };
  auto f4 = [&](double,double) { return 4; };
  auto f5 = [ ](double,double) { return 5; };

  int(*p1)(double,double) = get_ptr(f1);
  int(*p2)(double,double) = get_ptr(f2);
  int(*p3)(double,double) = get_ptr(f3);
  int(*p4)(double,double) = get_ptr(f4);
  int(*p5)(double,double) = get_ptr(f5);

  assert( p1(0,0) == 1 );
  assert( p2(0,0) == 2 );
  assert( p3(0,0) == 3 );
  assert( p4(0,0) == 4 );
  assert( p5(0,0) == 5 );
}

这对于get_ptr(f1)get_ptr(f5) 看起来很可疑,人们可能期望推断出相同的类型。然而,lambdas 是编译器生成的结构,似乎编译器会为每个 lambdas 生成不同的类型,不管以前的 lambdas 是否看起来可以重用。

所以在编译器肯定会为每个 lambda 生成不同类型的情况下,上述技巧对我来说非常有用。如果不是这种情况,我的 hack 的概括是没有用的。

【问题讨论】:

  • f1f5 无论如何不能是相同的类型,因为一个 lambda 返回 1,另一个返回 5。它们的 operator() 实现必然不同。
  • (草案)规范说如下(在 5.1.2.3 中): lambda 表达式的类型(也是闭包对象的类型)是唯一的、未命名的非-union 类类型——称为闭包类型...
  • @Yuushi 完美。请写出答案!
  • 我喜欢这个关于编译器如何处理和处理 lambda 以及 cmets 的概括性问题,对此的公认答案为回顾提供了很好的参考。这是我的最爱!
  • @HenriMenke 我不认为它会调用未定义的行为。这是已定义的行为,但(根据我的回答)可能是 surprising 行为。

标签: c++ templates lambda c++14


【解决方案1】:

来自规范草案(特别是 n3376),5.1.2.3(强调我的)。

lambda 表达式的类型(也是闭包对象的类型)是一个唯一,未命名的非联合类类型——称为闭包类型...

【讨论】:

    【解决方案2】:

    您实际上所做的是创建了一个持有 lambda 副本的单例,每个类型的 lambda 都是不同的(每个 [expr.prim.lambda.closure]/1 表示每个 lambda 表达式)。这通常是有效的,因为 lambda 是不可变的,但是,可变的 lambda 会揭示其中的区别:

    auto f6 = [i = 1]( double, double ) mutable { return 6 * i++; };
    int(*p6)(double,double) = get_ptr(f6);
    
    assert( p6(0,0) == 6 );
    assert( p6(0,0) == 12 );
    
    int(*p6_prime)(double,double) = get_ptr(f6);
    assert( p6_prime(0,0) == 18 ); // (!) p6_prime === p6
    assert( p6(0,0) == 24 );
    
    assert( f6(0,0) == 6 );
    assert( f6(0,0) == 12 );
    assert( f6(0,0) == 18 );
    assert( f6(0,0) == 24 );
    

    Live demo on wandbox

    原因是您原始文章中的假设不正确:

    但是,由于代理是一个静态变量,它会在每次调用函数时被覆盖。

    那不是真的。 static 变量设置一次,第一次调用该函数。对同一函数的进一步调用不会评估该语句,因此proxy 将保留第一次调用get_ptr 的特定模板实例化时的值。

    通常你可以将static 声明和赋值分开,但在这种情况下你不能,因为 lambdas 还没有默认构造函数。如果你这样做了,那么在 lambda 上第二次调用 get_ptr 将重置 first 调用返回的指针正在使用的 lambda,将上述结果更改为:

    int(*p6_prime)(double,double) = get_ptr(f6);
    assert( p6_prime(0,0) == 6 );
    assert( p6(0,0) == 12 ); // (!) p6 === p_prime
    

    【讨论】:

    • 感谢您的回答和cmets。是的,确实,我也注意到这段代码有各种各样的问题。尽管如此,我的问题的核心,编译器是否为每个 lambda 生成不同的类型,得到了回答。我可能很快会编辑这个问题,所以没有人会想到实际使用这段代码。
    • @HenriMenke 告诉你一个秘密:当我们到达这里时我们会做什么:我们阅读了你的问题的标题(1 行),我们立即跳到答案,“好的”。继续生活。不要打扰编辑,只需保留您的原始内容以保持历史完整性。
    • @v.oddou 我确实保留了原件。我只在上面添加了免责声明。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-25
    • 2022-08-11
    • 2013-03-14
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多