C++ Lambda 编译器实现原理

本贴最后更新于 2105 天前,其中的信息可能已经渤澥桑田

Lambda 表达式语法

Lambda 表达式完整的格式如下:

[捕获列表] (形参列表) mutable 异常列表-> 返回类型
{
    函数体
}

各项的含义:

  1. 捕获列表:捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。
  2. 形参列表:和普通函数的形参列表一样。可省略,即无参数列表
  3. mutable:mutable 关键字,如果有,则表示在函数体中可以修改捕获变量,根据具体需求决定是否需要省略。
  4. 异常列表:noexcept / throw(...),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常。
  5. 返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型。
  6. 函数体:代码实现。可省略,但是没意义。

使用示例

void LambdaDemo()
{
    int a = 1;
    int b = 2;
    auto lambda = [a, b](int x, int y)mutable throw() -> bool
    {
        return a + b > x + y;
    };
    bool ret = lambda(3, 4);
}

编译器实现原理

编译器实现 lambda 表达式大致分为一下几个步骤

  1. 创建 lambda 类,实现构造函数,使用 lambda 表达式的函数体重载 operator()(所以 lambda 表达式 也叫匿名函数对象)
  2. 创建 lambda 对象
  3. 通过对象调用 operator()

编译器将 lambda 表达式翻译后的代码:

class lambda_xxxx
{
private:
    int a;
    int b;
public:
    lambda_xxxx(int _a, int _b) :a(_a), b(_b)
    {
    }
    bool operator()(int x, int y) throw()
    {
        return a + b > x + y;
    }
};
void LambdaDemo()
{
    int a = 1;
    int b = 2;
    lambda_xxxx lambda = lambda_xxxx(a, b);
    bool ret = lambda.operator()(3, 4);
}

其中,类名 lambda_xxxx 的 xxxx 是为了防止命名冲突加上的。

lambda_xxxx 与 lambda 表达式 的对应关系

  1. lambda 表达式中的捕获列表,对应 lambda_xxxx 类的 private 成员
  2. lambda 表达式中的形参列表,对应 lambda_xxxx 类成员函数 operator() 的形参列表
  3. lambda 表达式中的 mutable,对应 lambda_xxxx 类成员函数 operator() 的常属性 const,即是否是 常成员函数
  4. lambda 表达式中的返回类型,对应 lambda_xxxx 类成员函数 operator() 的返回类型
  5. lambda 表达式中的函数体,对应 lambda_xxxx 类成员函数 operator() 的函数体

另外,lambda 表达 捕获列表的捕获方式,也影响 对应 lambda_xxxx 类的 private 成员 的类型

  1. 值捕获:private 成员 的类型与捕获变量的类型一致
  2. 引用捕获:private 成员 的类型是捕获变量的引用类型

不捕获任何外部变量

如果 lambda 表达式不捕获任何外部变量,在特定的情况下,会有额外的代码生成。
其中,特定情况是指:有 lambda_xxxx 类函数指针 的类型转换
如以下代码

typedef int(_stdcall *Func)(int);
int Test(Func func)
{
	return func(1);
}
void LambdaDemo()
{
	Test([](int i) {
		return i;
	});
}

Test 函数接受一个函数指针作为参数,并调用这个函数指针。

实际调用 Test 时,传入的参数却是一个 Lambda 表达式,所以这里有一个类型的隐式转换
lambda_xxxx => 函数指针。

上面已经提到,Lambda 表达式就是一个 lambda_xxxx 类的匿名对象,与函数指针之间按理说不应该存在转换,但是上述代码却没有问题。

其问题关键在于,上述代码中,lambda 表达式没有捕获任何外部变量,即 lambda_xxxx 类没有任何成员变量,在 operator() 中也就不会用到任何成员变量,也就是说,operator() 虽然是个成员函数,它却不依赖 this 就可以调用。

因为不依赖 this,所以 一个 lambda_xxxx 类的匿名对象与函数指针之间就存在转换的可能。

大致过程如下:

  1. 在 lambda_xxxx 类中生成一个静态函数,静态函数的函数签名与 operator() 一致,在这个静态函数中,通过一个空指针去调用该类的 operator()
    2.在 lambda_xxxx 重载与函数指针的类型转换操作符,在这个函数中,返回第 1 步中静态函数的地址。

上述代码在编译器的翻译后代码如下:

typedef int(_stdcall *Func)(int);

class lambda_xxxx 
{
private:
	//没有捕获任何外部变量,所有没有成员
public:
        /*...省略其他代码...*/
	int operator()(int i)
	{
		return i;
	}
	static int _stdcall lambda_invoker_stdcall(int i)
	{
		return ((lambda_xxxx *)nullptr)->operator()(i);
	}

	operator Func() const
	{
		return &lambda_invoker_stdcall;
	}
};

int Test(Func func)
{
	return func(1);
}
void LambdaDemo()
{
	auto lambda = lambda_xxxx ();
	Func func = lambda.operator Func();
	Test(func);
}

上述代码只是以 __stdcall 调用约定的函数指针举例,实际使用时,对于不同调用约定,会生成对应版本的静态函数和类型转换函数

以上结论的正确性可以通过显式调用 lambda 的转换函数与反汇编来证明

void LambdaDemo()
{
	auto lambda = [](int i) {return i;};
	Func func = lambda.operator Func();
	Test(func);
}
  • 转为函数指针

1png

  • 将静态函数 lambda_invoker_stdcall 地址作为 类型转换函数的返回值

2png

  • 静态函数 lambda_invoker_stdcall 中,使用 0 作为 this 调用 operator()

3png

  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    107 引用 • 153 回帖
  • Lambda
    24 引用 • 19 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...