我正在处理C ++中一些lambda的记忆,但我对它们的大小感到有些困惑。
这是我的测试代码:
#include <iostream>
#include <string>
int main()
{
auto f = [](){ return 17; };
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
}
您可以在此处运行:http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598
ouptut是:
17
0x7d90ba8f626f
1
这表明我的lambda的大小是1.
这怎么可能?
lambda至少应该是指向它的实现的指针吗?
答案 0 :(得分:106)
有问题的lambda实际上有无状态。
检查:
struct lambda {
auto operator()() const { return 17; }
};
如果我们有lambda f;
,那么它就是一个空类。上面的lambda
不仅在功能上类似于你的lambda,它(基本上)是你的lambda的实现方式! (它还需要一个隐式转换为函数指针运算符,名称lambda
将替换为一些编译器生成的伪指针)
在C ++中,对象不是指针。它们是实际的东西。它们仅占用将数据存储在其中所需的空间。指向对象的指针可能比对象大。
虽然您可能会认为lambda是指向函数的指针,但它并不是。您无法将auto f = [](){ return 17; };
重新分配给其他函数或lambda!
auto f = [](){ return 17; };
f = [](){ return -42; };
以上是非法。 f
中没有空间来存储将要调用哪个函数 - 该信息存储在f
的类型中,而不是在f
!
如果你这样做了:
int(*f)() = [](){ return 17; };
或者这个:
std::function<int()> f = [](){ return 17; };
您不再直接存储lambda。在这两种情况下,f = [](){ return -42; }
都是合法的 - 所以在这些情况下,我们正在以f
的值存储我们正在调用的函数。并且sizeof(f)
不再是1
,而是sizeof(int(*)())
或更大(基本上,指针大小或更大,正如您所期望的那样。std::function
具有标准所隐含的最小尺寸(他们必须能够存储&#34;在他们自己内部&#34;可达到一定大小的小调),这在实践中至少与函数指针一样大。)
在int(*f)()
的情况下,您正在存储一个函数指针,该函数指向一个函数,该函数的行为与您调用该lambda的行为相同。这仅适用于无状态lambda(具有空[]
捕获列表的lambda)。
在std::function<int()> f
的情况下,您正在创建一个类型擦除类std::function<int()>
实例(在本例中)使用placement new来将size-1 lambda的副本存储在内部缓冲区中(并且,如果传入更大的lambda(具有更多状态),则将使用堆分配。
作为猜测,这些东西可能就是您认为正在发生的事情。 lambda是一个对象,其类型由其签名描述。在C ++中,决定对手动函数对象实现进行lambdas 零成本抽象。这允许您将lambda传递给std
算法(或类似算法),并在实例化算法模板时使其内容对编译器完全可见。如果lambda的类型为std::function<void(int)>
,则其内容将不会完全可见,并且手工制作的函数对象可能更快。
C ++标准化的目标是高级编程,与手工编写的C代码相比,零开销。
现在你明白你的f
实际上是无国籍的,你脑子里应该有另一个问题:lambda没有状态。为什么它的大小不是0
?
答案很简短。
C ++中的所有对象在标准下必须具有最小值1,并且相同类型的两个对象不能具有相同的地址。这些是连接的,因为T
类型的数组会将元素放置sizeof(T)
。
现在,由于它没有状态,有时它不占用空间。当它是单独的#34;时不会发生这种情况,但在某些情况下它可能会发生。 std::tuple
和类似的库代码利用了这个事实。以下是它的工作原理:
由于lambda等同于operator()
重载的类,无状态lambdas(带有[]
捕获列表)都是空类。他们有sizeof
1
。实际上,如果你继承它们(这是允许的!),它们将不会占用空间,只要它不会导致相同类型的地址冲突。 (这称为空基优化)。
template<class T>
struct toy:T {
toy(toy const&)=default;
toy(toy &&)=default;
toy(T const&t):T(t) {}
toy(T &&t):T(std::move(t)) {}
int state = 0;
};
template<class Lambda>
toy<Lambda> make_toy( Lambda const& l ) { return {l}; }
sizeof(make_toy( []{std::cout << "hello world!\n"; } ))
是sizeof(int)
(以上是非法的,因为您无法在非评估的上下文中创建lambda:您必须创建一个名为auto toy = make_toy(blah);
然后执行{{ 1}},但那只是噪音)。 sizeof(blah)
仍为sizeof([]{std::cout << "hello world!\n"; })
(类似资格)。
如果我们创建另一种玩具类型:
1
这有 lambda的两个副本。由于他们无法共享相同的地址,template<class T>
struct toy2:T {
toy2(toy2 const&)=default;
toy2(T const&t):T(t), t2(t) {}
T t2;
};
template<class Lambda>
toy2<Lambda> make_toy2( Lambda const& l ) { return {l}; }
为sizeof(toy2(some_lambda))
!
答案 1 :(得分:50)
lambda不是函数指针。
lambda是一个类的实例。您的代码大致相当于:
class f_lambda {
public:
auto operator() { return 17; }
};
f_lambda f;
std::cout << f() << std::endl;
std::cout << &f << std::endl;
std::cout << sizeof(f) << std::endl;
表示lambda的内部类没有类成员,因此其sizeof()
为1(由于充分说明elsewhere的原因,它不能为0。)
如果你的lambda要捕获一些变量,它们将等同于班级成员,而你的sizeof()
将相应地指示。
答案 2 :(得分:26)
您的编译器或多或少地将lambda转换为以下结构类型:
struct _SomeInternalName {
int operator()() { return 17; }
};
int main()
{
_SomeInternalName f;
std::cout << f() << std::endl;
}
由于该结构没有非静态成员,因此它与空结构的大小相同,即1
。
只要向lambda添加非空捕获列表,它就会发生变化:
int i = 42;
auto f = [i]() { return i; };
将转换为
struct _SomeInternalName {
int i;
_SomeInternalName(int outer_i) : i(outer_i) {}
int operator()() { return i; }
};
int main()
{
int i = 42;
_SomeInternalName f(i);
std::cout << f() << std::endl;
}
由于生成的struct现在需要为捕获存储非静态int
成员,因此其大小将增长到sizeof(int)
。当您捕获更多东西时,大小将继续增长。
(请将结构类比与一粒盐进行比较。虽然这是推理lambda在内部如何工作的好方法,但这不是编译器将要做的字面翻译)
答案 3 :(得分:12)
在mimumum,lambda是不是指向它的实现的指针?
不一定。根据标准,唯一的未命名类的大小是实现定义的。摘自 [expr.prim.lambda] ,C ++ 14(强调我的):
lambda-expression的类型(也是闭包对象的类型)是一个唯一的,未命名的nonunion类类型 - 称为闭包类型 - 其属性如下所述。
[...]
实现可以定义闭包类型与下面描述的不同,前提是这不会改变程序的可观察行为而不是通过更改:
- 封闭类型的大小和/或对齐,
- 闭包类型是否可以轻易复制(第9条),
- 闭包类型是标准布局类(第9条)还是
- 闭包类型是否为POD类(第9条)
在您的情况下 - 对于您使用的编译器 - 您的大小为1,这并不意味着它已修复。它可以在不同的编译器实现之间变化。
答案 4 :(得分:7)
来自http://en.cppreference.com/w/cpp/language/lambda:
lambda表达式构造一个未命名的prvalue临时对象,该对象具有唯一的非命名非联合非聚合类类型,称为闭包类型,在最小的块中声明(出于ADL的目的)包含lambda表达式的范围,类范围或命名空间范围。
如果lambda表达式通过副本捕获任何内容(隐式使用捕获子句[=]或显式捕获不包含字符&amp;,例如[a,b,c] ),闭包类型包括未命名的非静态数据成员,以未指定的顺序声明,保存所有捕获的实体的副本。
对于通过引用捕获的实体(使用默认捕获[&amp;]或使用字符&amp;,例如[&amp; a,&amp; b,&amp; c]) ,如果在闭包类型
中声明了其他数据成员,则未指定
来自http://en.cppreference.com/w/cpp/language/sizeof
当应用于空类类型时,始终返回1.