Demystifying C++ lambda

2023/03/20 cpp 共 2367 字,约 7 分钟

写在前面

lambda表达式也叫做匿名表达式,是纯右值表达式,它的类型是独有的无名非联合非聚合类类型,被称为闭包类型closure type),因此lambda表达式的类型并不是function,常用的格式:[ captures ] ( params ) specs requires (optional) { body },更多用法参考 cppreference.com

how it works

一个简单的例子,利用lamda表达式遍历数组

#include <stdio.h>

template<int N, typename Func>
void print(int (&nums)[N], Func&& f) {
  for (int i = 0; i < N; ++i) {
    f(nums[i]);
  }
}

int main() {
  int arr[] = {1, 2, 3, 4, 5};
  auto visited = [](int x) { printf("%d ", x); };
  print(arr, visited);
}

通过C++ Insights (cppinsights.io)可以得到展开结果。显而易见

  • lambda函数被定义成了一个类,通过重载operator()实现函数功能,

  • 闭包类型为 __lambda_12_18,并不是我们认为的 std::function,只有编译期知道这个类型,只能通过auto或者模板参数推导出来。

  • using retType_12_18 = void (*)(int);让该类型可以传递给 void (*)(int)指针

    无捕获lambda可以转换为函数指针(尽管它是否适合C调用约定取决于实现),但这并不意味着它是一个函数指针。

#include <stdio.h>

template<>
void print<5, __lambda_12_18 &>(int (&nums)[5], __lambda_12_18 & f)
{
  for(int i = 0; i < 5; ++i) {
    f.operator()(nums[i]);
  }
}

int main()
{
  int arr[5] = {1, 2, 3, 4, 5};
    
  class __lambda_12_18
  {
    public: 
    inline /*constexpr */ void operator()(int x) const
    {
      printf("%d ", x);
    }
    
    using retType_12_18 = void (*)(int);
    inline constexpr operator retType_12_18 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline /*constexpr */ void __invoke(int x)
    {
      __lambda_12_18{}.operator()(x);
    }
    public:
    // /*constexpr */ __lambda_12_18() = default;
  };
  
  __lambda_12_18 visited = __lambda_12_18{};
  print(arr, visited);
  return 0;
}

一个可以捕获值的例子,捕获了值a和b的引用。

int main()
{
  int a = 10;
  int b = 20;
  auto f1 = [a, &b](int x) { 
  	return a + b + x;
  };
  f1(7);
  return 0;
}

展开结果如下,我们可以知道捕获的值成为了这个类的私有成员变量,并且在包含了闭包之后,该类型就不能自动转换成函数指针的类型了。

int main()
{
  int a = 10;
  int b = 20;
    
  class __lambda_5_13  // 只有编译期可以推导出来
  {
    public: 
    inline /*constexpr */ int operator()(int x) const
    {
      return (a + b) + x;
    }
    private: 
    int a;
    int & b;
    
    public:
    __lambda_5_13(int & _a, int & _b)
    : a{_a}
    , b{_b}
    {}
  };
  
  __lambda_5_13 f1 = __lambda_5_13{a, b};
  return 0;
}

Attention

当捕获指针的时候,需要小心,比如当我们捕获this指针的时候,我们按值捕获或者按引用捕获的结果都是一样的糟糕,即生命周期并不会被延长,因为这是一个指针。需要捕获*this,或者将该类继承自enable_shared_from_this,使用 shared_from_this就可以增加引用计数,延长生命周期。

还有一点,我们按值捕获的时候,是不能更改里面的值的,比如下面这个,如果一定需要更改,则在参数列表后面加上mutable

int main()
{
  int a = 10;
  int b = 20;
  auto f1 = [ a, &b](int x) {  // error cannot assign to a variable captured by copy in a non-mutable lambda
  	a = b;
  };
  auto f1 = [ a, &b](int x) mutable {  // ok
  	a = b;
  };  
  f1(7);
  return 0;
}

总结

  • lambda是一个栈对象。它有一个构造函数和析构函数。它将遵循所有的C++规则。lambda的类型将包含捕获的值/引用;它们将是该对象的成员,就像任何其他类型的任何其他对象成员一样。
  • lambda函数被定义成了一个类,通过重载operator()实现函数功能,
  • 无捕获lambda可以转换为函数指针(尽管它是否适合C调用约定取决于实现),但这并不意味着它是一个函数指针。

Reference

c++ - C++11 lambda implementation and memory model - Stack Overflow

C++ Lambda Under the Hood

Search

    Table of Contents