Skip to content

Commit c3794cf

Browse files
committed
add lambda sect bind
1 parent 92f0c5b commit c3794cf

File tree

3 files changed

+234
-8
lines changed

3 files changed

+234
-8
lines changed

docs/cpp_lifetime.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,24 @@ int main() {
177177

178178
TODO: 介绍
179179

180-
## 临时变量的生命周期
180+
## 临时变量的生命周期是一行
181181

182182
TODO
183+
184+
```cpp
185+
int main() {
186+
std::string const &s = std::string("hello");
187+
std::cout << s; // OK
188+
}
189+
```
190+
191+
```cpp
192+
std::string const &identity(std::string const &s) {
193+
return s;
194+
}
195+
196+
int main() {
197+
std::string const &s = identity(std::string("hello"));
198+
std::cout << s; // BOOM!
199+
}
200+
```

docs/cpp_tricks.md

+24
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,28 @@ if (auto it = table.find(key); it != table.end()) {
11691169

11701170
## bind 是历史糟粕,应该由 Lambda 表达式取代
11711171

1172+
众所周知, `std::bind` 可以为函数绑定一部分参数,形成一个新的函数(对象)。
1173+
1174+
```cpp
1175+
int func(int x, int y) {
1176+
printf("func(%d, %d)\n", x, y);
1177+
return x + y;
1178+
}
1179+
1180+
auto new_func = std::bind(func, 1, std::placeholders::_1);
1181+
1182+
new_func(2); // 调用 new_func(2) 时,实际上调用的是 func(1, 2)
1183+
}
1184+
```
1185+
1186+
输出:
1187+
1188+
```
1189+
func(1, 2)
1190+
```
1191+
1192+
当我们绑定出来的函数对象还需要接受参数时,就变得尤为复杂:需要使用占位符(placeholder)。
1193+
11721194
```cpp
11731195
int func(int x, int y, int z, int &w);
11741196
@@ -1288,6 +1310,8 @@ printf("%d\n", x); // 42
12881310
12891311
### 举个绑定随机数生成器例子
12901312
1313+
bind 写法:
1314+
12911315
```cpp
12921316
std::mt19937 gen(seed);
12931317
std::uniform_real_distribution<double> uni(0, 1);

docs/lambda.md

+191-7
Original file line numberDiff line numberDiff line change
@@ -956,20 +956,24 @@ int main() {
956956

957957
## bind 为函数对象绑定参数
958958

959+
原始函数:
960+
959961
```cpp
960962
int hello(int x, int y) {
961963
fmt::println("hello({}, {})", x, y);
962964
return x + y;
963965
}
964966

965967
int main() {
966-
fmt::println("main 调用 hello(2, 3) 结果:{}", hello(2, 3));
967-
fmt::println("main 调用 hello(2, 4) 结果:{}", hello(2, 4));
968-
fmt::println("main 调用 hello(2, 5) 结果:{}", hello(2, 5));
968+
hello(2, 3);
969+
hello(2, 4);
970+
hello(2, 5);
969971
return 0;
970972
}
971973
```
972974
975+
绑定部分参数:
976+
973977
```cpp
974978
int hello(int x, int y) {
975979
fmt::println("hello({}, {})", x, y);
@@ -978,15 +982,195 @@ int hello(int x, int y) {
978982
979983
int main() {
980984
auto hello2 = std::bind(hello, 2, std::placeholders::_1);
981-
fmt::println("main 调用 hello2(3) 结果:{}", hello2(3));
982-
fmt::println("main 调用 hello2(4) 结果:{}", hello2(4));
983-
fmt::println("main 调用 hello2(5) 结果:{}", hello2(5));
985+
hello2(3); // hello(2, 3)
986+
hello2(4); // hello(2, 4)
987+
hello2(5); // hello(2, 5)
988+
return 0;
989+
}
990+
```
991+
992+
> {{ icon.tip }} `std::placeholders::_1` 表示 `hello2` 的第一参数。
993+
994+
> {{ icon.tip }} std::placeholders::_1 在 bind 表达式中位于 hello 的的第二参数位置,这意味着:把 hello2 的第一参数,传递到 hello 的第二参数上去。
995+
996+
绑定全部参数:
997+
998+
```cpp
999+
int hello(int x, int y) {
1000+
fmt::println("hello({}, {})", x, y);
1001+
return x + y;
1002+
}
1003+
1004+
int main() {
1005+
auto hello23 = std::bind(hello, 2, 3);
1006+
hello23(); // hello(2, 3)
1007+
return 0;
1008+
}
1009+
```
1010+
1011+
绑定引用参数:
1012+
1013+
```cpp
1014+
int inc(int &x) {
1015+
x += 1;
1016+
}
1017+
1018+
int main() {
1019+
int x = 0;
1020+
auto incx = std::bind(inc, std::ref(x));
1021+
incx();
1022+
fmt::println("x = {}", x); // x = 1
1023+
incx();
1024+
fmt::println("x = {}", x); // x = 2
9841025
return 0;
9851026
}
9861027
```
9871028

988-
### `std::placeholders`
1029+
> {{ icon.warn }} 如果不使用 `std::ref`,那么 `main` 里的局部变量 `x` 不会改变!因为 `std::bind` 有一个恼人的设计:默认按拷贝捕获,会把参数拷贝一份,而不是保留引用。
9891030
9901031
### bind 是一个失败的设计
9911032

1033+
当我们绑定出来的函数对象还需要接受参数时,就变得尤为复杂:需要使用占位符(placeholder)。
1034+
1035+
```cpp
1036+
int func(int x, int y, int z, int &w);
1037+
1038+
int w = rand();
1039+
1040+
auto bound = std::bind(func, std::placeholders::_2, 1, std::placeholders::_1, std::ref(w)); //
1041+
1042+
int res = bound(5, 6); // 等价于 func(6, 1, 5, w);
1043+
```
1044+
1045+
这是一个绑定器,把 `func` 的第二个参数和第四个参数固定下来,形成一个新的函数对象,然后只需要传入前面两个参数就可以调用原来的函数了。
1046+
1047+
这是一个非常旧的技术,C++98 时代就有了。但是,现在有了 Lambda 表达式,可以更简洁地实现:
1048+
1049+
```cpp
1050+
int func(int x, int y, int z, int &w);
1051+
1052+
int w = rand();
1053+
1054+
auto lambda = [&w](int x, int y) { return func(y, 1, x, w); };
1055+
1056+
int res = lambda(5, 6);
1057+
```
1058+
1059+
Lambda 表达式有许多优势:
1060+
1061+
- 简洁:不需要写一大堆看不懂的 `std::placeholders::_1`,直接写变量名就可以了。
1062+
- 灵活:可以在 Lambda 中使用任意多的变量,调整顺序,而不仅仅是 `std::placeholders::_1`
1063+
- 易懂:写起来和普通函数调用一样,所有人都容易看懂。
1064+
- 捕获引用:`std::bind` 不支持捕获引用,总是拷贝参数,必须配合 `std::ref` 才能捕获到引用。而 Lambda 可以随意捕获不同类型的变量,按值(`[x]`)或按引用(`[&x]`),还可以移动捕获(`[x = move(x)]`),甚至捕获 this(`[this]`)。
1065+
- 夹带私货:可以在 lambda 体内很方便地夹带其他额外转换操作,比如:
1066+
1067+
```cpp
1068+
auto lambda = [&w](int x, int y) { return func(y + 8, 1, x * x, ++w) * 2; };
1069+
```
1070+
1071+
#### bind 的历史
1072+
1073+
为什么 C++11 有了 Lambda 表达式,还要提出 `std::bind` 呢?
1074+
1075+
虽然 bind 和 lambda 看似都是在 C++11 引入的,实际上 bind 的提出远远早于 lambda。
1076+
1077+
> {{ icon.fun }} 标准委员会:我们不生产库,我们只是 boost 的搬运工。
1078+
1079+
当时还是 C++98,由于没有 lambda,难以创建函数对象,“捕获参数”非常困难。
1080+
1081+
为了解决“捕获难”问题,在第三方库 boost 中提出了 `boost::bind`,由于当时只有 C++98,很多有益于函数式编程的特性都没有,所以实现的非常丑陋。
1082+
1083+
例如,因为 C++98 没有变长模板参数,无法实现 `<class ...Args>`。所以实际上当时 boost 所有支持多参数的函数,实际上都是通过:
1084+
1085+
```cpp
1086+
void some_func();
1087+
void some_func(int i1);
1088+
void some_func(int i1, int i2);
1089+
void some_func(int i1, int i2, int i3);
1090+
void some_func(int i1, int i2, int i3, int i4);
1091+
// ...
1092+
```
1093+
1094+
这样暴力重载几十个函数来实现的,而且参数数量有上限。通常会实现 0 到 20 个参数的重载,更多就不支持了。
1095+
1096+
例如,我们知道现在 bind 需要配合各种 `std::placeholders::_1` 使用,有没有想过这套丑陋的占位符是为什么?为什么不用 `std::placeholder<1>`,这样不是更可扩展吗?
1097+
1098+
没错,当时 `boost::bind` 就是用暴力重载几十个参数数量不等的函数,排列组合,嗯是排出来的,所以我们会看到 `boost::placeholders` 只有有限个数的占位符数量。
1099+
1100+
糟糕的是,标准库的 `std::bind``boost::bind` 原封不动搬了过来,甚至 `placeholders` 的暴力组合也没有变,造成了 `std::bind` 如今丑陋的接口。
1101+
1102+
人家 `boost::bind` 是因为不能修改语言语法,才只能那样憋屈的啊?可现在你码是标准委员会啊,你可以修改语言语法啊?
1103+
1104+
然而,C++ 标准的更新是以“提案”的方式,逐步“增量”更新进入语言标准的。即使是在 C++98 到 C++11 这段时间内,内部也是有一个很长的消化流程的,也就是说有很多子版本,只是对外看起来好像只有一个 C++11。
1105+
1106+
比方说,我 2001 年提出 `std::bind` 提案,2005 年被批准进入未来将要发布的 C++11 标准。然后又一个人在 2006 年提出其实不需要 bind,完全可以用更好的 lambda 语法来代替 bind,然后等到了 2008 年才批准进入即将发布的 C++11 标准。但是已经进入标准的东西就不会再退出了,哪怕还没有发布。就这样 bind 和 lambda 同时进入了标准。
1107+
1108+
所以闹了半天,lambda 实际上是 bind 的上位替代,有了 lambda 根本不需要 bind 的。只不过是由于 C++ 委员会前后扯皮的“制度优势”,导致 bind 和他的上位替代 lambda 同时进入了 C++11 标准一起发布。
1109+
1110+
> {{ icon.fun }} 这下看懂了。
1111+
1112+
很多同学就不理解,小彭老师说“lambda 是 bind 的上位替代”,他就质疑“可他们不都是 C++11 提出的吗?”
1113+
1114+
有没有一种可能,C++11 和 C++98 之间为什么年代差了那么久远,就是因为一个标准一拖再拖,内部实际上已经迭代了好几个小版本了,才发布出来。
1115+
1116+
> {{ icon.story }} 再举个例子,CTAD 和 `optional` 都是 C++17 引入的,为什么还要 `make_optional` 这个帮手函数?不是说 CTAD 是 `make_xxx` 的上位替代吗?可见,C++ 标准中这种“同一个版本内”自己打自己耳光的现象比比皆是。
1117+
1118+
> {{ icon.fun }} 所以,现在还坚持用 bind 的,都是些 2005 年前后在象牙塔接受 C++ 教育,但又不肯“终身学习”的劳保。这批劳保又去“上岸”当“教师”,继续复制 2005 年的错误毒害青少年,实现了劳保的再生产。
1119+
1120+
#### thread 膝盖中箭
1121+
1122+
糟糕的是,bind 的这种荼毒,甚至影响到了线程库:`std::thread` 的构造函数就是基于 `std::bind` 的!
1123+
1124+
这导致了 `std::thread``std::bind` 一样,无法捕获引用。
1125+
1126+
```cpp
1127+
void thread_func(int &x) {
1128+
x = 42;
1129+
}
1130+
1131+
int x = 0;
1132+
std::thread t(thread_func, x);
1133+
t.join();
1134+
printf("%d\n", x); // 0
1135+
```
1136+
1137+
为了避免踩到 bind 的坑,我建议所有同学,构造 `std::thread` 时,统一只指定“单个参数”,也就是函数本身。如果需要捕获参数,请使用 lambda。因为 lambda 中,捕获了哪些变量,参数的顺序是什么,哪些捕获是引用,哪些捕获是拷贝,非常清晰。
1138+
1139+
```cpp
1140+
void thread_func(int &x) {
1141+
x = 42;
1142+
}
1143+
1144+
int x = 0;
1145+
std::thread t([&x] { // [&x] 表示按引用捕获 x;如果写作 [x],那就是拷贝捕获
1146+
thread_func(x);
1147+
});
1148+
t.join();
1149+
printf("%d\n", x); // 42
1150+
```
1151+
1152+
#### 举个绑定随机数生成器例子
1153+
1154+
bind 写法:
1155+
1156+
```cpp
1157+
std::mt19937 gen(seed);
1158+
std::uniform_real_distribution<double> uni(0, 1);
1159+
auto frand = std::bind(uni, std::ref(gen));
1160+
double x = frand();
1161+
double y = frand();
1162+
```
1163+
1164+
改用 lambda:
1165+
1166+
```cpp
1167+
std::mt19937 gen(seed);
1168+
std::uniform_real_distribution<double> uni(0, 1);
1169+
auto frand = [uni, &gen] {
1170+
return uni(gen);
1171+
};
1172+
double x = frand();
1173+
double y = frand();
1174+
```
1175+
9921176
### `std::bind_front``std::bind_back`

0 commit comments

Comments
 (0)