【问题标题】:Does capture by value in a C++ lambda expression require the value to be copied with the lambda object?C++ lambda 表达式中的按值捕获是否需要使用 lambda 对象复制值?
【发布时间】:2012-04-10 22:59:33
【问题描述】:

标准是否定义了这段代码会发生什么?

#include <iostream>

template <typename Func>
void callfunc(Func f)
{
   ::std::cout << "In callfunc.\n";
    f();
}

template <typename Func>
void callfuncref(Func &f)
{
   ::std::cout << "In callfuncref.\n";
    f();
}

int main()
{
    int n = 10;
    // n is captured by value, and the lambda expression is mutable so
    // modifications to n are allowed inside the lambda block.
    auto foo = [n]() mutable -> void {
       ::std::cout << "Before increment n == " << n << '\n';
       ++n;
       ::std::cout << "After increment n == " << n << '\n';
    };
    callfunc(foo);
    callfunc(foo);
    callfuncref(foo);
    callfunc(foo);
    return 0;
}

使用 g++ 的输出是:

$ ./a.out 
In callfunc.
Before increment n == 10
After increment n == 11
In callfunc.
Before increment n == 10
After increment n == 11
In callfuncref.
Before increment n == 10
After increment n == 11
In callfunc.
Before increment n == 11
After increment n == 12

标准是否需要此输出的所有功能?

特别是如果制作了 lambda 对象的副本,则所有捕获的值也会被复制。但是,如果 lambda 对象通过引用传递,则不会复制任何捕获的值。并且在调用函数之前不会对捕获的值进行复制,因此在调用之间会保留对捕获值的突变。

【问题讨论】:

    标签: c++ lambda c++11


    【解决方案1】:

    lambda 的类型只是一个类 (n3290 §5.1.2/3),带有一个执行主体 (/5) 的 operator() 和一个隐式复制构造函数 (/19),并捕获一个变量by copy 相当于将其复制初始化 (/21) 到该类的非静态数据成员 (/14),并且每次使用该变量都会被相应的数据成员 (/17) 替换。经过这种转换,lambda 表达式仅成为该类的一个实例,C++ 的一般规则如下。

    这意味着,您的代码应该以与以下相同的方式工作:

    int main()
    {
        int n = 10;
    
        class __Foo__           // §5.1.2/3
        {
            int __n__;          // §5.1.2/14
        public:
            void operator()()   // §5.1.2/5
            {
                std::cout << "Before increment n == " << __n__ << '\n';
                ++ __n__;       // §5.1.2/17
                std::cout << "After increment n == " << __n__ << '\n';
            }
            __Foo__() = delete;
            __Foo__(int n) : __n__(n) {}
          //__Foo__(const __Foo__&) = default;  // §5.1.2/19
        }
        foo {n};                // §5.1.2/21
    
        callfunc(foo);
        callfunc(foo);
        callfuncref(foo);
        callfunc(foo);
    }
    

    很明显callfuncref 在这里做了什么。

    【讨论】:

    • 这些 lambda 等效类是否也有移动构造函数和移动赋值操作?
    • @Omnifarious:移动构造函数:“也许”(第 5.1.2/19 节:“并且可能有一个隐式声明的移动构造函数”)。 lambda 没有任何赋值运算符,它们已被删除。
    【解决方案2】:

    我发现通过手动将 lambda 扩展为结构/类最容易理解这种行为,这或多或少是这样的(因为n 是按值捕获的,按引用捕获会看起来有点不同) :

    class SomeTemp {
        mutable int n;
        public:
        SomeTemp(int n) : n(n) {}
        void operator()() const
        {
           ::std::cout << "Before increment n == " << n << '\n';
           ++n;
           ::std::cout << "After increment n == " << n << '\n';
        }
    } foo(n);
    

    您的函数callfunc 和callfuncref 或多或少地作用于这种类型的对象。现在让我们检查您所做的调用:

    callfunc(foo);
    

    这里是按值传递的,所以foo 将使用默认的复制构造函数进行复制。 callfunc 中的操作只会影响复制值的内部状态,不会在实际的 foo 对象中改变状态。

    callfunc(foo);
    

    同样的东西

    callfuncref(foo);
    

    啊,这里我们通过引用传递 foo,所以 callfuncref(它调用 operator())将更新实际的 foo 对象,而不是临时副本。这将导致 foo 的 n 之后更新为 11,这是常规的 pass-by-reference 行为。因此,当您再次调用时:

    callfunc(foo);
    

    您将再次对副本进行操作,但 foo 的副本,其中 n 设置为 11。这显示了您的期望。

    【讨论】:

    • 在另一个我不太关心标准所说的内容的不同问题中,我会认为你的答案是更好的答案。 :-)
    【解决方案3】:

    除非您明确地全部捕获[&amp;] 或通过引用[&amp;n] 捕获特定变量,否则该值是通过复制捕获的。所以整个输出都是标准的。

    【讨论】:

    • 我对@9​​87654323@ 特别感兴趣。这更多是关于捕获值的可变性,而不是按值或引用捕获是默认的。
    猜你喜欢
    • 2013-11-09
    • 1970-01-01
    • 1970-01-01
    • 2014-11-07
    • 1970-01-01
    • 1970-01-01
    • 2011-02-19
    相关资源
    最近更新 更多