CPP {Lambda函数}
@LOC_1
Lambda函数
定义
lambda的本质 你可以理解为是一个类对象, 她里面的成员变量 就是所捕获的外部量[]
和 Lambda函数参数, 其实 你完全可以把[]
捕获的外部值 和 Lambda的函数参数 完全混为一谈;
.
即她与普通函数 是完全不同的, 普通函数(全局函数/成员函数(成员函数本质是 函数参数里有一个this
)) 他们在编译期就放入了.text
段 即普通函数的地址 是固定死的; 但Lambda不是的! 她是运行时的, *只有在程序执行时 遇到了[](){}
Lambda函数时 此时会生成了一个Lambda_()
对象(如下定义), 她是完全动态的, 并不是说 程序在编译期就确定了一个Lambda函数;
struct Lambda_{
//>> []里所捕获的值
T & obj; // 浅拷贝方式: 对应`[&]`模式
const T obj; // 深拷贝方式: 对应`[=]`模式
//>> ()参数
Params par;
//>> {}函数执行逻辑
Func func;
};
因此当你写成[?](?){}
时, 本质上 就是创建了一个Lambda()
对象; (一定要从这个角度 很多BUG就迎刃而解了);
lambda可以分为3种模式: [], [&], [=]
([obj] 可以理解为是[=]; [&obj]可以理解为是[&]
);
.
但这三种模式, 并不是完全独立无关的, 他们之间也有一些共同点;
int D4;
struct ST{
int D0;
void F( int D1){
int D2;
static int D3;
[](){}; // Lambda
}
};
Lambda捕获外部量的方式, 分为: {自动捕获, 显式捕获}; 自动捕获 一定不会出现在[]
里面 这是不允许的, 显式捕获 即你自己需要手动添加到[]
里面;
.
比如说D4, D3
就是自动捕获 即即使是[]
她也能访问的量, 且自动捕获 一定是浅拷贝方式, 你如果[D4] 或 [&, D4] 或 [&D4]
都是错误的(此时会爆错误error: 'D4' cannot be captured because it does not have automatic storage duration
);
.
其他的量this, D1, D2
必须通过显式捕获方式 才可以获取(D0
不算是外部量 因为她属于this
里面); 此时 她可以是{浅拷贝/深拷贝}, 但有个例外 就是this
你不管是[&]/[=]
捕获的都是当前类对象的浅拷贝 (并不会说 创建了一个ST对象), 因为this == ST*
因此即使[=]
她也是拷贝了一个指针而且, 之所以[&this]
这个语法是错误的 因为this == (ST*)的右值
你不能对右值取地址;
算法
匿名函数的效率
对于一个完全相同的函数, 他的三个实现方式: F0: 全局普通函数; F1: std::function<> = [&]方式; F2: auto = [&]( auto & _dfs)方式
;
假如F0
他最多递归1e6
次(这反映了他使用的栈空间大小), 耗时为500
, 则:
.
F1
最多递归5e5
次(这说明他会使用额外的更多的栈空间, 其原因可能是:sizeof(F1)==32
占了内存 而sizeof(F2)==1
), 耗时为700
;
.
F2
最多递归1e6
次(与普通函数一样), 耗时为300
;
(注意, 虽然说匿名函数还有一个[&]
, 即他还会捕获很多局部变量(而普通函数不会), 但这个花销 不必考虑 影响很小);
因此, 综上, 一定要使用F2
这种方式;
错误
涉及Lambda, 一定要从 我们定义中讲的struct Lambda_
这个角度去理解 即Lambda函数的本质 就是构造了一个Lambda_
类对象, 很多BUG就迎刃而解了;
比如Func( int D0){ int D1; connect( ..., [&](){ ...});
, 虽然说 你捕获了一些临时变量 即你Lambda里面是可以访问D0,D1
的, 但是等到你信号触发时 此时D0,D1
早摧毁了 就会出错; 即使你写成=
的方式, 虽然使用D0,D1
没问题, 但是你早不在Func
函数里面了 没有了上下文 你访问D0,D1
也没用;
.
因此 涉及到connect
时, 强烈建议 不要写[&/=]
, 而是你用到什么 写放什么, 而且 实际上 一般写[this]
就够了, 因为你放临时变量 等到信号触发时 早就不在上下文了; (@MARK: @LOC_0
);
再举个很有意思的例子: Func( int D0){ if( D0 == 0){ connect( ..., [D0](){ ...});} }
, 我们会依次调用Func(0, 1, 2, 3)
然后我们会触发一次信号; (不可以写[&D0]
因为她是临时变量 等信号触发 就野指针了)
.
虽然说 最后一次D0 == 3
, 但是 触发信号 你会发现 Lambda捕获的D0 == 0
, 这就可以从我们讲的Lambda函数的本质 就是构造了一个Lambda_
对象的角度去解释, 第一次D0==0
时 此时Lambda函数 她所构造的Lambda对象里 就是const int D0 = 0
;
@DELI;
[&]
的使用规范
class ST{
D0;
void F( D1){
static D2;
D3;
lam0 = [&](){};
lam1 = [=](){};
}
}
对于lam0 假如你会在`F`函数执行之外 执行lam0, 那么你只能使用`D0, D2`, 不可以使用`D1, D3`, 虽然你还是能访问到他俩, 但是会发生意想不到的错误;
`.` 因为, `D1, D3`都是临时变量, 比如lam0 她是槽函数, 那么等信息触发 她根本就不在`F()`函数内, 因此`D1,D3`早没了;
lam1就不存在这个问题, `D0...D3`都可以用;
假如你lam0就是要用, 那么写成`[&, D1, D3]` 必须值传递;
@DELI;
以下的整个解释 都是完全错误的! 请依据最新的上面定义里 对Lambda的解释 去理解这个问题;
.
很简单, 因为每一次 都定义了一个Lambda对象;
#lambda他获取的外部变量, 在函数每一次调用 他才会去获取#
FOR_( i, 1, 20){
int * j = new int;
auto f = [&](){ DE_(&j);}; // 每次输出的地址是不一样的, 因为外界的`j`变量 他是临时变量 会一直摧毁和创建; 注意我们输出的是`&j` 不是`j` 因为我们要输出`j`这个变量的地址;
f();
}
我们知道 随着每次for循环, j
的地址是一直在变化的, 而我们调用f()
他输出的地址 不是一个死的固定地址 而是动态变化的;
这说明, 函数[&]
这个代码, 他并不是说 在一开始第一次定义这个函数时 就给他传入了一个死的j
地址进入, 不是的, 而是: 在每一次你调用f()
函数时, 他在运行时动态的 去获取此时j
的地址;
.
我猜测: 他[&]
捕获的, 其实是j
这个变量名, 而不是j
的地址, 这样 当你使用j
时, 就是类似于宏定义;
. .
再举个例子, 你把代码改成auto f = [=](){ DE_(i);};
, 你会发现f()
每次输出的i
是动态变化的, 也是说明 即使是[=]
他也不是说 一开始函数定义时 给你传入了一个i==1
常量, 不是的, 他也是捕获了i
这个变量名(宏定义), [=]
是执行了auto i = ::i
操作 即复制了一份新的对象;
.
一定要理解, 这里的变量名/宏定义 的深度含义, 不管是[&] [=]
其本质 确实是这样的, 以下代码可以印证:
struct ST{
int d; ST(){} ST( ST const& _a){ DE_(_a.d); d = _a.d;}
};
FOR_( i, 1, 20){
ST s; s.d = i;
auto f = [=](){ DE_(s.d);}; // 每次输出的 就等于`i`;
f(); // 此时 因为`[=]` 他会用`s`来构造一个新的对象出来, 这是动态的 即调用`f()`时, 拿*此时的s* 去构造新对象;
}
@DELI;
#错误#: 为了效率起见, 对于递归函数 如果他的量级很大(比如1e6) 就不用写成匿名函数了, 因为比起普通函数 他的效率要低的多, 会导致超时; 对于非递归函数, 可以使用;
这是错误 的, 因为写成auto dfs = [&]( auto _dfs)
方式, 效率很高;
@DELI;
function<void()> p;
{
T obj;
p = [&](){
cout<< obj;
};
}
p();
这是个严重错误的代码, 你的匿名函数 虽然获取了obj
, 但是 等到你调用p()
时, 此时的obj
已经析构掉了, 除非你使用[=]()
;
性质
lambda 他非常强大…
他可以写模板函数, 比如 auto F = []( auto _a){ DE_(_a);};
那么他就是模板函数 即调用F(123), F("abc"), F(3.333)
就和调用模板函数一样;
他也可以直接运行(即不需要非得auto F = lambda;
), 即直接lambda( 123)
, 而且她还可以是constexpr
函数;
.
比如constexpr int A = []( int _p){ int ANS = 1; while(_p>0){ --_p; ANS*=10;} return ANS;}( 8);
, 这等价于是constexpr int A = 1e8;
;
@DELI;
auto F = [?](?){};
, 其中 lambda表达式 他与F
接收值(函数指针) 两者是毫无关联的! 两者是完全独立的, 不要说F
是lambda 不是的 她就是个函数指针 说白了 auto == Lambda_& (即我们上面定义的那个Lambda_类)
; (比如说 QT里connect
时 我们就放了一个lambda 并不需要把他转成函数变量;
@DELI;
T t;
写法1: [](参数)->返回值{}; // 不能访问`t`
写法2: [=](参数)->返回值{}; // 可以访问`t` 但不能修改他! 而且这是*值拷贝*(即你此时使用`t` 其实他不是上面那个 而是*新拷贝了一个* 调用了`T(T const&)`构造);
写法3: [&](参数)->返回值{}; // 可以访问`t` 也可以修改(她是*引用传递*);
写法4: [t](参数)->返回值{}; // 效果与*写法2*一样;
写法5: [&t](参数)->返回值{}; // 效果与*写法3*一样;
@DELI;
用变量来接受这个lambda;
写法1: auto func = lambda; // 此时`sizeof(func)==1`;
写法2: std::function<返回值(参数)> func = lambda; // 此时`sizeof(func)==32`;
@DELI;
#支持递归#
写法1: std::function<void(int)> dfs = [&](int){ dfs(123);};
. 注意两点, 一个是[&], 一个是std::function的声明;
写法2: auto dfs = [&]( auto _dfs, int)->void{ _dfs( _dfs, 123);}
. 注意, 到时候外部调用时是dfs(dfs,?); 必须要显式的声明返回值(即->void); 参数`auto _dfs`不一定放在开头参数 放哪都可以;
@DELI;
auto F = [](...){...};
此時sizeof(F) == 1
, 為啥?
而如果寫成function<void()> F = ...
, 此時sizeof(F) == 32
(即他的大小 就是function
這個類的大小 與你實際的函數定義無關);
@DELI;
函數變量 也可以加static
;
auto dfs = [&](){ ...};
, 如果寫成static auto dfs = ...;
效率會高 (因為之前是每次都申請一個變量);
@DELI;
最好顯式的寫出 返回值, 即auto F = []() -> ?{};
;
如果要自動推導的話 她是不會進行強轉的 (即你必須保證你所有的返回值 類型是完全一致的), 比如[](){ if(?){return 0;} return 0LL;}
這個代碼是錯誤的 (一個int
一個LL
, 雖然可以強轉 但會報錯);
.
一個例子是, if(?){ return 0LL;} int64_t a; return a;}
, 如果你的電腦上 int64_t == long long
則沒問題, 可如果在其他編譯器上 他是long int
, 那這會出錯;
@DELI;
#参数可以写成auto
#
sort(A.begin(), A.end(), []( auto & a, auto & b){ return a < b;});
@DELI;
成员函数内部的匿名函数
类的成员函数内部的匿名函数, 如果要执行其他成员函数, 需要在加入[this]
的访问权限, 否则如果是[](){}
你可以认为 他就是个全局函数;
class ST{
void f(){}
void g(){
auto fun = [this](){ f();};
}
};
@DELI;
递归匿名函数
对于一个非递归的匿名函数, 可以用auto
, auto func = [&](int _a) -> void{ };
但是, 如果该函数是(递归)的, 则会报错: variable 'func' declared with deduced type 'auto' cannot appear in its own initializer func( _a)
此时有2种做法:
1: 要显式的定义他的类型, 即: function<void(int)> func = [&]...
(而且必须要有[&]
)
2: 因为函数本身也是個變量 可以把他也放到参数里面: auto func = [&]( auto _func, ...) -> ?{ _func( _func, ...);
(注意递归调用使用的是_func
, 而不是func
), 返回值也必須寫(->?
);
@DELI;
匿名函数的访问域;
int a;
vector< int> b;
[](){
//< `a,b,c` are not accessible.
};
[&](){
//< `a,b` are accessible and can be modified
};
[=](){
//< `a,b` can only be visited;
};
[a, &b](){
//< `a` can only be visited, `b` can be modified;
};
int c;
@DELI;
[&]修改外部变量
int a = 123;
auto F = [&](){ //< or `[&a](){`
a = 666;
};
The value of a
will be modified.
@DELI;