为什么lambda表达式的大小是1字节?

94

我正在处理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;
}

输出结果为:

17
0x7d90ba8f626f
1

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

  • 这怎么可能?

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


18
它被实现为一个函数对象(一个带有 operator()struct)。 - george_ptr
15
一个空结构体的大小不能为0,这就是为什么结果是1。尝试捕获一些内容,并观察大小会发生什么。 - Mohamad Elghawi
3
为什么lambda应该是一个指针?它是一个具有调用运算符的对象。 - Kerrek SB
7
C++中的Lambda表达式存在于编译时,调用在编译或链接时被链接(甚至是内联)。因此,在对象本身中没有必要有运行时指针。@KerrekSB认为Lambda表达式包含函数指针并不是一个不自然的猜测,因为大多数实现Lambda表达式的语言比C++更加动态。 - Kyle Strand
2
@KerrekSB “what matters” - 在什么意义上?闭包对象可以为空(而不是包含函数指针)的原因是因为要调用的函数在编译/链接时已知。这就是OP似乎误解的地方。我不明白你的评论如何澄清事情。 - Kyle Strand
显示剩余2条评论
5个回答

117

需要翻译的内容:

这个 lambda 实际上没有状态。

看一下代码:

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

如果我们有lambda f;,那它就是一个空类。上述的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(带有空的[]捕获列表)。
std::function<int()> f的情况下,您正在创建一个类型擦除类std::function<int()>实例,该实例(在此情况下)使用放置new来将大小为1的lambda副本存储在内部缓冲区中(如果传入了更大的lambda(具有更多状态),则会使用堆分配)。
猜测可能是您认为正在发生的事情之一。lambda是一个对象,其类型由其签名描述。在C++中,决定使lambda成为零成本抽象,覆盖手动函数对象实现。这使您可以将lambda传递到std算法(或类似算法)中,并在实例化算法模板时使其内容完全可见于编译器。如果lambda具有像std::function<void(int)>这样的类型,则其内容将不会完全可见,手工制作的函数对象可能会更快。
C++标准化的目标是高级编程与手工制作的C代码的零开销。
现在您已经了解到您的f实际上是无状态的,应该在您的脑海中有另一个问题:lambda没有状态。为什么它的大小不为0
简短的答案如下:
C++中的所有对象都必须具有标准下的最小大小为1,并且相同类型的两个对象不能具有相同的地址。这些是相关的,因为类型为T的数组将以sizeof(T)的间隔放置元素。
现在,由于它没有状态,有时它可以不占用空间。当它“单独”时,这种情况不会发生,但在某些情况下会发生。std::tuple和类似的库代码利用了这个事实。以下是它的工作原理:
作为等同于重载了operator()的类,无状态的lambda表达式(使用[]捕获列表)都是空类。它们的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); 然后执行 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


7
注:函数指针可能比void更小。两个历史例子:首先是字寻址机器,在这种机器上sizeof(void) == sizeof(char*) > sizeof(struct*) == sizeof(int*)。(void和char需要一些额外的位来保存字内偏移量)。其次是8086存储模型,其中void*/int*是段地址加偏移地址,可以覆盖所有内存,但函数适合于单个64K段(因此函数指针只有16位)。 - Martin Bonner supports Monica
1
@martin 真。添加了额外的 () - Yakk - Adam Nevraumont

54
一个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
你能否提供一个“elsewhere”的链接,解释为什么sizeof()不能为0? - user1717828

27

你的编译器会将 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内部工作方式的方法,但这不是编译器将要做的字面转换)


13
这个lambda表达式的类型——闭包对象的类型是一个独特的、未命名的非联合类类型,被称为闭包类型。根据C++14标准中[expr.prim.lambda]的摘录(强调我的):不一定需要将lambda作为指向其实现的指针。 一个实现可以按照不同于下面所描述的方式定义闭包类型,只要这不通过更改来改变程序的可观察行为:

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

——闭包类型是否可以平凡地复制(第9条),

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

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

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

你确定这一位适用吗?没有捕获组的lambda不是真正的“闭包”。(标准是否将空捕获组lambda称为“闭包”?) - Kyle Strand
1
是的,没错。这就是标准所说的:“_lambda表达式的求值结果是一个prvalue临时对象。这个临时对象被称为闭包对象_”,无论是否捕获变量,它都是一个闭包对象,只不过其中一个将不包含upvalues。 - legends2k
我没有点踩,但可能那个点踩的人认为这个答案不太有价值,因为它没有解释为什么可以在不包括指向调用运算符函数的运行时指针的情况下实现lambda(从理论角度而非标准角度)。(请参见我与KerrekSB在问题下的讨论。) - Kyle Strand

8

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

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

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

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

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接