深入理解 C++ 的 lambda 表达式
Table of Contents
C++ lambda 表达式 出现10多年了,还是有人不甚了解。这就是 C++ 最垃圾的地方:你需要知道编译器是怎么做的,才能真正理解它的语法。本文是对 C++ lambda 表达式的一个简述,争取做到深入浅出彻底理解。注意我们不讲 Lambda 演算。
1 Lambda 表达式是什么
如果我们要对一个结构体列表按照其中一个成员来排序,在没有 lambda 表达式的情况下,我们大概会这么写:
#include <algorithm> #include <string> #include <vector> using Date = std::string; struct Person { std::string name; Date birthday; }; std::vector<Person> peoples{{"Marie Curie", "1867-11-7"}, {"Albert Einstein", "1879-3-14"}, {"Johann Carl Friedrich Gauß", "1777-4-30"}}; void sortPeopleNoLambda() { // 按照姓名排序 struct ByName { bool operator()(const Person& a, const Person& b) { return a.name < b.name; } }; std::sort(peoples.begin(), peoples.end(), ByName{}); }
使用 lambda 表达式,代码会简单一些:
void sortPeopleWithLambda() { std::sort(peoples.begin(), peoples.end(), [](const Person& a, const Person& b){ return a.name < b.name; }); }
实际上,根据C++ 标准,编译器会把 lambda 表达式转化成类似
struct ByName{}
的形式。
// lambda 表达式代码 [](const Person& a, const Person& b){ return a.name < b.name; } // 编译器转成 struct __lambda_1 /* 不要在意名字 */ { inline bool operator(const Person& a, const Person& b) const { a.name < b.name; } __lambda_1() = delete; // 没有默认构造函数 __lambda_1& operator=(const __lambda_1&) = delete; // 不能赋值 }; __lambda_1(); // 对象实例编译器自动创建的,所以不会报错
这就是最基本的 lambda 表达式的样子。
2 lambda 表达式作为函数指针
有些老代码接收函数指针,但可以传 lambda 表达式进去,怎么做到的呢?
例如:
void c_style_call(int(*f)(int)) { std::print("return value of f(7) is {}", f(7)); } int main() { // implicit convertion to function point c_style_call([](int i) { return i * i; }); }
根据 C++标准,编译器会添加一个类型转换函数,例如上一节结构体排序的例子, lambda 会添加一个转换函数,调用是执行隐式转换,就像下面这样:
struct __lambda_1 /* 不要在意名字 */ { inline bool operator(const Person& a, const Person& b) const { a.name < b.name; } __lambda_1() = delete; // 没有默认构造函数 __lambda_1& operator=(const __lambda_1&) = delete; // 不能赋值 // 转换为函数指针 using __func_type = bool(*)(const Person& a, const Person& b); inline operator __func_type() const noexcept { return &__invoke; } private: static inline bool __invoke(const Persion& a, const Person& b) { return a.name < b.name; } }; __lambda_1(); // 对象实例编译器自动创建的,所以不会报错
如果要显示转换,可以用 static_cast
,或者一个肮脏的小技巧:
auto *fp_compile_error = [](int i) { return i * i; }; // compile error! auto *fptr = static_cast<int(*)(int)>([](int i) {return i*i;}); // ok // 肮脏的小技巧: auto *fptr2 = +[](int i) { return i * i; } // ok
我觉得你知道为什么使用一个 +
就可以转换成函数指针了。
3 变量捕获
Lambda 表达式可以使用上下文的变量,例如:
int i = 0; int j = 0; auto f = [=] { // i 和 j 是拷贝过来的 return i == j; }
[=]
是默认的,通常说是传值捕获。编译器会把上面的lambda表达四代码转为:
struct __lambda_2 { __lambda_2(int i, int j): __i(i), __j(j) {} inline bool operator()() const { return __i == __j; } private: int __i; int __j; };
如果是引用捕获:
int i = 0; int j = 0; auto f = [&] { // i 和 j 是引用 return i == j; }
lambda 表达式会转为:
struct __lambda_2 { __lambda_2(int& i, int& j): __i(i), __j(j) {} inline bool operator()() const { return __i == __j; } private: int& __i; int& __j; };
如果是 this
捕获:
struct X { void printAsync() { callAsync([this] { // 可以使用 X 类里的成员 std::print("X::i={}", i); }); } private: int i{42}; };
编译器把 lambda 转为:
struct X { void printAsync() { struct __lambda_3 { __lambda_3(X* _this): __this(_this) {} void operator()() const { std::print("X::i={}", __this->i); } private: X* __this; }; } callAsync(__lambda_3(this)); private: int i{42}; };
4 "茴"字有几种写法之 —— lambda 捕获
下面函数会输出什么?42 还是 43?
#include <print> // C++23 int main() { static int i{42}; auto f = [=]{ i++; }; f(); std::print("{}", i); // 43! }
只能捕获局部变量 , 不能捕获全局变量。因为 i
是 static
,跟全局一样,不是局部变量,所以不会被捕获,因此 i++
也就是将 static
变量 ++
,而不是其副本。
同理:
#include <print> // C++23 int i{42}; int main() { auto f = []{ i++; }; f(); std::print("{}", i); // 43! }
简单地说,如果不是 odr-use 就可以不用捕获。
int main() { constexpr int i = 42; auto f = []{ std::print("{}", i); }; // ok, i is not odr-used f(); auto f2 = []{ std::print("{}", &i); }; // Error! i is odr-used f2(); auto f3 = [&] { std::print("{}", &i); }; // ok, print 42 f3(); const int j = 42; auto fj = []{ std::print("{}", j); }; // ok, i implicit constexpr fj(); const float fp = 42.0f; auto ff = [] { std::print("{}", i); }; // Error! float is not constexpr ff(); }
5 直接调用函数表达式 (IIFE)
lambda 表达式可以直接调用,不需赋值:
int main() { []{ std::print("Hello world!"); } (); }
这他娘的有啥用?那几个花哨的括号都去掉,只留下 std::print()
不好吗?
lambda 表达式像函数一样可以有复杂的结构也有返回值,但可以把逻辑体写在本地:
5.1 简化逻辑
这里本来要编写一个没多大用的函数,但也可以直接在本地写个 lambda 表达式把逻辑直接在原地写好。
int main() { // ... std::vector<Foo> foos; foos.emplace_back([&]{ if (hasDatabase) { return getFooFromDB(); } return getFooFromElsewhere(); }()); }
5.2 Call Once
某些逻辑整个程序过程中只需要运行一次,后续不再运行,这可以用 lambda 直接调用来做到:
struct X { X() { static auto _ = []{ std::print("call once!"); return 0; }(); } }; X(); // "call once!" X(); // nothing X(); // nothing X(); // nothing
6 泛型 lambda
lambda 表达式参数可以用 auto
。
std::map<int, std::string> httpStatus { // ... {400, "Bad Request"}, {401, "Unauthorized"}, {404, "Not Found"} // ... }; std::for_each(httpStatus.begin(), httpStatus.end(), [](auto &item) { std::print("{}:{}", item.first, item.second); });
编译器会为 auto
lambda 表达式类,添加一个模板。例如
// lambda [](auto i) { std::print("{}", i); }; // 编译器会把它改成: struct __lambda_6 { template<typename T> void operator()(T i) const { std::print("{}", i); } template<typename T> using __func_type = void(*)(T i); template<typename T> inline operator __func_type<T>() const noexcept { return &__invoke<T>; } private: template<typename T> static void __invoke(T i) { std::print("{}", i); } }; __lambda_6();
auto&&
右值引用同理:
// lambda std::vector<std::string> v; [&v](auto&& item) { v.push_back(std::forward<decltype(item)>(item)); }; // 编译器会为其构造下面的代码: struct __lambda_7 { __lambda_7(std::vector<std::string>& _v): __v(v) {} template<typename T> void operator()(T&& item) const { __v.push_back(std::forward<decltype(item)>(item)); } // ... private: std::vector<std::string>& __v; }; __lambda_6();
7 模板 lambda
lambda 表达式可以是个 template:
template<typename T> constexpr auto c_cast = [](auto x) { return (T)x; } c_cast<int>(3.14159); // => 3
编译器会为这个 lambda 产生下面的代码:
template<typename T> struct __lambda_9 { template<typename U> inline auto operator()(U x) const { return (T)x; } }; template<typename T> auto c_cast = __lambda_9<T>();
我们可以推测:
// C++20 decltype([]{}) f1; decltype([]{}) f2;
f1
和 f2
拥有不同的类型。
8 初始化捕获
expr.prim.lambda#nt:init-capture
这是 C++14 的功能,捕获列表可以是个初始化参数了。
int x = 4; auto y = [&r = x, x = x+1]()->int { r += 2; return x+2; }(); // Updates ::x to 6, and initializes y to 7. auto z = [a = 42](int a) { return 1; }; // error: parameter and conceptual local variable have the same name auto counter = [i=0]() mutable -> decltype(i) { // OK, returns int return i++; };
9 constexpr lambda
C++17 可以给 lambda 设定 constexpr 标记了。
auto f = []() constexpr { return sizeof(void*) }; std::array<int, f()> arr{};
10 lambda 重载表
C++17 可以用 lambda 构造一个重载表:
#include <variant> #include <cstdio> #include <vector> template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }; // (1) template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // (2) using var_t = std::variant<int, const char*>; int main() { std::vector<var_t> vars = {1, 2, "Hello, World!"}; for (auto& v : vars) { std::visit(overloaded { // (3) [](int i) { printf("%d\n", i); }, [](const char* str) { puts(str); } }, v); } return 0; }
11 C++20 lambda template
C++20 添加了 concept,lambda 也可以用
auto f = []<typename T1, C1 T2> requires C2<sizeof(T1) + sizeof(T2)> (T1 a1, T1 b1, T2 a2, auto a3, auto a4) requires C3<decltype(a4), T2> { // T2 is constrained by a type-constraint. // T1 and T2 are constrained by a requires-clause, and // T2 and the type of a4 are constrained by a trailing requires-clause. };
12 lambda 递归
lambda 递归还是有些麻烦。
auto fact = [](this auto self, int n) -> int { // OK, explicit object parameter return (n <= 1) ? 1 : n * self(n-1); }; std::cout << fact(5); // OK, outputs 120
13 总结
C++ 把 lambda 表达式搞得很复杂,即时理解了它的本质,也记不住它的语法和细节。如果你不是 C++ 律师的话,建议躺平吧,遇到了再去查标准文档。