在C ++ lambda表达式中按值捕获是否需要使用lambda对象复制值?

时间:2012-04-09 13:10:31

标签: c++ lambda c++11

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

#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对象,则不会复制捕获的值。并且在调用函数之前没有捕获值的副本,因此在调用之间保留捕获值的突变。

3 个答案:

答案 0 :(得分:9)

lambda的类型只是一个类(n3290§5.1.2/ 3),其中operator()执行正文(/ 5),隐式复制构造函数(/ 19)和捕获复制副本等同于将其初始化(/ 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在这里做了什么。

答案 1 :(得分:4)

通过手动将lambda扩展为struct / class,我发现最容易理解这种行为,这或多或少会像这样(因为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);

您的函数callfunccallfuncref或多或少对此类对象有效。现在让我们来看看你做的电话:

callfunc(foo);

这里传递值,因此将使用默认的复制构造函数复制foocallfunc中的操作只会影响复制值的内部状态,实际的foo对象中没有状态改变。

callfunc(foo);

相同的东西

callfuncref(foo);

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

callfunc(foo);

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

答案 2 :(得分:2)

除非您明确捕获所有[&]或通过引用[&n]捕获特定变量,否则将通过副本捕获该值。所以整个输出是标准的。