当C++lambda表达式有大量引用捕获时,未命名函数对象的大小就会变大

When a C++ lambda expression has a lot of captures by reference, the size of the unnamed function object becomes large

本文关键字:对象 函数 未命名 表达式 C++lambda 引用      更新时间:2023-10-16

以下代码:

int main() {
    int a, b, c, d, e, f, g;
    auto func = [&](){cout << a << b << c << d << e << f << g << endl;};
    cout << sizeof(func) << endl;
    return 0;
}

输出56用g++4.8.2编译

由于所有局部变量都存储在同一堆栈帧中,记住一个指针就足以定位所有局部变量的地址。为什么lambda表达式构造一个如此大的未命名函数对象?

我不明白你为什么看起来很惊讶。

C++标准给出了一组需求,每个实现都可以自由选择任何符合需求的策略。

为什么实现会优化lambda对象的大小?

具体地说,你意识到这将如何将这个lambda的生成代码与周围函数的生成代码联系起来吗?

说起来很容易嘿!这是可以优化的,但要真正优化并确保它在所有边缘情况下都能工作要困难得多。所以,就我个人而言,我更喜欢一个简单有效的实现,而不是一个拙劣的优化尝试…

特别是当工作如此容易时:

struct S { int a, b, c, d, e, f, g; };
int main() {
    S s = {};
    auto func = [&](){
        std::cout << s.a << s.b << s.c << s.d << s.e << s.f << s.g << "n";
    };
    std::cout << sizeof(func) << "n";
    return 0;
}

看马:只有4个字节!

编译器通过堆栈指针引用捕获是合法的。有一个轻微的缺点(因为必须向所述堆栈指针添加偏移)。

在当前包含缺陷的C++标准下,您还必须通过伪指针捕获引用变量,因为绑定的生存期必须与引用的数据一样长,而不是与它直接绑定的引用一样长。

更简单的实现,每个捕获的变量都对应于一个构造函数参数和类成员变量,具有与"更普通"的C++代码一致的显著优势。需要为magic this做一些工作,但除此之外,lambda闭包是一个带有内联operator()的bog标准对象实例。对"更正常"的C++代码的优化策略将起作用,错误将主要与"更正常的"代码相同,等等。

如果编译器编写人员使用堆栈框架实现,那么该实现中引用的引用捕获可能无法像在其他编译器中那样工作。当缺陷得到解决(有利于它工作)时,代码将不得不再次更改。本质上,使用更简单实现的编译器几乎肯定会比使用花哨实现的编译器有更少的错误和更多的工作代码。

使用堆栈帧捕获,lambda的所有优化都必须针对该lambda进行自定义。它相当于一个类,该类捕获void*,对其执行指针运算,并将生成的数据强制转换为类型化指针。这将是非常难以优化的,因为指针算法往往会阻碍优化,尤其是堆栈变量之间的指针算法(通常未定义)。更糟糕的是,这种指针算法意味着堆栈变量状态的优化(消除变量、重叠寿命、寄存器)现在必须以纠缠的方式与lambdas的优化相互作用。

进行这样的优化将是一件好事。另外,由于lambda类型与编译单元绑定,因此干扰lambda的实现不会破坏编译单元之间的二进制兼容性。因此,一旦这些更改被证明是稳定的改进,您就可以相对安全地进行这些更改。然而,如果你真的实现了优化,你真的会希望能够恢复到更简单、经过验证的优化。

我鼓励您为您最喜欢的开源编译器提供补丁,以添加此功能。

因为它就是这样实现的。我不知道这个标准是否说明了它应该如何实现,但我想它的实现定义了在这种情况下lambda对象的大小。

对于编译器来说,存储一个指针并使用偏移量来执行您建议的优化并没有什么错。也许有些编译器会这么做,我不知道。