16 | 函数对象和lambda:进入函数式编程
16 | 函数对象和lambda:进入函数式编程
讲述:吴咏炜
时长14:32大小13.27M
C++98 的函数对象
函数的指针和引用
Lambda 表达式
变量捕获
泛型 lambda 表达式
bind 模板
function 模板
内容小结
课后思考
参考资料
赞 3
提建议
精选留言(24)
- zhengfan2020-07-10吴老师,您好。 四刷本讲。 我对您文中的这一句介绍有点好奇,“每个 lambda 表达式都有一个全局唯一的类型”。 请问这是怎么做到的?本质上不应该是一个函数指针么?这么规定的目的是什么?
作者回复: 还真不是函数指针。可以认为编译器帮你组合出了一个唯一的类型名称吧。 举一个例子,如果你写: auto adder = [n](int x) { return n + x; }; 编译器产生的代码类似于: struct lambda_1266ab7e { lambda_1266ab7e(int n) : n_{n} {} auto operator()(int x) const { return n_ + x; } private: int n_; }; auto adder = lambda_1266ab7e(n);
24 - 廖熊猫2020-01-02老师新年快乐。 lambda表达式大概是生成了一个匿名的struct吧,实现了operator(), 捕获的话对应struct上的字段。
作者回复: 新年快乐。 对,概念上就是这样。
14 - tt2020-01-021、感觉lambda表达式就是C++中的闭包。 2、lambda表达式可以立即进行求职,这一点和JavaScript里的立即执行函数(Imdiately Invoked Function Expression,IIFE)一样。在JavaScript里,它是用来解决作用域缺陷的。 感觉在动态语言里被用到极致的闭包等特性,因为C++的强大、完备,在C++里很普通。 lambda的定义对应一个匿名函数对象,捕获就是构造这个对象时某种方式的初始化过程,用lambda表达式隐藏了这个过程,只保留了这个意思,更直观和写意。 老师,我对协程很感兴趣,C++会有协程么?隐约感觉捕获变量这个东西是不是可以用在实现协程上? 最后,祝老师新年快乐!展开
作者回复: 对,就是闭包。 Stackful 协程见 Boost.Coroutine2。Stackless 协程已经进入 C++20,第 30 讲讨论。🤓 新年快乐!
8 - tt2020-02-14老师,回过头来看得时候,遇到了一个问题。 在用LAMBDA表达式解决多重初始化路径的问题时,说到这样还可以提高性能,因为不需要默认构造和不需要拷贝/移动。可是在第10讲中讲返回值优化的时候,不是说如果返回值时有条件判断,编译器都被会难倒,从而导致NRVO失效么(函数getA_duang)?
作者回复: 注意我这儿用的是 return Obj(…) 的形式,不是有名变量的返回(Obj a{…} 然后 再 return a),不属于 named return value optimization 的情况。NRVO 指的是本地变量的返回。C++17 开始,prvalue 从语言上作了特殊解释,要求这样的返回直接构造到目的位置。
共 2 条评论4 - 总统老唐2020-01-012020第一课,吴老师新年好
作者回复: 谢谢🙏。在这儿也顺祝所有的同学们新年好!😇🎈🎊
4 - Daniel2022-03-21class task { public: task(int data) : data_(data) {} auto lazy_launch() { return [this, count = get_count()]() mutable { ostringstream oss; oss << "Done work " << data_ << " (No. " << count << ") in thread " << this_thread::get_id() << '\n'; msg_ = oss.str(); calculate(); }; } void calculate() { this_thread::sleep_for(100ms); cout << msg_; } private: int data_; string msg_; }; 输出: Done work 37 (No. 2) in thread 3 Done work 37 (No. 2) in thread 3 按引用捕获,应该是线程1输出之前msg_被线程2覆盖了。展开
作者回复: 是这样。😊
2 - 罗 乾 林2020-01-01编译器遇到lambda 表达式时,产生一个匿名的函数对象,各种捕获相当于按值或者按引用设置给匿名对象的成员字段。 不对的地方,望老师指正。 对function<int(int, int)>这货怎么实现的比较好奇,大多数模板参数都是类型,做的都是是类型推导,这货居然是int(int, int)
作者回复: lambda表达式的理解没啥问题。 int(int, int) 也是一个类型:一个接受两个整数参数、返回一个整数的函数。function 的主要复杂性,应该是需要处理函数、函数指针、函数对象等各种情况。函数对象的大小不确定,因而 function 需要在堆上分配内存。operator() 我记得相当于一个虚函数调用的复杂度。
2 - ReCharge2022-06-11& 加本地变量名标明对其按引用捕获(不能在默认捕获符 & 后出现;因其已自动按引用捕获所有本地变量) 老师这句话有问题么?括号内外感觉描述矛盾 &本地变量名:这种写法不被允许么?
作者回复: 我的意思是,可以写 [&a, &b],也可以写 [=, &a, &b],但不能写 [&, &a, &b]。
1 - EncodedStar2020-01-09函数指针和引用这个模块中 当我们拿 add_2 去调用这三个函数模板时,fn 的类型将分别被推导为 int (*)(int)、int (&)(int) 和 int (*)(int)。 第一个和第三个都是 int (*)(int) 第一个是不是 int (int)
作者回复: 不是。你漏看了这句话: “除非你用一个引用模板参数来捕捉函数类型,传递给一个函数的函数实参会退化成为一个函数指针。”
1 - 空气2020-01-04吴老师,我在工作中很经常用到function。文中讲到function对象的创建比较耗资源,能否介绍一下原因,或者可以参考哪些资料?确实要使用的话,是否有必要使用共享指针管理来减轻复制和转移消耗? 如果lambda的推导类型不是function,那是什么类型呢?和function有什么区别?
作者回复: 你如果不是频繁创建 function 对象的话,关系也不大吧。我觉得多考虑移动就行了。除非性能测试工具报告瓶颈就在这儿了,用智能指针去优化不太值(毕竟需要修改使用的代码)。 每个 lambda 都有自己的独特类型,每次定义相当于编译器帮你产生了一个函数对象(就像这一讲里定义的那些函数对象一样)。 具体如何实现,我倒没读到过相关的文章。你可以网上搜搜看,或者阅读标准库里的源码。
1 - Geek_603cec2022-11-17 来自江苏我提一个小问题: 原文:虽然函数名字叫 accumulate——累加——但它的行为是通过第四个参数可修改的。我们把上面的加号 + 改成星号 *,上面的计算就从从 1 加到 5 变成了算 5 的阶乘了。 实际上,还需要把第三个参数改成1,否则结果是0.展开
作者回复: 嗯嗯,你说得很对。👍
- 你好梦梦2022-09-08 来自上海老师,对于一个function类型,如果把他作为函数型参,是用引用好,还是直接传值好
作者回复: function 对象不算小(你可以用 sizeof 检查一下)。因此,一般用引用传参。 另外参考第 41 讲。
- 当初莫相识2022-08-27 来自上海关于多线程那里,有个疑问。我在尝试将*this换为=,同样都是按值捕获,为什么结果却不同呢 return [=, count = get_count()]()。 我记得[]里=是按值捕获,&引用捕获,是我哪里没理解对吗,还是C++标准不同所致。
作者回复: [=] 捕获了 this(不建议,C++20 开始会报错),不是 *this。也就是说,只捕获了外围对象的指针,而不是复制了一份外围对象。
- 莫言2021-12-29请问下吴老师,this按引用捕获的时候,为什么get_count返回的值也是一样的呢?虽然引用不产生拷贝,但是get_count不是实实在在的执行了两次么
作者回复: get_count返回的值不一样。你看到输出的样子,是其他原因引起的。再仔细想想。😉
共 2 条评论 - 薛凯文2021-03-19Hello,老师。关于lambda adder。 auto adder = [](int n) { return [n](int x) { return x + n; }; }; 如果里面的lambda改成引用捕获,如下所示: auto adder = [](int n) { return [&n](int x) { return x + n; }; }; 结果就是不对的了. adder(3)(5)的结果是10(5 + 5). 请问是什么原因啊?展开
作者回复: 因为你产生了未定义行为。如果使用按引用捕获,你需要保证这个变量一直存在。 事实上,clang 对这种情况是可以直接报警的: warning: address of stack memory associated with parameter 'n' returned [-Wreturn-stack-address]
- 蔡冰成2021-02-22hi, 吴老师 auto dbl_lambda = [](const int& x) {return x * x;}; int dbl1 = dbl_lambda(3); 关于"只要能满足 constexpr 函数的条件,一个 lambda 表达式默认就是 constexpr 函数"这个结论, 我用上面对代码编译之后, 计算3*3依然是运行时调用, 使用的是https://gcc.godbolt.org/, 编译环境x86-64 gcc10.2 call 401172 <main::{lambda(int)#1}::operator()(int) const> mov DWORD PTR [rbp-0x4],eax 请问是有什么地方不正确吗? 如果使用正常对函数加constexpr声明则是编译期计算的展开
作者回复: 我猜是没开优化?我试下来没问题啊: https://godbolt.org/z/M73ccj
共 2 条评论 - zhengfan2020-06-30吴老师您好。 请问利用lamda表达式来替换bind,是否要使用std::thresholders来传递参数?
作者回复: std::placeholders?那只是用于 bind 的。lambda 里完全没看到有需要要用类似的东西。
共 3 条评论 - zhengfan2020-06-30吴老师您好,请教一下: 对于形如[]() mutable {}的lamda表达式 还能够被认为是一个constexpr吗?
作者回复: 有意思的问题。实测下来,C++ 不看形式,而是看实质。按你这个形式,能编译过。但如果这个 lambda 表达式真的会修改自己的存储的数值,则不可以是 constexpr。 见: https://godbolt.org/z/wc5axG 写 auto l = [x = 1]() { return x++; }; constexpr auto l = [x = 1]() mutable { return x++; }; 都不行。 auto l = [x = 1]() mutable { return x++; }; 可以。
共 2 条评论 - 晚风·和煦2020-02-22老师,一个空类,编译器没有生成默认的构造函数是吗?😂
作者回复: 「若不对类类型(struct、class 或 union)提供任何用户声明的构造函数,则编译器将始终声明一个作为其类的 inline public 成员的默认构造函数。 」 https://zh.cppreference.com/w/cpp/language/default_constructor
- 橙子8882020-01-04最近项目里使用到了libgo这个C++写的协程库,示例代码中用到了好多老师今天讲的知识点: void foo() { printf("function pointer\n"); } struct A { void fA() { printf("std::bind\n"); } void fB() { printf("std::function\n"); } }; int main() { go foo; go []{ printf("lambda\n"); }; go std::bind(&A::fA, A()); std::function<void()> fn(std::bind(&A::fB, A())); go fn; } 其中跟在"go"后面的内容总算能理解了,但是"go"的实现原理还是没搞懂,不知道后面协程这块的内容会不会有讲到。 另外对老师今天讲的 ”一般而言,按值捕获是比较安全的做法。按引用捕获时则需要更小心些,必须能够确保被捕获的变量和 lambda 表达式的生命期至少一样长“ 这句话深有体会,我在项目里按值捕获指针给协程用,结果调试的时候就是各种随机的崩溃。。。展开
作者回复: libgo 我没有任何使用经验,不过,看起来它和大部分库实现的协程一样,都是 stackful coroutine。我第 30 讲会讲的是会进入 C++20 的 stackless coroutine。 每个 libgo 的协程都有自己的独立栈空间,因此,协程唤起和休眠时都需要进行栈切换。无栈协程则跟唤起者使用同一个栈。有栈的协程实现另外还有 libco、Boost.Coroutine2 等。
共 2 条评论