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

sdg*_*sdh 89 c++ lambda sizeof c++11 c++14

我正在处理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;
}
Run Code Online (Sandbox Code Playgroud)

你可以在这里运行它:http://fiddle.jyt.io/github/b13f682d1237eb69ebdc60728bb52598

ouptut是:

17
0x7d90ba8f626f
1
Run Code Online (Sandbox Code Playgroud)

这表明我的lambda的大小是1.

  • 这怎么可能?

  • lambda至少应该是指向它的实现的指针吗?

Yak*_*ont 106

有问题的lambda实际上没有状态.

检查:

struct lambda {
  auto operator()() const { return 17; }
};
Run Code Online (Sandbox Code Playgroud)

如果有的话lambda f;,那是一个空洞的课程.上面的lambda函数不仅与lambda相似,而且(基本上)是如何实现lambda的!(它还需要一个隐式转换为函数指针运算符,并且该名称lambda将被一些编译器生成的伪指针替换)

在C++中,对象不是指针.它们是实际的东西.它们仅占用将数据存储在其中所需的空间.指向对象的指针可以大于对象.

虽然您可能会将lambda视为指向函数的指针,但事实并非如此.你不能将其重新分配auto f = [](){ return 17; };给不同的函数或lambda!

 auto f = [](){ return 17; };
 f = [](){ return -42; };
Run Code Online (Sandbox Code Playgroud)

以上是非法的.没有空间f来存储将要调用哪个函数 - 该信息存储在类型f,而不是存储在f!

如果你这样做:

int(*f)() = [](){ return 17; };
Run Code Online (Sandbox Code Playgroud)

或这个:

std::function<int()> f = [](){ return 17; };
Run Code Online (Sandbox Code Playgroud)

你不再直接存储lambda.在这两种情况下,f = [](){ return -42; }是合法的-所以在这种情况下,我们都存储功能,我们在价值调用f.并且sizeof(f)不再是1,而是sizeof(int(*)())更大或更大(基本上,指针大小或更大,正如您所期望的那样. std::function标准隐含的最小尺寸(它们必须能够将"内部"卡片存储到一定大小)至少与实际中的函数指针一样大).

在这种int(*f)()情况下,您正在存储一个函数指针,该函数指向一个函数,该函数的行为就像调用了该lambda一样.这仅适用于无状态lambdas(具有空[]捕获列表的lambdas ).

在这种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)分开.

现在,因为它没有状态,有时它不占用空间.当它"孤独"时就不会发生这种情况,但在某些情况下它可能会发生. std::tuple和类似的库代码利用了这个事实.下面是它的工作原理:

由于lambda等同于具有operator()重载的类,无状态lambdas(带有[]捕获列表)都是空类.他们拥有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}; }
Run Code Online (Sandbox Code Playgroud)

sizeof(make_toy( []{std::cout << "hello world!\n"; } ))IS sizeof(int)(当然,以上是非法的,因为你不能在一个非评估环境中创建一个lambda:你必须创建一个名为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}; }
Run Code Online (Sandbox Code Playgroud)

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

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

Sam*_*hik 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;
Run Code Online (Sandbox Code Playgroud)

表示lambda的内部类没有类成员,因此它sizeof()是1(由于其他地方充分说明的原因,它不能为0 ).

如果你的lambda要捕获一些变量,它们将等同于类成员,你的sizeof()意志也会相应地指出.

  • 你可以链接到"其他地方",这解释了为什么`sizeof()`不能为0? (3认同)

Com*_*sMS 26

您的编译器或多或少地将lambda转换为以下结构类型:

struct _SomeInternalName {
    int operator()() { return 17; }
};

int main()
{
     _SomeInternalName f;
     std::cout << f() << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

由于该结构没有非静态成员,因此它与空结构具有相同的大小,即1.

只要向lambda添加非空捕获列表,它就会发生变化:

int i = 42;
auto f = [i]() { return i; };
Run Code Online (Sandbox Code Playgroud)

哪个会翻译成

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;
}
Run Code Online (Sandbox Code Playgroud)

由于生成的struct现在需要int为捕获存储非静态成员,因此其大小将增长到sizeof(int).当您捕获更多东西时,大小将继续增长.

(请将结构类比与一粒盐进行比较.虽然这是推理lambda在内部如何工作的好方法,但这不是编译器将要做的字面翻译)


leg*_*s2k 12

在mimumum,lambda不应该是指向它的实现的指针吗?

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

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

[...]

实现可以定义闭包类型与下面描述的不同,前提是这不会改变程序的可观察行为,只需更改:

- 封闭类型的大小和/或对齐方式,

- 封闭类型是否可以轻易复制(第9条),

- 闭包类型是否为标准布局类(第9条),或

- 闭包类型是否为POD类(第9条)

在你的情况下 - 对于你使用的编译器 - 你得到的大小为1,这并不意味着它是固定的.它可以在不同的编译器实现之间变化


geo*_*ptr 7

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

lambda表达式构造一个未命名的prvalue临时对象,该对象具有唯一的非命名非联合非聚合类类型,称为闭包类型,在包含的最小块作用域,类作用域或命名空间作用域中声明(用于ADL) lambda表达式.

如果lambda表达式通过副本捕获任何内容(隐式使用捕获子句[=]或显式捕获不包含字符&,例如[a,b,c]),则闭包类型包括未命名的非静态数据成员,以未指定的顺序声明,包含所有捕获的实体的副本.

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

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

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