@@ -283,8 +283,12 @@ <h1 id="_1">函数式编程</h1>
283283</ ul >
284284</ li >
285285< li > < a href ="#bind "> bind 为函数对象绑定参数</ a > < ul >
286- < li > < a href ="#stdplaceholders "> std::placeholders</ a > </ li >
287- < li > < a href ="#bind_1 "> bind 是一个失败的设计</ a > </ li >
286+ < li > < a href ="#bind_1 "> bind 是一个失败的设计</ a > < ul >
287+ < li > < a href ="#bind_2 "> bind 的历史</ a > </ li >
288+ < li > < a href ="#thread "> thread 膝盖中箭</ a > </ li >
289+ < li > < a href ="#_21 "> 举个绑定随机数生成器例子</ a > </ li >
290+ </ ul >
291+ </ li >
288292< li > < a href ="#stdbind_front-stdbind_back "> std::bind_front 和 std::bind_back</ a > </ li >
289293</ ul >
290294</ li >
@@ -1095,33 +1099,171 @@ <h4 id="_20">闭包捕获变量的生命周期问题</h4>
10951099< h4 id ="operator "> < code > operator()</ code > 很有迷惑性</ h4 >
10961100< h3 id ="c "> 函数指针是 C 语言陋习,改掉</ h3 >
10971101< h2 id ="bind "> bind 为函数对象绑定参数</ h2 >
1102+ < p > 原始函数:</ p >
10981103< pre > < code class ="language-cpp "> int hello(int x, int y) {
10991104 fmt::println("hello({}, {})", x, y);
11001105 return x + y;
11011106}
11021107
11031108int main() {
1104- fmt::println("main 调用 hello(2, 3) 结果:{}", hello(2, 3) );
1105- fmt::println("main 调用 hello(2, 4) 结果:{}", hello(2, 4) );
1106- fmt::println("main 调用 hello(2, 5) 结果:{}", hello(2, 5) );
1109+ hello(2, 3);
1110+ hello(2, 4);
1111+ hello(2, 5);
11071112 return 0;
11081113}
11091114</ code > </ pre >
1115+ < p > 绑定部分参数:</ p >
11101116< pre > < code class ="language-cpp "> int hello(int x, int y) {
11111117 fmt::println("hello({}, {})", x, y);
11121118 return x + y;
11131119}
11141120
11151121int main() {
11161122 auto hello2 = std::bind(hello, 2, std::placeholders::_1);
1117- fmt::println("main 调用 hello2(3) 结果:{}", hello2(3));
1118- fmt::println("main 调用 hello2(4) 结果:{}", hello2(4));
1119- fmt::println("main 调用 hello2(5) 结果:{}", hello2(5));
1123+ hello2(3); // hello(2, 3)
1124+ hello2(4); // hello(2, 4)
1125+ hello2(5); // hello(2, 5)
1126+ return 0;
1127+ }
1128+ </ code > </ pre >
1129+ < blockquote >
1130+ < p > < img src ="../img/bulb.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> < code > std::placeholders::_1</ code > 表示 < code > hello2</ code > 的第一参数。</ p >
1131+ < p > < img src ="../img/bulb.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> std::placeholders::_1 在 bind 表达式中位于 hello 的的第二参数位置,这意味着:把 hello2 的第一参数,传递到 hello 的第二参数上去。</ p >
1132+ </ blockquote >
1133+ < p > 绑定全部参数:</ p >
1134+ < pre > < code class ="language-cpp "> int hello(int x, int y) {
1135+ fmt::println("hello({}, {})", x, y);
1136+ return x + y;
1137+ }
1138+
1139+ int main() {
1140+ auto hello23 = std::bind(hello, 2, 3);
1141+ hello23(); // hello(2, 3)
1142+ return 0;
1143+ }
1144+ </ code > </ pre >
1145+ < p > 绑定引用参数:</ p >
1146+ < pre > < code class ="language-cpp "> int inc(int &x) {
1147+ x += 1;
1148+ }
1149+
1150+ int main() {
1151+ int x = 0;
1152+ auto incx = std::bind(inc, std::ref(x));
1153+ incx();
1154+ fmt::println("x = {}", x); // x = 1
1155+ incx();
1156+ fmt::println("x = {}", x); // x = 2
11201157 return 0;
11211158}
11221159</ code > </ pre >
1123- < h3 id ="stdplaceholders "> < code > std::placeholders</ code > </ h3 >
1160+ < blockquote >
1161+ < p > < img src ="../img/warning.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 如果不使用 < code > std::ref</ code > ,那么 < code > main</ code > 里的局部变量 < code > x</ code > 不会改变!因为 < code > std::bind</ code > 有一个恼人的设计:默认按拷贝捕获,会把参数拷贝一份,而不是保留引用。</ p >
1162+ </ blockquote >
11241163< h3 id ="bind_1 "> bind 是一个失败的设计</ h3 >
1164+ < p > 当我们绑定出来的函数对象还需要接受参数时,就变得尤为复杂:需要使用占位符(placeholder)。</ p >
1165+ < pre > < code class ="language-cpp "> int func(int x, int y, int z, int &w);
1166+
1167+ int w = rand();
1168+
1169+ auto bound = std::bind(func, std::placeholders::_2, 1, std::placeholders::_1, std::ref(w)); //
1170+
1171+ int res = bound(5, 6); // 等价于 func(6, 1, 5, w);
1172+ </ code > </ pre >
1173+ < p > 这是一个绑定器,把 < code > func</ code > 的第二个参数和第四个参数固定下来,形成一个新的函数对象,然后只需要传入前面两个参数就可以调用原来的函数了。</ p >
1174+ < p > 这是一个非常旧的技术,C++98 时代就有了。但是,现在有了 Lambda 表达式,可以更简洁地实现:</ p >
1175+ < pre > < code class ="language-cpp "> int func(int x, int y, int z, int &w);
1176+
1177+ int w = rand();
1178+
1179+ auto lambda = [&w](int x, int y) { return func(y, 1, x, w); };
1180+
1181+ int res = lambda(5, 6);
1182+ </ code > </ pre >
1183+ < p > Lambda 表达式有许多优势:</ p >
1184+ < ul >
1185+ < li > 简洁:不需要写一大堆看不懂的 < code > std::placeholders::_1</ code > ,直接写变量名就可以了。</ li >
1186+ < li > 灵活:可以在 Lambda 中使用任意多的变量,调整顺序,而不仅仅是 < code > std::placeholders::_1</ code > 。</ li >
1187+ < li > 易懂:写起来和普通函数调用一样,所有人都容易看懂。</ li >
1188+ < li > 捕获引用:< code > std::bind</ code > 不支持捕获引用,总是拷贝参数,必须配合 < code > std::ref</ code > 才能捕获到引用。而 Lambda 可以随意捕获不同类型的变量,按值(< code > [x]</ code > )或按引用(< code > [&x]</ code > ),还可以移动捕获(< code > [x = move(x)]</ code > ),甚至捕获 this(< code > [this]</ code > )。</ li >
1189+ < li > 夹带私货:可以在 lambda 体内很方便地夹带其他额外转换操作,比如:</ li >
1190+ </ ul >
1191+ < pre > < code class ="language-cpp "> auto lambda = [&w](int x, int y) { return func(y + 8, 1, x * x, ++w) * 2; };
1192+ </ code > </ pre >
1193+ < h4 id ="bind_2 "> bind 的历史</ h4 >
1194+ < p > 为什么 C++11 有了 Lambda 表达式,还要提出 < code > std::bind</ code > 呢?</ p >
1195+ < p > 虽然 bind 和 lambda 看似都是在 C++11 引入的,实际上 bind 的提出远远早于 lambda。</ p >
1196+ < blockquote >
1197+ < p > < img src ="../img/awesomeface.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 标准委员会:我们不生产库,我们只是 boost 的搬运工。</ p >
1198+ </ blockquote >
1199+ < p > 当时还是 C++98,由于没有 lambda,难以创建函数对象,“捕获参数”非常困难。</ p >
1200+ < p > 为了解决“捕获难”问题,在第三方库 boost 中提出了 < code > boost::bind</ code > ,由于当时只有 C++98,很多有益于函数式编程的特性都没有,所以实现的非常丑陋。</ p >
1201+ < p > 例如,因为 C++98 没有变长模板参数,无法实现 < code > <class ...Args></ code > 。所以实际上当时 boost 所有支持多参数的函数,实际上都是通过:</ p >
1202+ < pre > < code class ="language-cpp "> void some_func();
1203+ void some_func(int i1);
1204+ void some_func(int i1, int i2);
1205+ void some_func(int i1, int i2, int i3);
1206+ void some_func(int i1, int i2, int i3, int i4);
1207+ // ...
1208+ </ code > </ pre >
1209+ < p > 这样暴力重载几十个函数来实现的,而且参数数量有上限。通常会实现 0 到 20 个参数的重载,更多就不支持了。</ p >
1210+ < p > 例如,我们知道现在 bind 需要配合各种 < code > std::placeholders::_1</ code > 使用,有没有想过这套丑陋的占位符是为什么?为什么不用 < code > std::placeholder<1></ code > ,这样不是更可扩展吗?</ p >
1211+ < p > 没错,当时 < code > boost::bind</ code > 就是用暴力重载几十个参数数量不等的函数,排列组合,嗯是排出来的,所以我们会看到 < code > boost::placeholders</ code > 只有有限个数的占位符数量。</ p >
1212+ < p > 糟糕的是,标准库的 < code > std::bind</ code > 把 < code > boost::bind</ code > 原封不动搬了过来,甚至 < code > placeholders</ code > 的暴力组合也没有变,造成了 < code > std::bind</ code > 如今丑陋的接口。</ p >
1213+ < p > 人家 < code > boost::bind</ code > 是因为不能修改语言语法,才只能那样憋屈的啊?可现在你码是标准委员会啊,你可以修改语言语法啊?</ p >
1214+ < p > 然而,C++ 标准的更新是以“提案”的方式,逐步“增量”更新进入语言标准的。即使是在 C++98 到 C++11 这段时间内,内部也是有一个很长的消化流程的,也就是说有很多子版本,只是对外看起来好像只有一个 C++11。</ p >
1215+ < p > 比方说,我 2001 年提出 < code > std::bind</ code > 提案,2005 年被批准进入未来将要发布的 C++11 标准。然后又一个人在 2006 年提出其实不需要 bind,完全可以用更好的 lambda 语法来代替 bind,然后等到了 2008 年才批准进入即将发布的 C++11 标准。但是已经进入标准的东西就不会再退出了,哪怕还没有发布。就这样 bind 和 lambda 同时进入了标准。</ p >
1216+ < p > 所以闹了半天,lambda 实际上是 bind 的上位替代,有了 lambda 根本不需要 bind 的。只不过是由于 C++ 委员会前后扯皮的“制度优势”,导致 bind 和他的上位替代 lambda 同时进入了 C++11 标准一起发布。</ p >
1217+ < blockquote >
1218+ < p > < img src ="../img/awesomeface.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 这下看懂了。</ p >
1219+ </ blockquote >
1220+ < p > 很多同学就不理解,小彭老师说“lambda 是 bind 的上位替代”,他就质疑“可他们不都是 C++11 提出的吗?”</ p >
1221+ < p > 有没有一种可能,C++11 和 C++98 之间为什么年代差了那么久远,就是因为一个标准一拖再拖,内部实际上已经迭代了好几个小版本了,才发布出来。</ p >
1222+ < blockquote >
1223+ < p > < img src ="../img/book.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 再举个例子,CTAD 和 < code > optional</ code > 都是 C++17 引入的,为什么还要 < code > make_optional</ code > 这个帮手函数?不是说 CTAD 是 < code > make_xxx</ code > 的上位替代吗?可见,C++ 标准中这种“同一个版本内”自己打自己耳光的现象比比皆是。</ p >
1224+ < p > < img src ="../img/awesomeface.png " height ="30px " width ="auto " style ="margin: 0; border: none "/> 所以,现在还坚持用 bind 的,都是些 2005 年前后在象牙塔接受 C++ 教育,但又不肯“终身学习”的劳保。这批劳保又去“上岸”当“教师”,继续复制 2005 年的错误毒害青少年,实现了劳保的再生产。</ p >
1225+ </ blockquote >
1226+ < h4 id ="thread "> thread 膝盖中箭</ h4 >
1227+ < p > 糟糕的是,bind 的这种荼毒,甚至影响到了线程库:< code > std::thread</ code > 的构造函数就是基于 < code > std::bind</ code > 的!</ p >
1228+ < p > 这导致了 < code > std::thread</ code > 和 < code > std::bind</ code > 一样,无法捕获引用。</ p >
1229+ < pre > < code class ="language-cpp "> void thread_func(int &x) {
1230+ x = 42;
1231+ }
1232+
1233+ int x = 0;
1234+ std::thread t(thread_func, x);
1235+ t.join();
1236+ printf("%d\n", x); // 0
1237+ </ code > </ pre >
1238+ < p > 为了避免踩到 bind 的坑,我建议所有同学,构造 < code > std::thread</ code > 时,统一只指定“单个参数”,也就是函数本身。如果需要捕获参数,请使用 lambda。因为 lambda 中,捕获了哪些变量,参数的顺序是什么,哪些捕获是引用,哪些捕获是拷贝,非常清晰。</ p >
1239+ < pre > < code class ="language-cpp "> void thread_func(int &x) {
1240+ x = 42;
1241+ }
1242+
1243+ int x = 0;
1244+ std::thread t([&x] { // [&x] 表示按引用捕获 x;如果写作 [x],那就是拷贝捕获
1245+ thread_func(x);
1246+ });
1247+ t.join();
1248+ printf("%d\n", x); // 42
1249+ </ code > </ pre >
1250+ < h4 id ="_21 "> 举个绑定随机数生成器例子</ h4 >
1251+ < p > bind 写法:</ p >
1252+ < pre > < code class ="language-cpp "> std::mt19937 gen(seed);
1253+ std::uniform_real_distribution<double> uni(0, 1);
1254+ auto frand = std::bind(uni, std::ref(gen));
1255+ double x = frand();
1256+ double y = frand();
1257+ </ code > </ pre >
1258+ < p > 改用 lambda:</ p >
1259+ < pre > < code class ="language-cpp "> std::mt19937 gen(seed);
1260+ std::uniform_real_distribution<double> uni(0, 1);
1261+ auto frand = [uni, &gen] {
1262+ return uni(gen);
1263+ };
1264+ double x = frand();
1265+ double y = frand();
1266+ </ code > </ pre >
11251267< h3 id ="stdbind_front-stdbind_back "> < code > std::bind_front</ code > 和 < code > std::bind_back</ code > </ h3 > </ div >
11261268 </ div >
11271269 </ div >
0 commit comments