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&amp; 完全没有意义。引用甚至可能需要比 int 更多的内存。 【参考方案1】:

tl;dr 版本在底部。


§5.1.2 [expr.prim.lambda]

p1 lambda-expressionlambda-introducer lambda-declaratoropt 复合语句

p3 lambda-expression 的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型——称为 闭包类型——其属性如下所述。此类类型不是聚合 (8.5.1)。 闭包类型在包含相应lambda-expression的最小块作用域、类作用域或命名空间作用域中声明。 (我的注释:函数具有块作用域。

p5 lambda-expression闭包类型 有一个公共的 inline 函数调用运算符 [...]

p7 lambda-expressioncompound-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 函数对象中的静态变量如何工作?的主要内容,如果未能解决你的问题,请参考以下文章

Lambda 函数的最佳实践

lambda 如何在 MSVC2017 15.9.3 中使用 /std:c++17 中的静态局部错误返回值?

如何修改 lambda 函数中的变量?

工作和面试中的单例

自动递增对象id JS构造函数(静态方法和变量)

题型:python