为什么 lambda 的大小为 1 个字节?
Why does a lambda have a size of 1 byte?
我正在使用 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 不应该至少是一个指向其实现的指针吗?
您的编译器或多或少地将 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 的内部工作方式,但这并不是编译器将执行的操作的字面翻译)
lambda 不是函数指针。
lambda 是 class 的实例。您的代码大约相当于:
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 的内部 class 没有 class 成员,因此它的 sizeof()
是 1(它不能是 0,原因已充分说明 elsewhere) .
如果您的 lambda 要捕获一些变量,它们将等同于 class 个成员,并且您的 sizeof()
将相应地指示。
来自http://en.cppreference.com/w/cpp/language/lambda:
The lambda expression constructs an unnamed prvalue temporary object of unique unnamed non-union non-aggregate class type, known as closure type, which is declared (for the purposes of ADL) in the smallest block scope, class scope, or namespace scope that contains the lambda expression.
If the lambda-expression captures anything by copy (either implicitly with capture clause [=] or explicitly with a capture that does not include the character &, e.g. [a, b, c]), the closure type includes unnamed non-static data members, declared in unspecified order, that hold copies of all entities that were so captured.
For the entities that are captured by reference (with the default capture [&] or when using the character &, e.g. [&a, &b, &c]), it is unspecified if additional data members are declared in the closure type
来自http://en.cppreference.com/w/cpp/language/sizeof
When applied to an empty class type, always returns 1.
Shouldn't the lambda be, at mimumum, a pointer to its implementation?
不一定。根据标准,唯一的、未命名的 class 的大小是 实现定义的 。摘自 [expr.prim.lambda],C++14(强调我的):
The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed nonunion class type — called the closure type — whose properties are described below.
[ ... ]
An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:
— the size and/or alignment of the closure type,
— whether the closure type is trivially copyable (Clause 9),
— whether the closure type is a standard-layout class (Clause 9), or
— whether the closure type is a POD class (Clause 9)
在您的情况下——对于您使用的编译器——您得到的大小为 1,这并不意味着它是固定的。它可能因不同的编译器实现而异。
有问题的 lambda 实际上没有状态。
检查:
struct lambda {
auto operator()() const { return 17; }
};
如果我们有 lambda f;
,它是空的 class。上面的 lambda
不仅在功能上与您的 lambda 相似,而且(基本上)它是如何实现您的 lambda 的! (它还需要隐式转换为函数指针运算符,名称 lambda
将被一些编译器生成的伪 guid 替换)
在 C++ 中,对象不是指针。它们是真实的东西。它们只用完了在其中存储数据所需的 space。指向对象的指针可以大于对象。
虽然您可能认为 lambda 是指向函数的指针,但事实并非如此。您不能将 auto f = [](){ return 17; };
重新分配给不同的函数或 lambda!
auto f = [](){ return 17; };
f = [](){ return -42; };
以上是非法。 f
中没有空间来存储 哪个 函数将被调用——该信息存储在 type 中 f
,不在f
!
的值中
如果您这样做:
int(*f)() = [](){ return 17; };
或者这个:
std::function<int()> f = [](){ return 17; };
您不再直接存储 lambda。在这两种情况下,f = [](){ return -42; }
都是合法的——所以在这些情况下,我们存储 我们正在调用的 函数在 f
的值中。并且 sizeof(f)
不再是 1
,而是 sizeof(int(*)())
或更大(基本上,如您所料,指针大小或更大。std::function
具有标准隐含的最小大小(他们必须能够存储 "inside themselves" 可调用文件,最大为一定大小)在实践中至少与函数指针一样大。
在 int(*f)()
情况下,您正在存储一个指向函数的函数指针,该函数的行为就像调用该 lambda 一样。这仅适用于无状态 lambda(具有空 []
捕获列表的)。
在 std::function<int()> f
的情况下,您正在创建一个类型擦除 class 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)
分开放置。
现在,因为它没有状态,有时它可以占用 no space。当它是 "alone" 时,这不会发生,但在某些情况下它可能会发生。 std::tuple
和类似的库代码利用了这一事实。这是它的工作原理:
由于 lambda 等同于 class 且 operator()
重载,因此无状态 lambda(具有 []
捕获列表)都是空的 class。他们有 sizeof
个 1
。事实上,如果你从它们继承(这是允许的!),它们将不会占用 space 只要它不会导致相同类型的地址冲突 。 (这被称为空碱基优化)。
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
!
我正在使用 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 不应该至少是一个指向其实现的指针吗?
您的编译器或多或少地将 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 的内部工作方式,但这并不是编译器将执行的操作的字面翻译)
lambda 不是函数指针。
lambda 是 class 的实例。您的代码大约相当于:
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 的内部 class 没有 class 成员,因此它的 sizeof()
是 1(它不能是 0,原因已充分说明 elsewhere) .
如果您的 lambda 要捕获一些变量,它们将等同于 class 个成员,并且您的 sizeof()
将相应地指示。
来自http://en.cppreference.com/w/cpp/language/lambda:
The lambda expression constructs an unnamed prvalue temporary object of unique unnamed non-union non-aggregate class type, known as closure type, which is declared (for the purposes of ADL) in the smallest block scope, class scope, or namespace scope that contains the lambda expression.
If the lambda-expression captures anything by copy (either implicitly with capture clause [=] or explicitly with a capture that does not include the character &, e.g. [a, b, c]), the closure type includes unnamed non-static data members, declared in unspecified order, that hold copies of all entities that were so captured.
For the entities that are captured by reference (with the default capture [&] or when using the character &, e.g. [&a, &b, &c]), it is unspecified if additional data members are declared in the closure type
来自http://en.cppreference.com/w/cpp/language/sizeof
When applied to an empty class type, always returns 1.
Shouldn't the lambda be, at mimumum, a pointer to its implementation?
不一定。根据标准,唯一的、未命名的 class 的大小是 实现定义的 。摘自 [expr.prim.lambda],C++14(强调我的):
The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed nonunion class type — called the closure type — whose properties are described below.
[ ... ]
An implementation may define the closure type differently from what is described below provided this does not alter the observable behavior of the program other than by changing:
— the size and/or alignment of the closure type,
— whether the closure type is trivially copyable (Clause 9),
— whether the closure type is a standard-layout class (Clause 9), or
— whether the closure type is a POD class (Clause 9)
在您的情况下——对于您使用的编译器——您得到的大小为 1,这并不意味着它是固定的。它可能因不同的编译器实现而异。
有问题的 lambda 实际上没有状态。
检查:
struct lambda {
auto operator()() const { return 17; }
};
如果我们有 lambda f;
,它是空的 class。上面的 lambda
不仅在功能上与您的 lambda 相似,而且(基本上)它是如何实现您的 lambda 的! (它还需要隐式转换为函数指针运算符,名称 lambda
将被一些编译器生成的伪 guid 替换)
在 C++ 中,对象不是指针。它们是真实的东西。它们只用完了在其中存储数据所需的 space。指向对象的指针可以大于对象。
虽然您可能认为 lambda 是指向函数的指针,但事实并非如此。您不能将 auto f = [](){ return 17; };
重新分配给不同的函数或 lambda!
auto f = [](){ return 17; };
f = [](){ return -42; };
以上是非法。 f
中没有空间来存储 哪个 函数将被调用——该信息存储在 type 中 f
,不在f
!
如果您这样做:
int(*f)() = [](){ return 17; };
或者这个:
std::function<int()> f = [](){ return 17; };
您不再直接存储 lambda。在这两种情况下,f = [](){ return -42; }
都是合法的——所以在这些情况下,我们存储 我们正在调用的 函数在 f
的值中。并且 sizeof(f)
不再是 1
,而是 sizeof(int(*)())
或更大(基本上,如您所料,指针大小或更大。std::function
具有标准隐含的最小大小(他们必须能够存储 "inside themselves" 可调用文件,最大为一定大小)在实践中至少与函数指针一样大。
在 int(*f)()
情况下,您正在存储一个指向函数的函数指针,该函数的行为就像调用该 lambda 一样。这仅适用于无状态 lambda(具有空 []
捕获列表的)。
在 std::function<int()> f
的情况下,您正在创建一个类型擦除 class 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)
分开放置。
现在,因为它没有状态,有时它可以占用 no space。当它是 "alone" 时,这不会发生,但在某些情况下它可能会发生。 std::tuple
和类似的库代码利用了这一事实。这是它的工作原理:
由于 lambda 等同于 class 且 operator()
重载,因此无状态 lambda(具有 []
捕获列表)都是空的 class。他们有 sizeof
个 1
。事实上,如果你从它们继承(这是允许的!),它们将不会占用 space 只要它不会导致相同类型的地址冲突 。 (这被称为空碱基优化)。
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
!