6

以下代码:

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 表达式会构造一个这么大的未命名函数对象?

4

3 回答 3

6

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

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个字节!

于 2014-04-18T08:12:11.970 回答
3

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

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

更简单的实现,其中每个捕获的变量对应于构造函数参数和类成员变量,具有与“更正常”的 C++ 代码对齐的严重优势。需要做一些魔术this工作,但除此之外,lambda 闭包是一个带有 inline 的沼泽标准对象实例operator()。“更正常”的 C++ 代码的优化策略将起作用,错误将与“更正常”的代码大部分相同,等等。

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

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

进行这样的优化将是一件好事。作为奖励,因为 lambda 类型与编译单元相关联,弄乱 lambda 的实现不会破坏编译单元之间的二进制兼容性。因此,一旦它们被证明是稳定的改进,您就可以相对安全地进行此类更改。但是,如果您确实实施了该优化,您真的会希望能够恢复到更简单的经过验证的优化。

我鼓励你为你最喜欢的开源编译器提供补丁来添加这个功能。

于 2015-07-09T14:33:36.560 回答
1

Because that's how it's implemented. I don't know if the standard says anything about how it should be implemented but I guess it's implementation defined how big a lambda object will be in that situation.

There would be nothing wrong for a compiler to store a single pointer and use the offsets, to do what you suggest, as an optimization. Perhaps some compilers do that, I don't know.

于 2014-04-18T07:41:21.550 回答