一、Lambda 表达式
Lambda 表达式对于了解和应用 C++11 以后的开发者来说,是一个很好用的语法糖。Lambda 表达式的特点和应用场景对于开发者来说已经很熟悉了。在前面的分析中,将 Lambda 表达式简单的定义为函数对象或闭包,这样描述的目的是便于理解和学习。但从实际情况来看,它们还是有些不准确的。
二、Lambda 表达式和 Closure 闭包
对于 Lambda 表达式来说,在不同的角度下分析可能表现的形式不同。比如从语法的角度来看,无捕获外部变量和有捕获外部变量的 Lambda 表达式来说,可能就有所不同。无捕获的可以单纯的看作是与普通函数无异的函数,而有捕获的则可以看作一个类。 那为什么前面把 Lambda 表达式看作一个闭包呢?闭包是函数与其捕获的外部自由变量共同组成的组合体(在前面的分析中也按离散数学中的定义描述过即通过添加最少数量的有序对,使原关系具备自反性、对称性或传递性而形成的新集合)。闭包是一个组合体,为了实现上下文的调用操作(离开 lambda 表达式后仍然可以操作外部变量,看下面的 lifting 例子),就必须分配一定的空间来处理上下文关系的内容。这但消耗了内存空间也在调用时增加了一定的开销。 lambda 表达式可以认为是从语法层次上的描述,而闭包更倾向于执行时的形态。从这种情况来看,单纯无捕获变量形式的 Lambda 表达式不能称为闭包。这也是为什么开头提到的说法不准确的原因。也可以这样说,同一个 Lambda 表达式可能产生多个不同的闭包。 这里就必须提到 Lambda Lifting(lambda 提升)和 Lambda Dropping(lambda 降级)。它们二者可以认为是互逆的,本质都是为了让编译器优化代码。Lambda Lifting,是为了将嵌套的引用了外部的自由变量的 lambda 表达式转化为不引用外部变量的顶函数,对 C++ 来说,就是将相关的 Lambda 表达式转化为普通函数对象的技术,它可以显式的由开发者控制进行也可以由编译器内部自动实现。 Lambda Dropping 是 Lambda Lifting 逆操作,即将原来需要提升为独立函数对象的 Lambda 表达式内联或部分展开到调用上下文中的方式。
三、编译器对 Lambda 表达式的处理
其实在前面的 std::visit 分析中,就对此问题进行过简单的展开分析。在实际的编译中,当编译器发现 lambda 表达式后,一般来说会进行如下的处理:
- 创建一个匿名的函数类(闭包类型)
- 将捕获的变量存储为该类的成员 (分别处理引用、值或隐式或混合捕获,处理方式看下面的例程)
- 将 lambda 体转换为该类的 operator() 方法,也就是创建一个仿函数
- 将匿名的函数类实例化为函数对象
- 调用函数对象
编译器通过上述的处理,其实是把 Lambda 表达式统一到了传统的编译模型。这也符合语法糖处理的风格。 但是需要说明的是,上述的处理是编译器对 lambda 表达式的通常处理机制。在某些情况下,如上面提到的 Lambda Lifting 和 Lambda Dropping,其具体的处理机制可能会有一些细节上的不同。具体的来说,就是在 Lambda Lifting 中,会消除环境上下文及内存分配处理,优化相关操作。比如提到的 lambda 表达式的对象化或内联等等。
四、例程分析
可以看下面的一个简单的例子:
#include <iostream>
int main() {
int a = 0;
int b = 10;
auto f = [a, &b]() { std::cout << a << "," << ++b << std::endl; };
f();
std::cout << a << "," << b << std::endl;
return 0;
}
编译后的代码:
#include <iostream>
int main() {
int a = 0;
int b = 10;
class __lambda_5_12 {
public:
inline /*constexpr*/ void operator()() const {
std::operator<<(std::cout.operator<<(a), ",").operator<<(++b).operator<<(std::endl);
}
private:
int a;
int& b;
public:
__lambda_5_12(int& _a, int& _b): a{_a}, b{_b} {}
};
__lambda_5_12 f = __lambda_5_12{a, b};
f.operator()();
std::operator<<(std::cout.operator<<(a), ",").operator<<(b).operator<<(std::endl);
return 0;
}
再看一个 lambda Lifting 的例子:
auto addResult = [](int v) { return [v](int x) { return x + v; }; };
auto againAdd = addResult(10);
std::cout << againAdd(10); // 输出 10
看一下原来的 visit 中的例程的编译后的代码(编译后的代码有删减):
#include <iomanip>
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
#include <vector>
using value_t = std::variant<int, long, double, std::basic_string<char>>;
template<class ... Ts>
struct overloaded : public Ts... {
using Ts::operator()...;
};
/* First instantiated from: insights.cpp:56 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct overloaded<__lambda_57_13, __lambda_58_13, __lambda_59_13> : public __lambda_57_13, public __lambda_58_13, public __lambda_59_13 {
using __lambda_57_13::operator();
template<class type_parameter_0_0>
inline /*constexpr*/ auto::operator()(type_parameter_0_0 arg) const {
(std::cout << arg) << ;
}
<>
::()<>( arg) {
std::<<(std::cout.<<(arg), );
}
<>
::()<>( arg) {
std::<<(std::cout.<<(arg), );
}
...
;
<>
(__lambda_57_13 __0, __lambda_58_13 __1, __lambda_59_13 __2)-> overloaded<__lambda_57_13, __lambda_58_13, __lambda_59_13>;
{
std::vector<std::variant<, , , std::basic_string<>>, std::allocator<std::variant<, , , std::basic_string<>>>> vec = std::vector<std::variant<, , , std::basic_string<>>, std::allocator<std::variant<, , , std::basic_string<>>>>{
std::initializer_list<std::variant<, , , std::basic_string<>>>{
std::variant<, , , std::basic_string<>>(),
std::variant<, , , std::basic_string<>>(),
std::variant<, , , std::basic_string<>>(),
std::variant<, , , std::basic_string<>>()
},
std::allocator<std::variant<, , , std::basic_string<>>>()
};
{
std::vector<std::variant<, , , std::basic_string<>>, std::allocator<std::variant<, , , std::basic_string<>>>>& __range1 = vec;
__gnu_cxx::__normal_iterator<std::variant<, , , std::basic_string<>>*, std::vector<std::variant<, , , std::basic_string<>>, std::allocator<std::variant<, , , std::basic_string<>>>>> __begin1 = __range();
__gnu_cxx::__normal_iterator<std::variant<, , , std::basic_string<>>*, std::vector<std::variant<, , , std::basic_string<>>, std::allocator<std::variant<, , , std::basic_string<>>>>> __end1 = __range();
(; __gnu_cxx::!=(__begin1, __end1); __begin++()) {
std::variant<, , , std::basic_string<>>& v = __begin*();
{
:
< >
{
std::cout << arg;
}
<>
{
std::cout.<<(arg);
}
...
:
< >
__invoke(type_parameter_0_0 && arg) {
__lambda_25_20{}.()<type_parameter_0_0>(arg);
}
};
std::(__lambda_25_20{}, v);
{
:
< >
std::variant<, , , std::basic_string<>> ()(type_parameter_0_0 && arg) {
arg + arg;
}
<>
std::variant<, , , std::basic_string<>> ()<&>(& arg) {
std::variant<, , , std::basic_string<>>(arg + arg);
}
<>
std::variant<, , , std::basic_string<>> ()<&>(& arg) {
std::variant<, , , std::basic_string<>>(arg + arg);
}
...
:
< >
std::variant<, , , std::basic_string<>> __invoke(type_parameter_0_0 && arg) {
__lambda_28_32{}.()<type_parameter_0_0>(arg);
}
};
std::variant<, , , std::basic_string<>> w = std::(__lambda_28_32{}, v);
std::<<(std::cout, );
{
:
< >
{
T = std::<(arg)>;
{
(std::<<(std::cout, ) << arg) << ;
} {
(std::is_same_v<T, >) {
(std::<<(std::cout, ) << arg) << ;
} {
(std::is_same_v<T, >) {
(std::<<(std::cout, ) << arg) << ;
} {
(std::is_same_v<T, std::basic_string<>>) {
(std::<<(std::cout, ) << std::(arg)) << ;
} {
;
}
}
}
}
}
...
:
< >
__invoke(type_parameter_0_0 && arg) {
__lambda_32_20{}.()<type_parameter_0_0>(arg);
}
};
std::(__lambda_32_20{}, w);
}
}
...
;
}
五、总结
掌握一个技术点,不只是需要会用,更要明白其内在的处理机制。只有内外通透,才能更加灵活的运用这个技术点去与其它的技术点融合,然后形成技术栈并最终形成技术体系。


