深入理解C++11的Lambda表达式

mutable修饰符

  先从一个引例代码开始,如下的代码是不能通过编译的:

#include <iostream>
using namespace std;

int main() {
    int j = 10;

    auto lambda = [=](){
        j += 1;     // 1.无法通过编译
    };

    auto lambda2 = [&](){
        j += 2;     // 2.可以编译通过
    };

    auto lambda3 = [=]() mutable{
        j += 1;     // 3.可以编译通过
    };

    lambda();
    lambda2();
    lambda3();
    return 0;
}

  C++中lambda表达式又称为“闭包”。闭包是一个函数与其相关的环境(即自由变量)组合而成的实体。换句话说,闭包可以“记住”其创建时的作用域,即使在外部作用域已经结束后,它仍然可以访问那些变量。其实,C++中的lambda最终会展开为一个局部作用域的仿函数。可以使用源码查探工具cpp insights查看具体的实现细节,如下所示:

#include <iostream>
using namespace std;

int main()
{
  int j = 10;

  // 为了能通过编译,将源代码中第一个lambda表达式中的j+=1注释掉了,因此这里的函数体为空。
  class __lambda_7_19
  {
    public: 
    inline /*constexpr */ void operator()() const
    {
    }

  };

  __lambda_7_19 lambda = __lambda_7_19{};

  class __lambda_11_20
  {
    public: 
    inline /*constexpr */ void operator()() const
    {
      j = j + 2;
    }

    private: 
    int & j;

    public:
    __lambda_11_20(int & _j)
    : j{_j}
    {}

  };

  __lambda_11_20 lambda2 = __lambda_11_20{j};

  class __lambda_15_20
  {
    public: 
    inline /*constexpr */ void operator()()
    {
      j = j + 1;
    }

    private: 
    int j;

    public:
    __lambda_15_20(int & _j)
    : j{_j}
    {}

  };

  __lambda_15_20 lambda3 = __lambda_15_20{j};
  lambda.operator()();
  lambda2.operator()();
  lambda3.operator()();
  return 0;
}

  对于lambda表达式而言,默认情况下其仿函数的()操作符是const类型的,如__lambda_7_19__lambda_11_20所示。显然,对于1中的情况而言,想要修改变量j是非法的。但是,对于2中的情况而言,由于捕获采用的传引用的方式,改变引用对象的值是没有问题的,const限定符只限定了不能修改引用。比较13就不难发现,添加了mutable修饰符以后,lambda3的()操作符不再有const限定,就能够正常通过编译并使用。

传值的细节

  闭包可以“记住”其创建时的作用域,即使在外部作用域已经结束后,它仍然可以访问那些变量。准确理解上述表达才能更深入的探讨C++中lambda中传值和传引用的具体细节。同样的,我们也先从一个引例代码开始,不妨思考以下代码的输出结果:

#include <iostream>

int main()
{
    int j = 10;

    auto lambda1 = [=](){ return j + 1; };
    auto lambda2 = [&](){ return j + 2; };

    std::cout << "lambda1() = " << lambda1() << std::endl;
    std::cout << "lambda2() = " << lambda2() << std::endl;

    j++;

    std::cout << "lambda1() = " << lambda1() << std::endl;
    std::cout << "lambda2() = " << lambda2() << std::endl;
    return 0;
}

  上述代码的输出结果如下,不知道是否符合个人的理解和判断呢?

11
12
11
13

  前两行的输出,一般情况下使用过lambda表达式的C++程序员都能够正确的得到结果。遗憾的是,在我没能理解lambda具体细节的情况下,对第三个输出值11充满了疑惑。如上一章节所示,我们不妨查看一下传值和传引用的具体细节。以下是使用源码查探工具cpp insights得到的结果:

#include <iostream>

int main()
{
  int j = 10;

  class __lambda_7_20
  {
    public: 
    inline /*constexpr */ int operator()() const
    {
      return j + 1;
    }

    private: 
    int j;

    public:
    __lambda_7_20(int & _j)
    : j{_j}
    {}

  };

  __lambda_7_20 lambda1 = __lambda_7_20{j};

  class __lambda_8_20
  {
    public: 
    inline /*constexpr */ int operator()() const
    {
      return j + 2;
    }

    private: 
    int & j;

    public:
    __lambda_8_20(int & _j)
    : j{_j}
    {}

  };

  __lambda_8_20 lambda2 = __lambda_8_20{j};
  std::operator<<(std::cout, "lambda1() = ").operator<<(lambda1.operator()()).operator<<(std::endl);
  std::operator<<(std::cout, "lambda2() = ").operator<<(lambda2.operator()()).operator<<(std::endl);
  j++;
  std::operator<<(std::cout, "lambda1() = ").operator<<(lambda1.operator()()).operator<<(std::endl);
  std::operator<<(std::cout, "lambda2() = ").operator<<(lambda2.operator()()).operator<<(std::endl);
  return 0;
}

  不难看出,第一个lambda表达式通过传值的方式捕获,声明的时候其实就已经使用传值的方式将j赋值给仿函数的成员变量。第二个lambda表达式则是通过传引用的方式捕获,声明的时候同样适用传引用的方式初始化了仿函数的引用成员变量。因此,lambda1只做了一次值的拷贝,在声明以后捕获的值就被保存下来了。后续所有的调用都会以初始化时的值为准,因此示例代码的第三个输出为11。由此,就能更好的理解“闭包”的含义了。
  综上,C++中的lambda表达式在使用传值的时候要尤其小心,考虑清楚按照传值捕获的变量是否符合编码逻辑。虽然,大多数时候为了避免不必要的拷贝开销一般采用传引用的方式进行拷贝,但是了解细节并避免潜在的错误总是必要的。


当珍惜每一片时光~