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

时间:2016-05-27 11:00:22

标签: c++ c++11 lambda c++14 sizeof

我正在处理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至少应该是指向它的实现的指针吗?

5 个答案:

答案 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.