C++:Lambda :C++14中的lambda特性及使用。
Lambda高级特性及使用
本文主要总结 Lambda一些高级特性及属性:话不多说,以下是我即将叙述的内容。
💚💚💚
Lambda实现原理(可以通过 C++ Insights 观察代码生成定义)
值捕获和引用捕获区别举例
Lambda表达式生成对象的大小
模板参数包捕获
转换lambda表达式到函数指针
lambda表达式存储于容器
首先看一下C++ Primer 给出的lambda表达式的定义
一个lambda表达式表示一个可调用的代码单元。一个表达式具有如下形式
[捕获列表] (参数列表)-> 返回类型 {函数体}
1.Lambda实现原理(可以通过 C++ Insights 观察代码生成定义)
例如:我们定义下面一个 lambda表达式
lambda本身是一个函数对象,当编写好lambda表达式后,编译器会将该表达式翻译成一个未命名的类的未命名对象, 且在lambda表达式产生的类中含有一个重载的函数调用运算符。
// 定义一个简单的 lambda表达式
auto Lambda = [](int x) {return ++x;};
// 或者 带上返回类型
auto Lambda_2 = [](int x)->int { ++x;};
// 观察表达式生成的代码类定义
class __lambda_1_15
{
public:
inline /*constexpr */ int operator()(int x) const
{
return 0;
}
using retType_1_15 = int (*)(int);
inline constexpr operator retType_1_15 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ int __invoke(int x)
{
return __lambda_1_15{}.operator()(x);
}
};
__lambda_1_15 Lambda = __lambda_1_15{};
// 带上返回类型
class __lambda_2_17
{
public:
inline /*constexpr */ int operator()(int x) const
{
++x;
}
using retType_2_17 = auto (*)(int) -> int;
inline constexpr operator retType_2_17 () const noexcept
{
return __invoke;
};
private:
static inline /*constexpr */ int __invoke(int x)
{
return __lambda_2_17{}.operator()(x);
}
};
__lambda_2_17 Lambda_2 = __lambda_2_17{};
观察上述Lambda 生成的代码可知:
- auto Lambda = [](int x) {return ++x;}; lambda表达式,编译器会生成一个类对象 ,这个类对象的特点就是:会重载 operator()操作符,定义一个 函数指针(函数指针和 lambda表达式特点对应)。
- Lambda(1) —》调用Lambda表达式会调用 步骤一类对象的 函数指针
值捕获和引用捕获的区别
1. 无论是按值捕获还是按引用捕获,均只能捕获局部变量。
💚💚💚
如果Lambda 一定想要使用全局变量或者静态变量,那么直接使用即可。
// lambda 捕获全局变量:error : capture of variable 'x' with non-automatic storage duration
#include<iostream>
int x = 10;
int main()
{
auto Lambda = [x](){return x+2;};
std::cout<< Lambda();
}
上述Lambda表达式捕获了 全局变量x ,这种捕获方式,编译器是不能通过编译的。
// lambda 直接使用 全局变量
#include<iostream>
int x = 10;
int main()
{
auto Lambda = [](){return x+2;};
std::cout<< Lambda();
}
// 输出打印 : 12
2. 按值捕获,不能捕获一个类的成员变量。
// Lambda 正常的捕获 局部变量
#include<iostream>
class A
{
public:
A():a(0) {}
void func()
{
int b = 0;
auto fc = [b]{
std::cout<< "捕获b: " << b << std::endl;
return;
}
fc();
}
private:
int a;
}
int main()
{
A a;
a.func();
return 0;
}
// 打印结果: capture local b: 0
// Lambda 不能捕获成员变量
// 当我们试图 捕获 成员变量 a 时:
auto fc = [a]{
std::cout<< "capture local a: " << a << std::endl;
return;
};
编译结果
💚💚💚
直接说答案,我们可以同 this 指针,来修改Lambda 表达式
#include<iostream>
class A
{
public:
A():a(2) {}
void func()
{
int b = 0;
auto fc = [this]{
std::cout<< "capture local a: " << a << std::endl;
return;
};
fc();
}
private:
int a = 2;
};
int main()
{
A a;
a.func();
return 0;
}
// 运行打印结果:capture local a: 2
3. lambda 表达式按值捕获某个变量,在其函数体修改变量,会报错。
Lambda 规定 :在 lambda 函数体内不能修改变量的左值。
#include<iostream>
int main()
{
int a = 3;
auto lambda = [a](){
++a; // 实际上修改了变量左值。
}
lambda();
return 0;
}
上述代码编译后标错,并输出如下错误信息
$ g++ lambda.cpp -o lambda.exe
lambda.cpp: In lambda function:
lambda.cpp:8:5: error: increment of read-only variable 'a'
++a;
^
lambda.cpp: In function 'int main()':
lambda.cpp:10:2: error: expected ',' or ';' before 'lambda'
lambda();
首先我们先通过 分析下上述Lambda表达式经编译器后扩展为如下实现
注:上述代码编译器会编译失败,但是为了让大家看清扩展的代码,我将其伪代码贴出来。
#include<iostream>
int main()
{
int a = 3;
class __lambda_7_16
{
public:
inline /*constexpr */ void operator()() const
{
++a;
}
private:
int a;
public:
__lambda_7_16(int & _a)
: a{_a}
{}
};
__lambda_7_16 lambda = __lambda_7_16{a};
lambda.operator()();
return 0;
}
💚💚💚
上述编译器扩展后的代码可知:
Lambda 闭包类有一个 operator()操作符,而且用 const 修饰,所以不能在该接口中修改闭包类的成员。
3.1. 修改方案
要想解决该问题,我们有三种方案
- 将operator() 接口中的 const 修饰符去掉 。
- 将闭包类的成员变量声明为 mutable
- 按引用捕获变量
💚 将闭包类的成员声明为 mutable :相当于去掉了 operator()接口的 const 属性
#include<iostream>
int main()
{
int a = 3;
auto lambda = [a]() mutable{
++a;
std::cout << "a value: " << a<< std::endl;
};
lambda();
return 0;
}
// 看一下 编译后的类代码:显然在 operator() 操作符中去掉了 const属性
#include<iostream>
int main()
{
int a = 3;
class __lambda_7_16
{
public:
inline /*constexpr */ void operator()()
{
++a;
std::operator<<(std::cout, "a value: ").operator<<(a).operator<<(std::endl);
}
private:
int a;
public:
__lambda_7_16(int & _a)
: a{_a}
{}
};
__lambda_7_16 lambda = __lambda_7_16{a};
lambda.operator()();
return 0;
}
💚:通过用于捕获修改lambda 函数体外的变量
#include<iostream>
int main()
{
int a = 4;
auto lambda = [&a]() {
++a;
std::cout << "a value: " << a<< std::endl;
};
lambda();
return 0;
}
// C++ Insight 编译后的代码:类对象成员变量a 已经修改为引用。
#include<iostream>
int main()
{
int a = 4;
class __lambda_7_16
{
public:
inline /*constexpr */ void operator()() const
{
++a;
std::operator<<(std::cout, "a value: ").operator<<(a).operator<<(std::endl);
}
private:
int & a;
public:
__lambda_7_16(int & _a)
: a{_a}
{}
};
__lambda_7_16 lambda = __lambda_7_16{a};
lambda.operator()();
return 0;
}
// 打印结果:a value: 5
3. 引用捕获注意事项
3.1 引用捕获应该避免 :悬空引用
引用可以改变外部变量; 但有可能出现悬挂引用(Dangling reference)必须保证lambda 调用时,捕获引用变量依然有效
// 悬挂引用
#include<iostream>
#include<functional>
#include<string>
void func(std::function<int()>& fb) {
std::string a("abcdefr");
fb = [&a]() {
std::cout << a << std::endl;
return 0;
};
}
void func11() {
std::string a("a");
std::cout << a << std::endl;
}
int main() {
std::function<int()> fb;
func(fb);
fb();
func11();
return 0;
}
打印结果:很显然 lambda 并没有打印出 预期字符 “abcdefr”。
这就是悬挂引用捕获带来的影响,至于为什么会出现这个问题,这实际上是:未定义行为,编译器不知如何处理带来的。
关于C++未定义行为,C++有如下规范说明,你可以好好体会一下。
💚💚💚
作为 C/C++ 的一大特色,未定义行为让不少人都头疼不已。未定义行为不可预测的特点,使调试的难度加大了不少。
避开未定义行为的关键是养成良好的编程习惯。当然一些辅助的检测手段对于消除未定义行为也能起到非常大的帮助。
Lambda表达式生成对象的大小
针对这个问题,首先需要明白:lambda表达式必然会将捕获的的值存放在某个地方,现在我们来看看 lambda表达式占用的内存。
#include<iostream>
int main() {
int a = 0;
int b = 0;
auto lambda = [a,b](){return 1;};
std::cout << sizeof(lambda) << std::endl;
auto lambda2 = [&a,&b](){return 1;};
std::cout << sizeof(lambda2) << std::endl;
auto lambda3 = [a,&b](){return 1;};
std::cout << sizeof(lambda3) << std::endl;
auto lambda4 = [](){return 1;};
std::cout << sizeof(lambda4) << std::endl;
return 0;
}
// 打印结果
8
16
16
1
从实验我们可以看出:按值捕获内存大小和引用的数据本身类型是正相关的,按引用捕获会导致 lambda 内存变大,而空捕获的lambda 表达式只会占用一个字节。
模板参数包捕获
Lambda 表达式可以通过在捕获列表中 声明 [args…] 这种形式,进行可变参数捕获。我们可以参考如下代码
#include<iostream>
#include<tuple>
template <typename... Args>
void captureTest(Args... args) {
auto lb = [args...] () {
auto tpl = std::make_tuple(args...);
std::cout << "tpl size: " << std::tuple_size<decltype(tpl)>::value << "\n";
};
lb();
}
int main() {
captureTest(1.0, 2.0, "hello world",'a',true);
return 0;
}
// 打印结果
tpl size: 5
转换Lambda 表达式到函数指针
在C+中给出了标准定义
The closure type for a lambda-expression with no lambda-capture has a public non- virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.
即只有 无状态闭包才能转换为 函数指针,也即没有捕获任何变量的 lambda 表达式才能转换为相应的函数指针。
lambda表达式存储于容器
在C++11中,我们一般通过 std::function,将lambda表达式存储于 vector容器汇总。
针对同类型的无状态 lambda表达式,其可以直接存储于 vector容器中,这是因为无状态 lambda表达式会转换为函数指针,因此 此时 vector容器存储的是相应的同类型函数指针。
#include<iostream>
#include<vector>
int main() {
auto lb = [](){};
std::vector<decltype(lb)> vc;
vc.push_back([](){});
return 0;
}
// 上述代码不会编译通过:因为每个 lambda表达式所生成的闭包类型都是唯一的且互不相同,但是 vector 中需要存储相同类型的闭包,
// 所以此代码编译不通过。
更多推荐

所有评论(0)