匿名的函数?
C++11引入的超大特性之一是 lambda expression ,即匿名函数表达式。
传统C/C++中,或许我们都已习惯于声明定义函数的传统模式。那么对函数进行“匿名”的意义何在?
最重要的原因之一是,匿名函数的定义轻巧快捷。
由于C/C++并不允许函数内部嵌套定义函数明明namespace/class/template都能嵌套的,因而函数必须被“分离”定义。
但很多时候我们需要临时定义一个函数进行使用(如在调用某些算法时),如果每次都需要分别声明、调用,具名函数数大幅增加极易导致其变得臃肿,且不利于程序的维护。
1 | int comp_stu_1(const Stu& x,const Stu& y) |
虽然但是,讲pre-C++11的情况为什么要用C++20的特性啊喂
类似的匿名函数在很多现代编程语言中都存在,如python直接将lambda
作为其关键字(相对地,即使lambda这一特性之大,C++中并没有为之引入任何新的关键字)。
lambda简介
lambda expression 本质上是重载了
operator()
的匿名类的一个实例。
声明语法
定义 lambda expression 的语法形如:
[capture-list](parameter-list) mutable noexcept -> return-type {function-body}
。
中括号
[]
标志一个lambda表达式的开始,其中capture-list
是捕获列表,以“捕获”lambda表达式所在命名空间的变量,并在函数内使用:&var-name
:以引用方式捕获一个变量(非const
引用)。若省略var-name
,则以引用方式捕获所有变量。
var-name
:以值方式捕获一个变量。所有的lambda函数默认都带const属性,即其不能修改值捕获变量的值(除非声明为mutable,但引用则不然。)
特别地,=
表示通过值方式捕获所有变量。this
:在类的非静态成员函数中,捕获this
指针。若不捕获,则不能使用this
显式/隐式调用该类的方法。注意捕获this
后其依然不是类的成员,不具有访问非public
成员的权限。*this
:C++17引入,在类的非静态成员函数中,为该lambda创建一个调用实例的副本(copy),相当于按值捕获了调用者自身。
不同的捕获方式以
,
相并列,通用捕获方法(=
,&
)可以被单个变量声明特化;同一个变量不能被相冲突的捕获方法声明。
C++20起,声明=
或&
蕴含捕获this
,但形如[=,this]
或[&,this]
的捕获声明是合法的(不作冲突计);this
和*this
的声明相冲突。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62int a = 1;
std::vector<int> vc{1,2,3};
auto f1 = [=]()
{
//captures vc and a by value
std::cout << a << vc[1] << std::endl;
//vc[0] = 2; //Not Compile: vc is const
//a += 1; //Not Compile: a is const
};
auto f2 = [=,&a](int x)
{
//captures vc by value and a by reference
a += x; //OK: ref-captured variables are modifiable
return x + vc.back();
};
//auto f3 = [&a,a](){}; //Not Compile: Conflicting statement on how to capture a
auto f4 = [=](int i,int x) mutable
{
vc[i] += x; //OK: Function was declared as mutable, enabling modification of value-captured variables
};
const int y = a;
auto f5 = [&y]()
{
//y += 1; //Not Compile: The const-status of ref-captured variable depends on the variable itself
};
class A
{
public:
int get() {return val;}
void some_const_func(){}
int& get2() {return val;}
void func()
{
auto fa = [this]{return get();}; //Explicit capture of this
auto fb = [=]{return get();}; //Since C++20: Implicit capture of this
auto fc = [&,this]{return get();} //universal statement and this can co-exist
//auto fd = [this]{return val;} //Not Compile: fd can't access private member val
//auto fe = [this,*this]{} //Not Compile: conflicting statement of capturing this
//auto ff = [*this]{return get();} //Not Compile: A captured by value, captured-by-value variables are const in lambda by default
auto fg = [*this]{some_const_func();} //OK
auto fh = [*this](int t) mutable {get2() += 1;} //mutable stated, works(yet now it does not affect the original copy A)
}
private:
int val;
};parameter-list
指参数列表,若其为空且函数未被声明为mutable
,则可以省略。
mutable
:可选项,lambda的函数体对capture-list
所有非引用类型变量均默认为const
,将lambda声明为mutable
将允许lambda内修改这些变量。
> 结合lambda本质是一个重载了operator()
类的实例,底层原理就是lambda的operator()
带有const
属性,所谓“捕获”变量其实就是将之纳入类内的数据成员。
> 相应地,将一个lambda声明为mutable
等价于将这些数据成员全部加以mutable
修饰。
noexcept
:C++23引入,可选项,修饰之和以noexcept
修饰一般函数作用相同。
-> return-type
:也叫 trailing return type ,相当于函数签名中指定函数的返回类型。C++14起为可选项,若省略之,则相当于函数的返回类型是auto
:
1
2
3
4
5
6
7
8auto f1 = []() -> double {return 1;};
//equivalent to
double f1() {return 1;}
//since C++14
auto f2 = []() {return 1;}
//equivalent to
auto f2() {return 1;}{function-body}
:函数体。
在拥有lambda大法后,上述排序行就可以改写为:
1 | std::ranges::sort(sp,[](const Stu& x,const Stu& y) {return x.id-y.id;}); |
lambda相较传统函数的优势有:
- 对函数的定义和应用极为灵活,且作为函数变量传递时,和查找函数指针定义相比较更为直观、易于维护;
- 可以自由控制lambda内部的命名可见情况(捕获列表),且其复用性较函数指针更强;
- 其“临时”特性给了编译器更大优化空间,效率更高;
- 作为一个对象,其定义和传递相较函数指针更为自由、更加安全。
1 | auto fid = [](const Stu& x,const Stu& y){return x.id-y.id;}; |
实际上在C++20中,ranges库带来的 projection 大法可以让上述排序代码更为简洁:
1 std::ranges::sort(sp,{},&Stu::id);
调用方式
匿名调用
既然叫lambda——“匿名”函数,自然以“匿名”之法调用之为一选项:
1 | std::views::filter(vec,[](auto&& at){return at.num > 3;}); |
具名调用
通过声明一个变量将之存储,实现更为灵活的调用和复用:
1 | auto fun = [](int ch) -> char {return ch + 48;}; |
注意,lambda表达式的变量类型只能声明为
auto
(只有编译器才知道lambda表达式的类型)。任何两个lambda表达式,即使其函数签名相同,类型也都不相同(相对地,函数签名相同的函数,其函数指针类型也相同):
1
2
3
4
5
6
7
8
9
10
11
12
13 void foo1(int);
void foo2(int);
void (*)(int)ptr = foo1;
ptr = foo2; //OK
static_assert(std::is_same_v<decltype(foo1),decltype(foo2)>); //passes
auto fun1 = [](int) -> void {};
//decltype(fun1) fun2 = [](int) -> void {}; //Not Compile: fun2 and fun1 are of different types, even their function signature is the same
auto fun2 = [](int) -> void {};
//static_assert(std::is_same_v<decltype(fun1),decltype(fun2)>); //Fails
返回函数
因为lambda函数本质上是一个对象(object),因此可以将之安全地作为返回值进行语义传递:
1 | auto fun_factory(int x) |
即时调用
由于[](){}
就相当于声明了一个 function
object,一定场景下,在其后再加点括号套娃也未尝不可:
1
char x = [&](size_t i){return std::string{vec[i]};}(3)[2];
底层实现
lambda表达式因其本质,亦被称为“函数对象”( function objects )。
现代C++中,“函数”("callable")泛化来说可以是以下之一:
- 函数指针(function
pointers),从C沿袭至今的、最原始的函数形式,变量形如
return-type(*)(parameter-list)
,如int(*)(int,int)
;
- 可调用对象(callable
objects),即重载了
operator()
类的实例,lambda表达式也是其中一种。
- 成员指针(pointer to member),符号形如
::*
。
在C++11前,通过以下方法也可以创建一个可以捕获变量的“函数对象”:
1 | int x = 1; |
考虑上述的实例,C++中lambda的可能看似“匪夷所思”的特性就不难解释了:
- 捕获的变量并不改变其属性(是否
const
、何种引用等等),但由于operator()
带有const
属性,因此不能更改按值捕获的变量;但可以通过非const
引用更改外部变量的值。
- 对于
mutable
的lambda,相当于给所有的捕获变量都加上了mutable
修饰。
C++20引入
<ranges>
时,所有STL Algorithm函数的ranges版本都采用了“函数对象”的实现方式,以此来避免ADL(Argument-Dependent Lookup)。
历代C++对lambda的改进
C++14
constexpr
相关:随着constexpr
函数限制的放宽让constexpr函数也像一个函数,所有的lambda表达式均默认成为constexpr
(在符合编译条件前提下)。泛型(generic)-lambda:C++11引入
auto
关键字后,在函数形参列表中使用之可以便捷地创建(模板变量不具名的)函数模板,如int fun(auto param);
。 C++14起,lambda函数也允许使用auto
来快速创建函数模板,极大地方便了临时调用场景下的使用(无需打出原始类型,甚至可以通过auto&&
来自动匹配const
和引用属性等)。如:
1
std::ranges::filter(vec,[](auto&& v){return v.back() > 3;});
指定初始化:C++14起,在使用值捕获一个变量时,可以在其后指定其初始化的方式(默认是通过复制构造函数初始化),如同在构造函数中传参进初始化列表一样。
如此可以自定义一个变量的初始化方式(复制或移动,或调用其它构造函数等等),使得 move-only 的类型也可以被lambda捕获。1
2
3
4
5int x = 3;
auto what_add = [a = x * 2](int t){return pow(t,a);};
std::unique_ptr<int> up;
auto take = [ptr = std::move(up)](int u){std::println("{}",u + *ptr;)};返回类型推导:C++14起,不指定 trailing return type 时函数返回类型可以自动推导(为
auto
),见上。
C++17
- 增加了对于
*this
的捕获,见上。
C++20
C++20起,可以在捕获声明
[]
后,添加模板声明<class T>
/<typename T>
,以此更为“丝滑”地达成模板参数复用的效果。例如:
1
auto fun = []<class Ty>(Ty&& arg){};
C++20起,捕获声明
[=]
或[&]
蕴含捕获this
,但其与this
的显式声明不冲突,具体见上。
C++23
允许对lambda表达式指定
noexcept
属性,见上。此前lambda表达式被声明为
mutable
时,空的参数列表不能省略;C++23起可以省略。
1
2
3auto f1 = []{return 1;} //Ignoring empty parameter list
auto f2 = []() mutable {return 1;} //pre-C++23
auto f3 = [] mutable {return 1;} //OK since C++23
结语
lambda函数自C++11引入以来,即已成为提高开发效率、增强程序可读性和易维护性的利器,极为适合日常应用中“轻巧”函数的定义、调用与传递,同时其部分特性(捕获控制、默认constexpr
等)也为编译器为之优化、进而提升效率提供了更多空间。