需要翻译的内容:
这个 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
!
operator()
的struct
)。 - george_ptr