以下代码:
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;
}
使用 g ++ 4.8.2
编译的输出56由于所有局部变量都存储在同一堆栈帧中,因此记住一个指针足以定位所有局部变量的地址。为什么lambda表达式构造了一个如此大的未命名函数对象?
答案 0 :(得分:5)
我不明白你为何感到惊讶。
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;
}
答案 1 :(得分:2)
编译器通过堆栈指针通过引用捕获是合法的。有一个小的缺点(因为必须在所述堆栈指针中添加偏移量)。
在包含缺陷的当前C ++标准下,您还必须通过伪指针捕获引用变量,因为绑定的生命周期必须持续与引用数据一样长,而不是它直接绑定到的引用。
更简单的实现,其中每个捕获的变量对应于构造函数参数和类成员变量,具有与“更正常”的C ++代码对齐的严重优势。需要完成魔法this
的一些工作,但除此之外,lambda闭包是一个带内联operator()
的沼泽标准对象实例。关于“更正常”的C ++代码的优化策略是可行的,错误与“更正常”的代码等大多相同。
如果编译器编写者不再使用堆栈帧实现,那么该实现中引用的引用捕获可能无法像在其他所有编译器中那样工作。当缺陷得到解决(有利于它工作)时,代码将不得不再次更改。从本质上讲,使用更简单实现的编译器几乎肯定会比使用花哨实现的编译器具有更少的错误和更多的工作代码。
通过堆栈帧捕获,必须为该lambda定制lambda的所有优化。它等同于捕获void*
的类,对其执行指针算法,并将结果数据转换为类型化指针。由于指针算法倾向于阻止优化,尤其是堆栈变量之间的指针算法(通常是未定义的),因此优化将非常困难。更糟糕的是,这样的指针算法意味着堆栈变量状态的优化(消除变量,重叠生命周期,寄存器)现在必须以纠缠的方式与lambdas的优化相互作用。
进行这样的优化将是一件好事。作为奖励,因为lambda类型与编译单元相关联,所以搞乱lambda的实现不会破坏编译单元之间的二进制兼容性。因此,一旦它们被证明是稳定的改进,您可以相对安全地进行此类更改。但是,如果你确实实现了这种优化,那么你真的希望能够恢复到更简单的经过验证的优化。
我建议您为自己喜欢的开源编译器提供补丁,以添加此功能。
答案 2 :(得分:1)
因为它是如何实现的。我不知道标准是否说明了它应该如何实现,但我猜它的实现定义了lambda对象在这种情况下会有多大。
编译器存储单个指针并使用偏移量来执行您建议的优化时,没有任何错误。也许有些编译器这样做,我不知道。