lambda函数对象中的静态变量是如何工作的
How do static variables in lambda function objects work?
在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是否捕获局部变量的事实?(它肯定会改变函数对象的底层实现,所以它可能会产生影响)这是允许的行为不一致吗?
我不是在寻找:"我的编译器输出…",这是一个太新的功能,不能信任当前的实现。我知道要求标准报价似乎很流行,因为世界发现这样的事情存在,但我仍然想要一个体面的来源。
tl;
§5.1.2 [expr.prim.lambda]
lambda表达式的闭包类型有一个公共p1 lambda表达式:
lambda表达式的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型——称为闭包类型——其属性将在下面描述。这个类类型不是聚合(8.5.1)。闭包类型在包含相应lambda表达式的最小块作用域、类作用域或命名空间作用域中声明。(我的注意:函数有一个块作用域。)
opt compound-statement
inline
函数调用操作符[…]
lambda表达式的复合语句产生函数调用操作符[…]的函数体 (8.4)由于复合语句直接作为函数调用操作符的主体,并且闭包类型是在最小(最内层)作用域中定义的,因此它与编写以下内容相同:
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
的一些东西不同),因为它是一个非联合类类型。
现在我们已经保证了对于每个函数调用,闭包类型都是相同的,我们可以放心地说静态局部变量也是相同的;它在函数调用操作符第一次调用时初始化,并一直有效到程序结束。
静态变量的行为应该与函数体中的行为一样。但是没有什么理由使用它,因为lambda对象可以有成员变量。
在下面的代码中,calls_to_cout
是按值捕获的,这给lambda一个同名的成员变量,初始化为calls_to_cout
的当前值。该成员变量在调用期间保留其值,但它是lambda对象的局部变量,因此lambda的任何副本都将获得自己的calls_to_cout成员变量,而不是所有成员都共享一个静态变量。这更安全,更好。
(因为lambda默认是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的实例之间共享单个变量,您仍然最好使用capture。只需捕获对变量的某种引用。例如,下面的函数返回一对函数,它们共享对单个变量的引用,每个函数在调用时对该共享变量执行自己的操作。
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';
静态可以在捕获中构造:-
auto v = vector<int>(99);
generate(v.begin(), v.end(), [x = int(1)] () mutable { return x++; });
可以由另一个
组成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());
在这里,只要inc持续时间较长,y也可以通过引用捕获。
有两种方法可以通过lambdas使用状态。
- 在lambda中将变量定义为
static
:变量为 - 在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;
}
我没有最终标准的副本,并且草案似乎没有明确地解决这个问题(参见PDF第87页开始的5.1.2节)。但它确实说lambda表达式的计算结果为闭包类型的单个对象,该对象可以重复调用。既然如此,我认为标准要求静态变量初始化一次且仅初始化一次,就像您手工编写了类operator()
和变量捕获一样。
但正如你所说,这是一个新功能;至少到目前为止,无论您的实现做了什么,无论标准说了什么,您都必须坚持下去。无论如何,在封闭作用域中显式捕获变量是更好的风格。
简短的回答: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;
}
vs: 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
- QSqlquery prepare()和bindvalue()不工作
- 导入库可以跨dll版本工作吗
- 以螺旋方式打印矩阵的程序.(工作不好)
- 对象指针在c++中是如何工作的
- 为什么在Windows上的VS 2019和Clang 9中"size_t"在没有标题的情况下工作
- VSOMEIP-2个设备之间的通信(TCP/UDP)不工作
- 为字符串中每 N 个字符插入空格的函数没有按照我认为的方式工作?
- C++为线程工作动态地分割例程
- 为什么我的 std::ref 无法按预期工作?
- 布尔比较运算符是如何在C++中工作的
- SampleConsensusPrerejective(ext.RANSAC)是如何真正工作的
- 不确定要在我的main中放入什么才能使我的代码正常工作
- 为什么std::condition_variable notify_all的工作速度比notify_one快(对于随机请
- <<操作员在下面的行中工作
- 有人能解释一下为什么下界是这样工作的吗C++的
- ExtractIconEx:可以工作,但偶尔会崩溃
- C++中的memset函数工作不正常
- 当我在第一个循环中使用"auto"时,它工作正常,但是使用"int"它会给出错误,为什么?
- 链表c++插入,所有情况都已检查,但没有任何工作
- 当 int 方法工作正常时,void 方法有何不同,或者为什么我不能调用 void 方法?