V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  C++

c++ lambda 表达式里 为什么值捕获的局部变量无法修改?

  •  
  •   amiwrong123 · 2022-03-06 10:55:17 +08:00 · 3010 次点击
    这是一个创建于 984 天前的主题,其中的信息可能已经有所发展或是发生改变。
    [&i] ( ) { std::cout << i; }
    
    // is equivalent to
    
    struct anonymous
    {
        int &m_i;
        anonymous(int &i) : m_i(i) {}
        inline auto operator()() const
        {
            std::cout << m_i;
        }
    };
    

    首先我知道,一个如上的 lambda 表达式,其实相当于生成了一个如上匿名 struct 的实例 a ,运行 lambda 表达式时,其实就相当于执行 a().

    int main()
    {
        int v1 = 42;
        auto f = [v1]() {return ++v1; };//值捕获
        v1 = 0;
        auto j = f(); //j 为 43
    
    }
    

    但是如上的代码却无法通过编译,除非你加上 mutable 。所以我是不是可以认为,如上的 lambda 表达式,是不是实际生成了如下的 struct ?

    struct anonymous
    {
        int m_i;
        anonymous(int i) : m_i(i) {}
        inline auto operator()() const//如果你加上 mutable ,这里的 const 才会去掉
        {
            m_i++;
        }
    };
    

    另外,c++ lambda 表达式是不是都可以认为 它的等价情况,就是生成了一个 重载了 operator()的匿名结构体的实例?如果不是的话,请帮忙举一个反例。

    12 条回复    2022-03-09 02:28:33 +08:00
    elfive
        1
    elfive  
       2022-03-06 11:05:54 +08:00 via iPhone
    [vl]这是按值捕获,要用引用捕获[&vl]
    codehz
        2
    codehz  
       2022-03-06 11:10:12 +08:00 via Android
    Lambda 确实就是匿名类型的实例化而已(没捕获的时候还带一个转换成函数指针的运算符重载)
    https://en.cppreference.com/w/cpp/language/lambda
    missdeer
        3
    missdeer  
       2022-03-06 11:10:52 +08:00
    这么理解没错,可以上 Compiler Explorer 验证一下,比如带 mutable 就是没 const: https://godbolt.org/z/87EaPznM8
    不带 mutable 就是有 const: https://godbolt.org/z/Y51KdT74j
    不过虽然现在的 gcc 和 msvc 确实都把 lambda 用仿函数实现,但这只是实现的方式,标准应该没说一定要这么做
    amiwrong123
        4
    amiwrong123  
    OP
       2022-03-06 11:10:57 +08:00
    @elfive #1
    是的,改成这样就可以编译通过。但我想知道,按值捕获的 lambda 表达式到底 实际上生成了什么样子的匿名结构体实例(然后才会造成,无法修改 按值捕获的变量)。
    victorbian
        5
    victorbian  
       2022-03-06 11:15:53 +08:00   ❤️ 1
    你想的没错,mutable 就是去掉 const ,而默认加 const 是为了让仿函数每次执行结果一致
    https://cppinsights.io/s/fc8369d5
    https://stackoverflow.com/a/5503690/8263383
    dangyuluo
        6
    dangyuluo  
       2022-03-06 12:54:40 +08:00
    Lambda 生成的类 operator()() 默认是 const 的,Google 搜索一下就有,实在是搜不到再发帖。

    看这里是怎么生成的

    https://cppinsights.io/s/8efa4ddf
    dangyuluo
        7
    dangyuluo  
       2022-03-06 12:55:55 +08:00   ❤️ 1
    文档里写: https://en.cppreference.com/w/cpp/language/lambda

    Unless the keyword mutable was used in the lambda-expression, the function-call operator or operator template is const-qualified and the objects that were captured by copy are non-modifiable from inside this operator()
    dcsuibian
        8
    dcsuibian  
       2022-03-06 15:17:46 +08:00
    我的理解,这是实现机制上的问题。Java 里也有一样的要求来着。

    lambda 里的 v1 并不是外面的 v1 。例如,原来的 v1 地址是 0x12345678 ,而 lambda 里的 v1 的地址是 0x23456789 ,它是把原来 0x12345678 里存的东西放到了 0x23456789 里面。所以实际上这俩不是一个东西。所以 v1 并没有跳脱它的作用域。

    那为什么要默认给他加个 const ?我以 Java 打比方。

    ```java
    public void test() {
    int v1=5;
    Runnable runnable=()->{
    v1+=2;
    };
    runnable.run();
    System.out.println(v1);
    }
    ```

    这个程序里面,创建了一个 lambda 并立即执行,我们会很自然地认为输出的 v1 是 7 ,但从实现的角度来说,由于 lambda 表达式里的 v1 并不是外面的那个 v1 ,仍然是 5 。
    语言开发者为了避免出现这个奇怪的现象就禁止了在 lambda 里面修改 v1 ,所以我这个程序里 runnable 里的 v1+=2 这个部分会报错。
    amiwrong123
        9
    amiwrong123  
    OP
       2022-03-08 21:56:14 +08:00
    @elfive #1
    @codehz #2
    @missdeer #3
    @victorbian #5
    @dangyuluo #6
    @dcsuibian #8

    各位大佬,问个问题。
    https://cppinsights.io/s/69a816df
    就是如上这个代码,为什么我看 main 函数里生成的 local class 里,有这么定义的 Member templates:
    ```
    template<class ... type_parameter_0_0>
    inline auto operator()(type_parameter_0_0... param) const
    {
    print(param...);
    }
    ```
    但实际上,local class 不能有 Member templates ( https://zh.cppreference.com/w/cpp/language/member_template 这里有说,你把 右边的代码放进 vs 也是编译不能通过)。
    所以,为什么实际生成的代码 违反了 invalid declaration of member template in local class ?

    另外,之所以还需要 void print() {}这个函数定义,是不是因为:最后一次调用 void print(const First& first, Rest &&... args)时,第二个参数 args 实际上已经是 void ,所以接着又会调用到 void print() {}里面?(不知道我这么解释对不对,所以不对,请帮忙用正确的术语描述一下😂)
    amiwrong123
        10
    amiwrong123  
    OP
       2022-03-08 21:59:51 +08:00
    另外, 楼上的代码是在 cppinsights 的 C++14 上执行的哈
    codehz
        11
    codehz  
       2022-03-08 22:08:51 +08:00 via Android
    Cpp insight 也就图一乐,为了便于理解才这样生成)
    看这个去理解标准那无异于刻舟求剑(
    victorbian
        12
    victorbian  
       2022-03-09 02:28:33 +08:00
    @amiwrong123 C++14 允许 lambda 表达式( local class 的一种)带有 member templates ,参见 [cppreference]( https://en.cppreference.com/w/cpp/language/class#:~:text=Local%20classes%20other%20than%20closure%20types%20(since%20C%2B%2B14)%20cannot%20have%20member%20templates),估计正因为如此 C++14 中 lambda 表达式才开始支持 auto 形参。

    关于 `void print(){}`,cppinsights 里已经很清楚了,最后一次调用 `void print<double>(const double & first)` 会调用 `print();`
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5574 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 01:36 · PVG 09:36 · LAX 17:36 · JFK 20:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.