3 函数
约 3210 字大约 11 分钟
2025-06-22
语法格式
[capture](params) specifiers exception-> ret {body};
- capture——捕获列表,用于捕获外部变量 [ ]不捕获任何变量 [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)(拷贝的副本在函数体内部是只读的) [=,&foo]按值捕获外部作用域中的所有变量,并按引用捕获变量foo [bar]只按值捕获bar变量,不捕获其他变量 [&bar]只按引用捕获bar变量 [this]捕获当前类中的this指针(让lambda表达式拥有和当前类成员函数同样的访问权限,如果已经使用了&或者=,默认添加此选项)
- params——参数列表
- specifers——可选限定符。mutable允许我们在lambda表达式函 数体内改变按值捕获的变量,或者调用非const的成员函数
- exception —— 可选异常说明符,我们可以使用noexcept来指明lambda是否会抛出异常。
- ret——函数返回值(一般不需要写,编译器会自己推导出来) 但返回的是初始化列表就不会自己推导,需要自己写出来
- body——函数体
capture
能捕获的变量必须是一个自动存储类型。简单来说就是非静态的局部变量。
int x = 0;
int main() {
int y = 0;
static int z = 0;
auto foo = [x, y, z] {};
return 0;
}
x和z会报错,因为变量x和z不是自动存储类型的变量,x不存在于lambda表达式定义的作用域。 如果想在lambda表达式中 使用全局变量或者静态局部变量可以不用捕获,因为全局变量和静态局部变量是所有作用域都可以使用的
[y] { return x + y + z; };
params
当不需要传入参数时可以省略小括号,这样就是结构最简单的匿名函数了
[]{}
但如果需要限定符的话即使不需要传入参数也要加小括号
[]()mutable{}
specifers
int a=1;
int b=2;
[=,&b]()mutable {
int c=a;
int d=b;
a++; //error
cout<<'a'<<a<<endl;
};
因为b是由按值捕获的因此不能修改,但可以加mutable关键字修改,虽然加了mutable修改了值但这个值依旧是拷贝过来的,对原本的数据没有影响
int a=1;
int b=2;
[=,&b]()mutable {
int c=a;
int d=b;
a++; //ok
cout<<'a'<<a<<endl;
};
以上情况只是定义,并没有调用,在表达式末尾加上()就表示调用了
[=,&b]()mutable {
int c=a;
int d=b;
a++;
cout<<'a'<<a<<endl;
}();
如果表达式有参数需要在末尾的小括号传入实参
[=,&b](int z)mutable {
int c=a;
int d=b;
a++;
cout<<'a'<<a<<endl;
}(10);
本质
lambda表达式类型在C++中会被看作是一个仿函数类型 lambda表达式的优势在于书写简单方便且易于维护,而函数对象 的优势在于使用更加灵活不受限制,但总的来说它们非常相似。而实际上这也正是lambda表达式 的实现原理。 ambda表达式在编译期会由编译器自动生成一个闭包类,在运行时由这个闭包类产生一个对 象,我们称它为闭包。在C++中,所谓的闭包可以简单地理解为一个匿名且可以包含定义时作用 域上下文的函数对象。
无状态lambda表达式
C++中无状态的lambda表达式可以隐式转换为函数指针
void f(void(*)()) {}
void g() { f([] {}); } // 编译成功
在上面的代码中,lambda表达式[ ] {}隐式转换为void(* )()类型的函数指针
void f(void(&)()) {}
void g() { f(*[] {}); }
这段代码也可以顺利地通过编译。我们经常会在STL的代码中遇到lambda表达式的这种应用。
可构造和可赋值的无状态lambda表达式
无状态lambda表达式可以转换为函数指针,但在C++20标 准之前无状态的lambda表达式类型既不能构造也无法赋值,这阻碍了许多应用的实现。 像stdfind_if这样的函数需要一个函数对象或函数指针来辅助排序 和查找,这种情况我们可以使用lambda表达式完成任务。但是如果遇到stdmap的比较函数对象是通过模板参数确定的,这个时候我们需要的是一个类型
auto greater = [](auto x, auto y) { return x > y; };
map<string, int, decltype(greater)> mymap;
它首先定义了一个无状态的lambda表达式greate,然后使用 decltype(greater)获取其类型作为模板实参传入模板。但是在C++17标准中是 不可行的,因为lambda表达式类型无法构造。编译器会明确告知,lambda表达式的默认构造函数已经被删除了 除了无法构造,无状态的lambda表达式也没办法赋值
auto greater = [](auto x, auto y) { return x > y; };
map<string, int, decltype(greater)> m1, m2;
m1 = m2;
复制赋值函数也被删除了
广义捕获
C++14标准中定义了广义捕获,所谓广义捕获实际上是两种捕获方式
- 第一种称为简单捕获,这种捕获就是我们在前文中提到的捕获方法,即[=]、[&]以及[this]等。
- 第二种叫作初始化捕获,这种捕获方式是在C++14标准中引入的,它解决了简单捕获的一个重要问题,即只能捕获lambda表达式定义上下文的变量,而无法捕获表达式结果以及自定义捕获变量名
int x = 5;
auto foo = [x = x + 1]{ return x; };
捕获列表是一个赋值表达式,不过这个赋值表达式有点 特殊,因为它通过等号跨越了两个作用域。等号左边的变量x存在于lambda表达式的作用域,而等号右边x存在于main函数的作用域。
int x = 5;
auto foo = [r = x + 1]{ return r; };
更清晰的写法 变量r只存在于lambda表达式,如果此时在lambda表达式函数体里使用变量 x,则会出现编译错误。
初始化捕获在某些场景下是非常实用的
- 使用移动操作减少代码运行的开销
std::string x = "hello c++ ";
auto foo = [x = std::move(x)]{ return x + "world"; };
使用std::move对捕获列表变量x进行初始化,这样避免了简单捕获的复制对象操作,代码运行效率得到了提升。
- 在异步调用时复制this对象,防止lambda表达式被调用时因原始this对象被析构造成未定义的行为
泛型lambda表达式
C++14标准让lambda表达式具备了模版函数的能力,我们称它为泛型lambda表达式。 虽然具备模版函数的能力,但是它的定义方式却用不到template关键字。只需要使用auto占位符即可
auto foo = [](auto a) { return a; };
int three = foo(3);
char const* hello = foo("hello");
模板语法的泛型lambda表达式
lambda表达式通过支持auto来实现泛型。但这种语法也会使我们难以与类型进行互动,对类型的操作变得异常复杂。
template<typename T>
struct is_std_vector : false_type {
};
template<typename T>
struct is_std_vector<vector<T>> : true_type {
};
auto f = [](auto vector) {
static_assert(is_std_vector<decltype(vector)>::value, "");
};
普通的函数模板可以轻松地通过形参模式匹配一个实参为vector的容器对象,但是对于 lambda表达式,auto不具备这种表达能力,所以不得不实现is_std_vector,并且通过 static_assert来辅助判断实参的真实类型是否为vector。 把一个本可以 通过模板推导完成的任务交给static_assert来完成是不合适的。除此之外,这样的语法让获取 vector存储对象的类型也变得十分复杂
auto f = [](auto vector) {
using T = typename decltype(vector)::value_type;
};
能这样实现已经是很侥幸了。vector容器类型会使用内嵌类型value_type表示存储对象的类型。但我们并不能保证面对的所有容器都会实现这一规则,所以依赖内嵌类型是 不可靠的。
auto f = [](const auto &x) {
using T = decltype(x);
T copy = x; // 可以编译,但是语义错误
using Iterator = typename T::iterator; // 编译错误
};
int main() {
vector<int> v;
f(v);
}
上面的代码中,decltype(x)推导出来的类型并不是stdvector &,所以T copy = x;不是一个复制而是引用。对于一个引用类型来说,T::iterator也是 不符合语法的,所以编译出错。 可以使用了 STL的decay,这样就可以将类型的cv以及引用属性删除:
auto f = [](const auto &x) {
using T = std::decay_t<decltype(x)>;
T copy = x;
using Iterator = typename T::iterator;
}
问题虽然解决了,但是要时刻注意auto,以免给代码带来意想不到的问题,况且这都是建立 在容器本身设计得比较完善的情况下才能继续下去的。 鉴于以上种种问题,C++委员会决定在C++20中添加模板对lambda的支持,语法非常简单:
[]<typename T>(T t) {}
于是,上面的例子就可以改写为:
auto f = [](vector vector) { };
以及
auto f = [](T const& x) { T copy = x; using Iterator = typename T::iterator; };
不仅简洁了很多,而且也更符合C++泛型编程的习惯。
常量lambda表达式和捕获* this
C++17标准对lambda表达式同样有两处增强: 一处是常量lambda表达式,主要特性体现在constexpr关键字上 一处是对捕获* this的增强。
为了更方便地复制和使用* this对象,C++17增加了捕获列表的语法 来简化这个操作,具体来说就是在捕获列表中直接添加[* this],然后在lambda表达式函数体内直 接使用this指向对象的成员 [* this]的语法让程序生成了一个* this对象的副本并存储在lambda表达式 内,可以在lambda表达式内直接访问这个复制对象的成员,消除了之前lambda表达式需要通过 tmp访问对象成员的尴尬。
捕获[=,this]
C++20中,对lambda表达式进行了小幅修改。让this指针的相关语义更加明确。 [=]可以捕获this指针,相似的,[=,* this] 会捕获this对象的副本。 但是在代码中大量出现[=]和[=,* this]的时候我们可能很容易忘记前者与 后者的区别。为了解决这个问题,在C++20标准中引入了[=, this]捕获this指针的语法,它实际上 表达的意思和[=]相同,目的是区分它与[=,* this]的不同
[=, this]{}; // C++17 编译报错或者报警告, C++20成功编译
虽然在C++17标准中认为[=, this]{};是有语法问题的,但是实践中GCC和CLang都只是给出 了警告而并未报错。另外,在C++20标准中还特别强调了要用[=, this]代替[=]
template <class T>
void g(T) {}
struct Foo {
int n = 0;
void f(int a) {
g([=](int k) { return n + a * k; });
}
};
编译器会输出警告信息,表示标准已经不再支持使用[=]隐式捕获this指针了,提示用户显式 添加this或者* this。
同时用两种语法捕获this指针是不允许的
[this, *this]{};
会报错
新函数
emplace
在C++11之后为标准库容器提供了一个emplace系列的函数,用于取代旧函数,emplace系列函数在性能上有优势
emplace_back
其中emplace_back用于取代push_back push_back和insert在插入元素的时候是将整个元素拷贝过去,然后在容器内进行拷贝构造创建一个新元素,而emplace_back是将元素的参数传递过去调用构造函数
这样emplace_back能就地通过参数构造对象,不需要拷贝操作,相比push_back能更好的避免内存的拷贝和移动,提升容器插入元素的性能
经过测试,emplace_back在使用时只会调用一次构造函数,而push_back在使用时会调用一次构造再调用一个拷贝(或者移动)
- emplace函数需要对应的参数对象有对应的构造函数,不然编译报错
- emplace函数在容器中直接构造元素传递给emplace函数的参数必须与元素类型的构造函数相匹配
贡献者
版权所有
版权归属:PinkDopeyBug