【问题标题】:Why does a lambda have a size of 1 byte?为什么 lambda 的大小为 1 个字节?
【发布时间】:2016-09-25 16:37:00
【问题描述】:

我正在使用 C++ 中的一些 lambdas 的内存,但我对它们的大小有点困惑。

这是我的测试代码:

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

输出是:

17
0x7d90ba8f626f
1

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

  • 这怎么可能?

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

【问题讨论】:

  • 其实现为函数对象(structoperator()
  • 空结构的大小不能为 0,因此结果为 1。尝试捕捉一些东西,看看大小会发生什么变化。
  • 为什么 lambda 应该是一个指针???它是一个具有调用运算符的对象。
  • C++ 中的 Lambda 存在于编译时,并且调用在编译或链接时被链接(甚至内联)。因此,对象本身不需要 runtime 指针。 @KerrekSB 期望 lambda 包含函数指针并不是一个不自然的猜测,因为大多数实现 lambda 的语言比 C++ 更具动态性。
  • @KerrekSB “什么重要”——在什么意义上? 原因闭包对象可以为空(而不是包含函数指针)是因为要调用的函数在编译/链接时是已知的。这是OP似乎误解的内容。我看不出你们的 cmets 是如何澄清事情的。

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


【解决方案1】:

有问题的 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 中没有空间存储要调用的 which 函数——该信息存储在 ftype 中,而不是f!

如果你这样做了:

int(*f)() = [](){ return 17; };

或者这个:

std::function<int()> f = [](){ return 17; };

您不再直接存储 lambda。在这两种情况下,f = [](){ return -42; } 都是合法的——所以在这些情况下,我们将 which 函数存储在 f 的值中。并且sizeof(f) 不再是1,而是sizeof(int(*)()) 或更大(基本上,如您所料,指针大小或更大。std::function 具有标准暗示的最小大小(它们必须能够存储“在它们自己内部”可调用的大小达到一定大小)在实践中至少与函数指针一样大)。

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

std::function&lt;int()&gt; f 的情况下,您正在创建一个类型擦除类std::function&lt;int()&gt; 实例,该实例(在这种情况下)使用placement new 将size-1 lambda 的副本存储在内部缓冲区中(并且,如果传入更大的 lambda(具有更多状态),将使用堆分配)。

作为猜测,您认为可能会发生这样的事情。 lambda 是一个对象,其类型由其签名描述。在 C++ 中,决定对手动函数对象实现进行 lambdas零成本 抽象。这使您可以将 lambda 传递给 std 算法(或类似算法),并在编译器实例化算法模板时使其内容对编译器完全可见。如果 lambda 具有像 std::function&lt;void(int)&gt; 这样的类型,则其内容将不会完全可见,而手工制作的函数对象可能会更快。

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 &lt;&lt; "hello world!\n"; } ))sizeof(int)(好吧,上面的内容是非法的,因为您不能在未评估的上下文中创建 lambda:您必须创建一个名为 auto toy = make_toy(blah); 然后执行 sizeof(blah),但这只是噪音)。 sizeof([]{std::cout &lt;&lt; "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

【讨论】:

  • Nit:函数指针可以小于 void*。两个历史例子:首先是字寻址机器,其中 sizeof(void*)==sizeof(char*) > sizeof(struct*) == sizeof(int*)。 (void* 和 char* 需要一些额外的位来保存一个字中的偏移量)。其次是 8086 内存模型,其中 void*/int* 是段+偏移量,可以覆盖所有内存,但函数适合单个 64K 段(所以函数指针只有 16 位)。
  • @martin true。添加了额外的()
【解决方案2】:

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,原因已充分说明 elsewhere)。

如果您的 lambda 要捕获一些变量,它们将等同于类成员,您的 sizeof() 将相应地指出。

【讨论】:

  • 能否链接到“其他地方”,这就解释了为什么sizeof() 不能为0?
【解决方案3】:

您的编译器或多或少地将 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)。随着您捕获的内容越多,大小会不断增长。

(请用一粒盐来类比 struct。虽然这是一种很好的方式来解释 lambdas 如何在内部工作,但这并不是编译器将要做什么的字面翻译)

【讨论】:

    【解决方案4】:

    lambda 不应该至少是一个指向其实现的指针吗?

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

    lambda 表达式的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型——称为闭包类型——其属性如下所述。

    [...]

    一个实现可以定义不同于下面描述的闭包类型,前提是这不会改变程序的可观察行为除了改变

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

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

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

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

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

    【讨论】:

    • 您确定此位适用吗?没有捕获组的 lambda 并不是真正的“闭包”。 (标准是否将空捕获组 lambdas 称为“闭包”?)
    • 是的,确实如此。这就是标准所说的“对 lambda 表达式的求值会产生一个 prvalue 临时。这个临时称为闭包对象。”,无论是否捕获,它都是一个闭包对象,只是它会没有upvalues。
    • 我没有投反对票,但可能投反对票的人认为这个答案没有价值,因为它没有解释为什么这是可能的(从理论的角度来看,而不是标准观点)来实现 lambda,而不包括指向调用操作符函数的运行时指针。 (请参阅我在问题下与 KerrekSB 的讨论。)
    【解决方案5】:

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

    lambda 表达式构造一个唯一的未命名非联合非聚合类类型的未命名纯右值临时对象,称为闭包类型,它在最小块中声明(为了 ADL 的目的)包含 lambda 表达式的作用域、类作用域或命名空间作用域。

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

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

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

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

    【讨论】:

      猜你喜欢
      • 2012-08-28
      • 2018-08-15
      • 2018-06-20
      • 1970-01-01
      • 2017-01-03
      • 2015-09-01
      • 2021-05-19
      • 2019-12-16
      • 1970-01-01
      相关资源
      最近更新 更多