为什么lambda的大小为1个字节?


89

我正在使用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

输出是:

17
0x7d90ba8f626f
1

这表明我的λ大小为1。

  • 这怎么可能?

  • lambda至少不应该是其实现的指针吗?


17
将其实现为功能对象(struct带有的a operator()
george_ptr

14
空结构的大小不能为0,因此结果为1。尝试捕捉一些东西,看看会发生什么变化。
Mohamad Elghawi '16

2
为什么lambda应该是指针???这是一个具有调用运算符的对象。
Kerrek SB

7
C ++中的Lambda在编译时存在,并且调用在编译或链接时被链接(或内联)。因此,在对象本身中不需要运行时指针。@KerrekSB期望lambda包含一个函数指针并不是不自然的猜测,因为大多数实现lambda的语言比C ++更动态。
凯尔·斯特兰德

2
@KerrekSB“重要”-在什么意义上?的原因的封闭的对象可以是空的(而不是包含一个函数指针)是因为要被调用的函数是在编译/链接时已知的。OP似乎对此有误解。我看不到您的评论如何使事情变得清晰。
凯尔·斯特兰德

Answers:


107

所讨论的lambda实际上没有状态

检查:

struct lambda {
  auto operator()() const { return 17; }
};

如果有的话lambda f;,这是一个空的类。上面的lambda功能不仅与lambda类似,而且(基本上)是lambda的实现方式!(它还需要隐式转换为函数指针运算符,并且该名称lambda将被编译器生成的伪guid替换)

在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具有标准所隐含的最小大小(它们必须能够将“内部”可调用对象存储为一定大小),该大小实际上至少与函数指针一样大)。

在这种int(*f)()情况下,您将存储一个函数指针,该函数指针的行为与调用lambda时的行为相同。这仅适用于无状态Lambda([]捕获列表为空的Lambda)。

在这种std::function<int()> f情况下,您要创建一个类型擦除类std::function<int()>实例(在这种情况下),该实例使用placement new将一个大小为1的lambda的副本存储在内部缓冲区中(并且,如果传入的lambda较大(状态更多) ),将使用堆分配)。

猜想,您可能正在想像这样的事情。λ是一个对象,其类型由其签名描述。在C ++中,决定通过手动函数对象实现使Lambdas 零成本抽象。这使您可以将lambda传递给std算法(或类似算法),并在实例化算法模板时使编译器完全看到其内容。如果lambda的类型为std::function<void(int)>,则其内容将不完全可见,而手工制作的函数对象可能会更快。

C ++标准化的目标是在手工C代码上实现零开销的高级编程。

既然您已经知道自己f实际上是无状态的,那么您的脑海中应该还有另一个问题:lambda没有状态。为什么没有大小0呢?


有一个简短的答案。

根据标准,C ++中的所有对象的最小大小都必须为1,并且相同类型的两个对象不能具有相同的地址。这些是连接的,因为类型数组T会将元素sizeof(T)分开放置。

现在,由于它没有状态,因此有时它不会占用空间。当它是“单独的”时,这不可能发生,但是在某些情况下可能会发生。 std::tuple类似的库代码利用了这一事实。下面是它的工作原理:

由于lambda等效于具有operator()重载的类,因此无状态lambda(具有[]捕获列表)都是空类。他们拥有sizeof1。实际上,如果您从它们继承(允许!),它们将不占用任何空间,只要它不会引起相同类型的地址冲突即可。(这被称为空基础优化)。

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)(当然,以上是非法的,因为你不能建立在非评估方面拉姆达:你必须创建一个名为auto toy = make_toy(blah);那么做sizeof(blah),但是这仅仅是噪声)。 sizeof([]{std::cout << "hello world!\n"; })仍然1(类似资格)。

如果我们创建另一个玩具类型:

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}; }

这有两个 lambda 副本。因为他们不能共享相同的地址,所以sizeof(toy2(some_lambda))2


6
Nit:函数指针可以小于void *。两个历史示例:首先是用字寻址的机器,其中sizeof(void *)== sizeof(char *)> sizeof(struct *)== sizeof(int *)。(void *和char *需要一些额外的位来保存一个字内的偏移量)。其次是8086内存模型,其中void * / int *是segment + offset,可以覆盖所有内存,但是函数适合单个64K段(因此函数指针只有16位)。
马丁·邦纳

1
@马丁是真的。额外()添加。
Yakk-亚当·内夫罗蒙特

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(由于其他地方有足够的说明,它不能为0 )。

如果您的lambda捕获了一些变量,它们将等同于类成员,并且您sizeof()将相应地进行指示。


3
您能否链接到“其他地方”,这说明了为什么sizeof()不能为0?
user1717828

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;
}

由于生成的结构现在需要存储非静态int成员以进行捕获,因此其大小将增加为sizeof(int)。随着您捕获更多内容,大小将保持增长。

(请以结构类比为例)。虽然这是推断lambda如何在内部工作的好方法,但这不是编译器将进行的字面翻译。)


12

lambda至少不能成为其实现的指针吗?

不必要。根据该标准,唯一的未命名类的大小由实现定义。摘自[expr.prim.lambda],C ++ 14(重点是我的):

lambda-expression的类型(也是闭包对象的类型)是唯一的,未命名的ununion类类型(称为闭包类型),其属性如下所述。

[...]

一个实现可以定义与以下描述不同的闭包类型,但前提是这不会改变程序的可观察行为,除非通过更改

—闭合类型的大小和/或对齐方式

—闭包类型是否可微复制(第9条),

—闭包类型是标准布局类(第9条),还是

—闭合类型是否为POD类(第9条)

对于您的情况(对于您使用的编译器),大小为1,这并不意味着它是固定的。在不同的编译器实现之间,它可能有所不同。


您确定该位适用吗?没有捕获组的lambda并不是真正的“关闭”。(标准是否将空捕获组的lambda称为“关闭”?)
凯尔·斯特兰德

1
是的,它确实。这就是标准所说的“ 对lambda表达式的求值会导致一个prvalue临时对象。此临时对象称为闭包对象。 ”,无论是否捕获,它都是一个闭包对象,只是一个没有上值。
legends2k

我没有拒绝投票,但也许拒绝投票的人认为这个答案没有价值,因为它没有解释为什么(从理论角度,而不是从标准角度)实现lambda而不包含运行时指针的原因。呼叫操作员功能。(请参阅我在问题下与KerrekSB的讨论。)
Kyle Strand

7

http://en.cppreference.com/w/cpp/language/lambda

lambda表达式构造一个唯一的未命名非工会非聚合类类型的未命名prvalue临时对象,该类类型称为闭包类型,在包含以下内容的最小块范围,类范围或名称空间范围中声明(出于ADL的目的) lambda表达式。

如果lambda-expression通过复制捕获任何内容(使用捕获子句[=]隐式捕获,或使用不包含字符&的捕获显式捕获,例如[a,b,c]),则闭包类型将包含未命名的非静态数据按未指定顺序声明的member 成员,这些成员包含所有如此捕获的实体的副本。

对于通过引用捕获的实体(使用默认捕获[&]或使用字符&时,例如[&a,&b,&c]),未指定是否在闭包类型中声明其他数据成员

来自http://en.cppreference.com/w/cpp/language/sizeof

应用于空类类型时,始终返回1。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.