lambda 函数对象中的静态变量如何工作?
Posted
技术标签:
【中文标题】lambda 函数对象中的静态变量如何工作?【英文标题】:How do static variables in lambda function objects work? 【发布时间】:2012-01-13 12:46:44 【问题描述】:在使用 lambda 的函数的调用中是否保留了 lambda 中使用的静态变量?还是每次函数调用都会再次“创建”函数对象?
无用的例子:
#include <iostream>
#include <vector>
#include <algorithm>
using std::cout;
void some_function()
std::vector<int> v = 0,1,2,3,4,5;
std::for_each( v.begin(), v.end(),
[](const int &i)
static int calls_to_cout = 0;
cout << "cout has been called " << calls_to_cout << " times.\n"
<< "\tCurrent int: " << i << "\n";
++calls_to_cout;
);
int main()
some_function();
some_function();
这个程序的正确输出是什么? 它是否取决于 lambda 是否捕获局部变量的事实? (它肯定会改变函数对象的底层实现,因此可能会产生影响)是允许的行为不一致吗?
我不是在寻找:“我的编译器输出......”,这是一个太新的功能,无法信任当前的实现恕我直言。我知道要求标准报价似乎很受欢迎,因为世界发现了这样的事情存在,但我仍然想要一个体面的来源。
【问题讨论】:
如果我正确理解您的问题,您可能在某处遗漏了calls_to_cout++
。
Lambdageek:哦,是的,不错的收获:) thinko。为什么编译器不能推断出来?那不是很酷吗:)
这至少是非常糟糕的做法。所以不管标准怎么说,我都不会使用这个(是的,我来自 Singleton 被认为是有害的部分)。
我的直觉说是的,因为只有一个版本的 lambda 会在编译时编译,所以那里的任何静态都只会初始化一次,但我不会把它作为我的官方 SO 答案. ;)
顺便说一句,经过const int&
完全没有意义。引用甚至可能需要比 int
更多的内存。
【参考方案1】:
tl;dr 版本在底部。
§5.1.2 [expr.prim.lambda]
p1 lambda-expression:lambda-introducer lambda-declaratoropt 复合语句
p3 lambda-expression 的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型——称为 闭包类型——其属性如下所述。此类类型不是聚合 (8.5.1)。 闭包类型在包含相应lambda-expression的最小块作用域、类作用域或命名空间作用域中声明。 (我的注释:函数具有块作用域。)
p5 lambda-expression 的 闭包类型 有一个公共的
inline
函数调用运算符 [...]p7 lambda-expression 的 compound-statement 产生函数调用运算符 [.. .]
由于复合语句直接作为函数调用操作符的主体,而闭包类型定义在最小(最内)作用域,则与编写如下:
void some_function()
struct /*unnamed unique*/
inline void operator()(int const& i) const
static int calls_to_cout = 0;
cout << "cout has been called " << calls_to_cout << " times.\n"
<< "\tCurrent int: " << i << "\n";
++calls_to_cout;
lambda;
std::vector<int> v = 0,1,2,3,4,5;
std::for_each( v.begin(), v.end(), lambda);
这是合法的C++,函数允许有static
局部变量。
§3.7.1 [basic.stc.static]
p1 所有没有动态存储时长、没有线程存储时长、非本地变量都有静态存储时长。 这些实体的存储将持续到程序的持续时间。
p3 关键字
static
可用于声明具有静态存储持续时间的局部变量。 [...]
§6.7 [stmt.dcl] p4
(这涉及在块范围内初始化具有静态存储持续时间的变量。)
[...] 否则,此类变量在控件第一次通过其声明时被初始化; [...]
重申:
lambda 表达式的类型是在最内层范围内创建的。 不是为每个函数调用重新创建(这没有意义,因为封闭的函数体就像我上面的示例一样)。 它遵守(几乎)普通类/结构的所有规则(只是关于this
的一些内容不同),因为它是非联合类类型。
现在我们已经保证对于每个函数调用,闭包类型都是相同的,我们可以肯定地说静态局部变量也是相同的;它在第一次调用函数调用运算符时被初始化并一直存在到程序结束。
【讨论】:
这并不能回答每个函数调用中本地类是否不同,即静态变量是否在每次调用some_function
...时重新初始化?
好的,但我仍然不确定本地类lambda
是否总是相同 类,或者每次都是不同的类。对于实际的 lambda 表达式,闭包类型对于表达式的每次求值都是“唯一的”,所以这有点重要。
@KerrekSB:本地类如何在运行时更改type?每次执行通过那里时,它只是实例化一个类。没有类名不允许编译器在运行时生成多种类型。它总是相同的类型,因此,它总是相同的功能。
@KerrekSB "lambda-expression 的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型,"不是" t 意味着表达式的每次运行时评估都会产生一个具有唯一类型的值。这里的“lambda-expression”指的是语法的特定扩展。因此,表达式的类型与程序中所有其他类型的类型不同,但表达式求值产生的值将共享相同的类型,并且不会彼此唯一。
小心,当使用 templated-lambda (C++20) 或 generic-lambda (C++14) 时,它可能会创建不同的静态变量实例,每个传递给的实际类型组合一个lambda。【参考方案2】:
静态变量的行为应该与函数体中的行为一样。但是几乎没有理由使用它,因为 lambda 对象可以有成员变量。
下面,calls_to_cout
是按值捕获的,它给lambda一个同名的成员变量,初始化为calls_to_cout
的当前值。该成员变量在调用之间保留其值,但对于 lambda 对象是本地的,因此 lambda 的任何副本都将获得它们自己的 calls_to_cout 成员变量,而不是全部共享一个静态变量。这样更安全、更好。
(并且由于 lambdas 默认为 const,并且此 lambda 修改了 calls_to_cout
,因此必须将其声明为可变的。)
void some_function()
vector<int> v = 0,1,2,3,4,5;
int calls_to_cout = 0;
for_each(v.begin(), v.end(),[calls_to_cout](const int &i) mutable
cout << "cout has been called " << calls_to_cout << " times.\n"
<< "\tCurrent int: " << i << "\n";
++calls_to_cout;
);
如果您确实希望在 lambda 实例之间共享单个变量,则最好使用捕获。只需捕获对变量的某种引用。例如,这里有一个函数,它返回一对函数,它们共享对单个变量的引用,每个函数在调用时对该共享变量执行自己的操作。
std::tuple<std::function<int()>,std::function<void()>>
make_incr_reset_pair()
std::shared_ptr<int> i = std::make_shared<int>(0);
return std::make_tuple(
[=]() return ++*i; ,
[=]() *i = 0; );
int main()
std::function<int()> increment;
std::function<void()> reset;
std::tie(increment,reset) = make_incr_reset_pair();
std::cout << increment() << '\n';
std::cout << increment() << '\n';
std::cout << increment() << '\n';
reset();
std::cout << increment() << '\n';
【讨论】:
【参考方案3】:可以在捕获中构造一个静态的:-
auto v = vector<int>(99);
generate(v.begin(), v.end(), [x = int(1)] () mutable return x++; );
lambda 可以由另一个 lambda 生成
auto inc = [y=int(1)] () mutable
++y; // has to be separate, it doesn't like ++y inside the []
return [y, x = int(1)] () mutable return y+x++; ;
;
generate(v.begin(), v.end(), inc());
这里,y也可以通过引用来捕获,只要inc持续时间更长。
【讨论】:
【参考方案4】:我没有最终标准的副本,draft 似乎没有明确解决该问题(请参阅第 5.1.2 节,从 PDF 的第 87 页开始)。但它确实表示 lambda 表达式的计算结果为 闭包类型 的单个对象,该对象可以重复调用。既然如此,我相信标准要求静态变量只初始化一次,就像你写出类operator()
,并手动捕获变量一样。
但正如你所说,这是一个新功能;至少就目前而言,无论标准怎么说,您都坚持执行您的实现。无论如何,在封闭范围内显式捕获变量会更好。
【讨论】:
【参考方案5】:有两种方法可以通过 lambdas 使用状态。
-
在 lambda 中将变量定义为
static
:变量为
通过 lambda 调用和 lambda 实例化持久化。
在 lambda 捕获中定义变量并将 lambda 标记为 mutable
:该变量在 lambda 调用中是持久的,但在每次和 lambda 实例化时都会重置
以下代码说明了区别:
void foo()
auto f = [k=int(1)]() mutable cout << k++ << "\n";; // define k in the capture
f();
f();
void bar()
auto f = []() static int k = 1; cout << k++ << "\n";; // define k as static
f();
f();
void test()
foo();
foo(); // k is reset every time the lambda is created
bar();
bar(); // k is persistent through lambda instantiations
return 0;
【讨论】:
【参考方案6】:简短的回答:在 lambda 中声明的静态变量与在封闭范围内自动捕获(通过引用)的函数静态变量的工作方式相同。
在这种情况下,即使 lambda 对象被返回两次,值仍然存在:
auto make_sum()
static int sum = 0;
static int count = 0;
//Wrong, since these do not have static duration, they are implicitly captured
//return [&sum, &count](const int&i)
return [](const int&i)
sum += i;
++count;
cout << "sum: "<< sum << " count: " << count << endl;
;
int main(int argc, const char * argv[])
vector<int> v = 0,1,1,2,3,5,8,13;
for_each(v.begin(), v.end(), make_sum());
for_each(v.begin(), v.end(), make_sum());
return 0;
对比:
auto make_sum()
return [](const int&i)
//Now they are inside the lambda
static int sum = 0;
static int count = 0;
sum += i;
++count;
cout << "sum: "<< sum << " count: " << count << endl;
;
int main(int argc, const char * argv[])
vector<int> v = 0,1,1,2,3,5,8,13;
for_each(v.begin(), v.end(), make_sum());
for_each(v.begin(), v.end(), make_sum());
return 0;
两者都给出相同的输出:
sum: 0 count: 1
sum: 1 count: 2
sum: 2 count: 3
sum: 4 count: 4
sum: 7 count: 5
sum: 12 count: 6
sum: 20 count: 7
sum: 33 count: 8
sum: 33 count: 9
sum: 34 count: 10
sum: 35 count: 11
sum: 37 count: 12
sum: 40 count: 13
sum: 45 count: 14
sum: 53 count: 15
sum: 66 count: 16
【讨论】:
以上是关于lambda 函数对象中的静态变量如何工作?的主要内容,如果未能解决你的问题,请参考以下文章