CPP {Lambda函数}

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;

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值