Skip to content

Commit f115f62

Browse files
committed
upgrade some functional
1 parent 18a9e52 commit f115f62

File tree

3 files changed

+331
-2
lines changed

3 files changed

+331
-2
lines changed

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ else()
1010
add_compile_options(-Wall -Wextra -Werror=return-type)
1111
endif()
1212

13+
find_package(fmt REQUIRED)
14+
1315
file(GLOB sources CONFIGURE_DEPENDS "*.cpp")
1416
foreach(source IN LISTS sources)
1517
get_filename_component(name ${source} NAME_WE)
1618
add_executable(${name} ${source})
19+
target_link_libraries(${name} PRIVATE fmt)
1720
endforeach()

cppguidebook.typ

+320
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,323 @@ void compute()
557557
#tip[对于没有返回值(返回类型为 `void`)的函数,可以省略 `return`。]
558558
559559
#warn[对于有返回值的函数,必须写 return 语句,否则程序出错。]
560+
561+
= 函数式编程
562+
563+
== 为什么需要函数?
564+
565+
```cpp
566+
int main() {
567+
std::vector<int> a = {1, 2, 3, 4};
568+
int sum = 0;
569+
for (int i = 0; i < a.size(); i++) {
570+
sum += a[i];
571+
}
572+
fmt::println("sum = {}", sum);
573+
return 0;
574+
}
575+
```
576+
577+
这是一个计算数组求和的简单程序。
578+
579+
但是,他只能计算数组 a 的求和,无法复用。
580+
581+
如果我们有另一个数组 b 也需要求和的话,就得把整个求和的 for 循环重新写一遍:
582+
583+
```cpp
584+
int main() {
585+
std::vector<int> a = {1, 2, 3, 4};
586+
int sum = 0;
587+
for (int i = 0; i < a.size(); i++) {
588+
sum += a[i];
589+
}
590+
fmt::println("sum of a = {}", sum);
591+
std::vector<int> b = {5, 6, 7, 8};
592+
sum = 0;
593+
for (int i = 0; i < a.size(); i++) {
594+
sum += b[i];
595+
}
596+
fmt::println("sum of b = {}", sum);
597+
return 0;
598+
}
599+
```
600+
601+
这就出现了程序设计的大忌:代码重复。
602+
603+
#fun[例如,你有吹空调的需求,和充手机的需求。你为了满足这两个需求,购买了两台发电机,分别为空调和手机供电。第二天,你又产生了玩电脑需求,于是你又购买一台发电机,专为电脑供电……]
604+
605+
重复的代码不仅影响代码的*可读性*,也增加了*维护*代码的成本。
606+
607+
+ 看起来乱糟糟的,信息密度低,让人一眼看不出代码在干什么的功能
608+
+ 很容易写错,看走眼,难调试
609+
+ 复制粘贴过程中,容易漏改,比如这里的 `sum += b[i]` 可能写成 `sum += a[i]` 而自己不发现
610+
+ 改起来不方便,当我们的需求变更时,需要多处修改,比如当我需要改为计算乘积时,需要把两个地方都改成 `sum *=`
611+
+ 改了以后可能漏改一部分,留下 Bug 隐患
612+
+ 敏捷开发需要反复修改代码,比如你正在调试 `+=` 和 `-=` 的区别,看结果变化,如果一次切换需要改多处,就影响了调试速度
613+
614+
=== 狂想:没有函数的世界?
615+
616+
如果你还是喜欢“一本道”写法的话,不妨想想看,完全不用任何标准库和第三方库的函数和类,把 `fmt::println` 和 `std::vector` 这些函数全部拆解成一个个系统调用。那这整个程序会有多难写?
617+
618+
```cpp
619+
int main() {
620+
#ifdef _WIN32
621+
int *a = (int *)VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
622+
#else
623+
int *a = (int *)mmap(NULL, 4 * sizeof(int), PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
624+
#endif
625+
a[0] = 1;
626+
a[1] = 2;
627+
a[2] = 3;
628+
a[3] = 4;
629+
int sum = 0;
630+
for (int i = 0; i < 4; i++) {
631+
sum += a[i];
632+
}
633+
char buffer[64];
634+
buffer[0] = 's';
635+
buffer[1] = 'u';
636+
buffer[2] = 'm';
637+
buffer[3] = ' ';
638+
buffer[4] = '=';
639+
buffer[5] = ' '; // 例如,如果要修改此处的提示文本,甚至需要修改后面的 len 变量...
640+
int len = 6;
641+
int x = sum;
642+
do {
643+
buffer[len++] = '0' + x % 10;
644+
x /= 10;
645+
} while (x);
646+
buffer[len++] = '\n';
647+
#ifdef _WIN32
648+
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buffer, len, NULL, NULL);
649+
#else
650+
write(1, buffer, len);
651+
#endif
652+
int *b = (int *)a;
653+
b[0] = 4;
654+
b[1] = 5;
655+
b[2] = 6;
656+
b[3] = 7;
657+
int sum = 0;
658+
for (int i = 0; i < 4; i++) {
659+
sum += b[i];
660+
}
661+
len = 6;
662+
x = sum;
663+
do {
664+
buffer[len++] = '0' + x % 10;
665+
x /= 10;
666+
} while (x);
667+
buffer[len++] = '\n';
668+
#ifdef _WIN32
669+
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), buffer, len, NULL, NULL);
670+
#else
671+
write(1, buffer, len);
672+
#endif
673+
#ifdef _WIN32
674+
VirtualFree(a, 0, MEM_RELEASE);
675+
#else
676+
munmap(a);
677+
#endif
678+
return 0;
679+
}
680+
```
681+
682+
不仅完全没有可读性、可维护性,甚至都没有可移植性。
683+
684+
除非你只写应付导师的“一次性”程序,一旦要实现复杂的业务需求,不可避免的要自己封装函数或类。网上所有鼓吹“不封装”“设计模式是面子工程”的反智言论,都是没有做过大型项目的。
685+
686+
=== 设计模式追求的是“可改”而不是“可读”!
687+
688+
很多设计模式教材片面强调*可读性*,仿佛设计模式就是为了“优雅”“高大上”“美学”?使得很多人认为,“我这个是自己的项目,不用美化给领导看”而拒绝设计模式。实际上设计模式的主要价值在于*方便后续修改*!
689+
690+
#fun[例如 B 站以前只支持上传普通视频,现在叔叔突然提出:要支持互动视频,充电视频,视频合集,还废除了视频分 p,还要支持上传短视频,竖屏开关等……每一个叔叔的要求,都需要大量程序员修改代码,无论涉及前端还是后端。]
691+
692+
与建筑、绘画等领域不同,一次交付完毕就可以几乎永久使用。而软件开发是一个持续的过程,每次需求变更,都导致代码需要修改。开发人员几乎需要一直围绕着软件代码,不断的修改。调查表明,程序员 90% 的时间花在*改代码*上,*写代码*只占 10%。
693+
694+
#fun[软件就像生物,要不断进化,软件不更新不维护了等于死。而若你的代码逐渐变得臃肿难以修改,无法适应新需求,那你的代码就像已经失去进化能力的生物种群,如《三体》世界观中“安置”到澳大利亚保留区里“绝育”的人类,被淘汰只是时间问题。]
695+
696+
如果我们能在*写代码*阶段,就把程序准备得*易于后续修改*,那就可以在后续 90% 的*改代码*阶段省下无数时间。
697+
698+
如何让代码易于修改?前人总结出一系列常用的写法,这类写法有助于让后续修改更容易,各自适用于不同的场合,这就是设计模式。
699+
700+
提升可维护性最基础的一点,就是避免重复!
701+
702+
当你有很多地方出现重复的代码时,一旦需要涉及修改这部分逻辑时,就需要到每一个出现了这个逻辑的代码中,去逐一修改。
703+
704+
#fun[例如你的名字,在出生证,身份证,学生证,毕业证,房产证,驾驶证,各种地方都出现了。那么你要改名的话,所有这些证件都需要重新印刷!如果能把他们合并成一个“统一证”,那么只需要修改“统一证”上的名字就行了。]
705+
706+
不过,现实中并没有频繁改名字的需求,这说明:
707+
708+
- 对于不常修改的东西,可以容忍一定的重复。
709+
- 越是未来有可能修改的,就越需要设计模式降重!
710+
711+
例如数学常数 PI = 3.1415926535897,这辈子都不可能出现修改的需求,那写死也没关系。如果要把 PI 定义成宏,只是出于“记不住”“写起来太长了”“复制粘贴麻烦”。所以对于 PI 这种不会修改的东西,降重只是增加*可读性*,而不是*可修改性*。
712+
713+
#tip[但是,不要想当然!需求的千变万化总是超出你的想象。]
714+
715+
例如你做了一个“愤怒的小鸟”游戏,需要用到重力加速度 g = 9.8,你想当然认为 g 以后不可能修改。老板也信誓旦旦向你保证:“没事,重力加速度不会改变。”你就写死在代码里了。
716+
717+
没想到,“愤怒的小鸟”老板突然要求你加入“月球章”关卡,在这些关卡中,重力加速度是 g = 1.6。
718+
719+
如果你一开始就已经把 g 提取出来,定义为常量:
720+
721+
```cpp
722+
struct Level {
723+
const double g = 9.8;
724+
725+
void physics_sim() {
726+
bird.v = g * t; // 假装这里是物理仿真程序
727+
pig.v = g * t; // 假装这里是物理仿真程序
728+
}
729+
};
730+
```
731+
732+
那么支持月球关卡,只需要修改一处就可以了。
733+
734+
```cpp
735+
struct Level {
736+
double g;
737+
738+
Level(Chapter chapter) {
739+
if (chapter == ChapterMoon) {
740+
g = 1.6;
741+
} else {
742+
g = 9.8;
743+
}
744+
}
745+
746+
void physics_sim() {
747+
bird.v = g * t; // 无需任何修改,自动适应了新的非常数 g
748+
pig.v = g * t; // 无需任何修改,自动适应了新的非常数 g
749+
}
750+
};
751+
```
752+
753+
#fun[小彭老师之前做 zeno 时,询问要不要把渲染管线节点化,方便用户动态编程?张猩猩就是信誓旦旦道:“渲染是一个高度成熟领域,不会有多少修改需求的。”小彭老师遂写死了渲染管线,专为性能极度优化,几个月后,张猩猩羞答答找到小彭老师:“小彭老师,那个,渲染,能不能改成节点啊……”。这个故事告诉我们,甲方的信誓旦旦放的一个屁都不能信。]
754+
755+
=== 用函数封装
756+
757+
函数就是来帮你解决代码重复问题的!要领:
758+
759+
*把共同的部分提取出来,把不同的部分作为参数传入。*
760+
761+
```cpp
762+
void sum(std::vector<int> const &v) {
763+
int sum = 0;
764+
for (int i = 0; i < v.size(); i++) {
765+
sum += v[i];
766+
}
767+
fmt::println("sum of v = {}", sum);
768+
}
769+
770+
int main() {
771+
std::vector<int> a = {1, 2, 3, 4};
772+
sum(a);
773+
std::vector<int> b = {5, 6, 7, 8};
774+
sum(b);
775+
return 0;
776+
}
777+
```
778+
779+
这样 main 函数里就可以只关心要求和的数组,而不用关心求和具体是如何实现的了。事后我们可以随时把 sum 的内容偷偷换掉,换成并行的算法,main 也不用知道。这就是*封装*,可以把重复的公共部分抽取出来,方便以后修改代码。
780+
781+
#fun[sum 函数相当于,当需要吹空调时,插上空调插座。当需要给手机充电时,插上手机充电器。你不需要关心插座里的电哪里来,“国家电网”会替你想办法解决,想办法优化,想办法升级到绿色能源。你只需要吹着空调给你正在开发的手机 App 优化就行了,大大减轻程序员心智负担。]
782+
783+
=== 要封装,但不要耦合
784+
785+
但是!这段代码仍然有个问题,我们把 sum 求和的结果,直接在 sum 里打印了出来。sum 里写死了,求完和之后只能直接打印,调用者 main 根本无法控制。
786+
787+
这是一种错误的封装,或者说,封装过头了。
788+
789+
#fun[你把手机充电器 (fmt::println) 焊死在了插座 (sum) 上,现在这个插座只能给手机充电 (用于直接打印) 了,不能给笔记本电脑充电 (求和结果不直接用于打印) 了!尽管通过更换充电线 (参数 v),还可以支持支持安卓 (a) 和苹果 (b) 两种手机的充电,但这样焊死的插座已经和笔记本电脑无缘了。]
790+
791+
=== 每个函数应该职责单一,别一心多用
792+
793+
很明显,“打印”和“求和”是两个独立的操作,不应该焊死在一块。
794+
795+
sum 函数的本职工作是“数组求和”,不应该附赠打印功能。
796+
797+
sum 计算出求和结果后,直接 return 即可。
798+
799+
如何处理这个结果,是调用者 main 的事,正如“国家电网”不会管你用他提供的电来吹空调还是玩游戏一样,只要不妨碍到其他居民的正常用电。
800+
801+
```cpp
802+
int sum(std::vector<int> const &v) {
803+
int sum = 0;
804+
for (int i = 0; i < v.size(); i++) {
805+
sum += v[i];
806+
}
807+
return sum;
808+
}
809+
810+
int main() {
811+
std::vector<int> a = {1, 2, 3, 4};
812+
fmt::println("sum of a = {}", sum(a));
813+
std::vector<int> b = {5, 6, 7, 8};
814+
fmt::println("sum of b = {}", sum(b));
815+
return 0;
816+
}
817+
```
818+
819+
这就是设计模式所说的*职责单一原则*。
820+
821+
=== 二次封装
822+
823+
假设我们要计算一个数组的平均值,可以再定义个函数 average,他可以基于 sum 实现:
824+
825+
```cpp
826+
int sum(std::vector<int> const &v) {
827+
int sum = 0;
828+
for (int i = 0; i < v.size(); i++) {
829+
sum += v[i];
830+
}
831+
return sum;
832+
}
833+
834+
int average(std::vector<int> const &v) {
835+
return sum(v) / v.size();
836+
}
837+
838+
int main() {
839+
std::vector<int> a = {1, 2, 3, 4};
840+
fmt::println("average of a = {}", average(a));
841+
std::vector<int> b = {5, 6, 7, 8};
842+
fmt::println("average of b = {}", average(b));
843+
return 0;
844+
}
845+
```
846+
847+
== 为什么需要函数式?
848+
849+
== 函数对象
850+
851+
== bind
852+
853+
```cpp
854+
int hello(int x, int y) {
855+
fmt::println("hello({}, {})", x, y);
856+
return x + y;
857+
}
858+
859+
int main() {
860+
fmt::println("main 调用 hello(2, 3) 结果:{}", hello(2, 3));
861+
fmt::println("main 调用 hello(2, 4) 结果:{}", hello(2, 4));
862+
fmt::println("main 调用 hello(2, 5) 结果:{}", hello(2, 5));
863+
return 0;
864+
}
865+
```
866+
867+
```cpp
868+
int hello(int x, int y) {
869+
fmt::println("hello({}, {})", x, y);
870+
return x + y;
871+
}
872+
873+
int main() {
874+
fmt::println("main 调用 hello2(3) 结果:{}", hello2(3));
875+
fmt::println("main 调用 hello2(4) 结果:{}", hello2(4));
876+
fmt::println("main 调用 hello2(5) 结果:{}", hello2(5));
877+
return 0;
878+
}
879+
```

e1.cpp

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
#include <print>
1+
#include <fmt/format.h>
2+
3+
int hello(int x, int y) {
4+
fmt::println("hello({}, {})", x, y);
5+
return x + y;
6+
}
27

38
int main() {
4-
std::println("Hello, World!");
9+
fmt::println("main 调用 hello 结果:{}", hello(2, 3));
10+
return 0;
511
}

0 commit comments

Comments
 (0)