【老万】后C++演义第三回:蛮生长亟待拨乱返正,勤整治方得服人归心
后C++演义:
第一回 比雅尼喜降生奥胡斯港,老修士神预言救世秃头
第二回 离英伦情切切欲成霸业,入贝尔喜加加初出江湖
第三回 蛮生长亟待拨乱返正 勤整治方得服人归心
上回书咱说到天将降大任于奥胡斯人比雅尼⋅斯特劳斯特鲁普也,必先令其师从面向对象编程之一代宗师克利斯登·奈加特修得硕士学位,又使之远渡英伦潜心修学,获剑桥大学之电脑博士。那比雅尼君年少得志,离了象牙塔,只身前往美利坚合众国之贝尔实验室,埋头苦干,脚踏巨人德尼斯·里奇之肩,身登青云之梯,独力发明 C++ 语言,为 C 程序引入对象,曰:C++,C 之超集也。一时之间风头无双,顿成世间最网红之编程语言。
古人云:萝卜快了不洗泥,柿子软了不剥皮。果不其然,C++ 御风而行之际,正是各 C++ 编译器鱼龙混杂,乱花渐欲迷人眼之时,令那比雅尼好不烦恼。
你道为何?盖创世之初,天崩地裂,开辟鸿蒙,世间一片混沌,饺子尚未有型。那各家之 C++ 编译器,竞相展示奇技淫巧,以招徕客户为能事,与今日之主播抢粉丝有得一比。为能脱颖而出,各村有各村之高招,竟对那 C++ 施以各种微创手术,或隆胸,或削骨,虽贯 C++ 之名,却藏私货之实。
或曰:百家争鸣,百花齐放,不亦乐乎?非也,非也,凡事皆有度,过犹不及。观那各家之变法,多有不合比雅尼理念者,亦有自相矛盾之处。虽众人拾柴火焰高,火烧眉毛亦非幸事。
那 C++ 编译器比不得《哈姆雷特》,断无千人千面之理。若语言不能统一,则一部《红楼梦》小说,需曹公写出重庆话、成都话、河南话、山东话、广东话、客家话版之满纸荒唐言,方可放之四海,如此不出五回曹公必喷血玉殒矣。是以标准化之重要性,不容小觑。
且各家编译器自行演化,难保向后兼容。用户每欲升级编译器,便有不兼容之虞,常需耗数月之功,方能使软件重新编译。即便编译成功,亦难免因编译器行为之变化而引入新 bug,需程序员加班赶工捉虫。升级之痛,如同流感。每年来一回,年年皆不同。是以好事者戏言光头导致电费多,真个气杀比雅尼也。
天下大势合久必分分久必合。比雅尼慧眼识出症结所在,曰:吾当如秦始皇,一统天下之 C++编译器,惠及黎民。旋联手国际标准化组织 ISO,启动 C++ 之标准化进程。依比君规划,C++ 首个标准化之版本将于西元 1998 面世。来吧,来吧,相约 98。
98 版 C++ 之目的,非为创新,乃是对其旁逸斜出之枝痛下杀手,悉数剪去。所谓慈不掌兵,不破不立。下得狠心,日后方能枝繁叶茂。
时间仓促,错误在所难免。C++98 未获比雅尼青睐,不足之处如鲠在喉。比君遂马不停蹄启动勘误。历五年,完成 C++03,与 C++98 并无大异,可谓 C++98 修订版也。
至此统一霸业达成,各编译器无不唯 C++03 标准马首是瞻,四海归心,众皆贺之。比雅尼曰:此万里长征之第一步耳。吐槽 C++ 之论,仍时有所闻,令吾不得安睡。吾当悬梁刺股,放大招于 C++0X,以德服人。
X 者,未知数也。盖标准委员会之官僚拖沓,概莫能外。聪明如比雅尼,亦无力操控,故以 X 名之。或五六,或七八,天知地知。
然造化弄人,至 2010 年,C++0X 仍未完工。比雅尼仰天太息:X 这么大,我想去数数,奈何母上仅赐余十个指头。越明年,新版始出,更名为 C++11,距前一次重大版本 C++98 已 13 岁矣。
宝剑锋从磨砺出,卧梅闻花达春绿。比雅尼十年磨一剑,一出天下惊。C++11 精妙之处,不一而足。比君亦颇自得,引为傲事,逢人便夸:C++11 比之 C++98,何止锦上添花,实已脱胎换骨,俨然全新之一语言也,相当 very good,当浮一大白。
列位看官,那比雅尼秃头人不打诳语,头顶无毛,办事必牢。吾当历数 C++11 革新之处,以证比氏诚不我欺也。
C++11 新添之功能,不止百余处,然令人击节赞叹之变革,吾以为有二:一曰函数式编程,使程序员工作效率倍增;二曰搬家语义,使程序执行效率并正确性倍增。
何为函数式编程(functional programming)?
函数(function)者,“含”数也。函数非数,乃变数间之关系。此一名词,由清朝之数学大家李善兰君译出。其《代数学》书中释曰:“凡此变数中函(包含)彼变数者,则此为彼之函数”。换言之,若给定彼一组变数,按某规则可算出此一组变数,则可称此特定规则为一函数。
程序员周知,函数乃代码组织之基本单位。以此 C++ 代码为例:
double square(double x) {
return x*x;
}
定义一函数曰 square,可求平方也。
甫经定义,此函数便可用于定义其它函数。用者只需知其然,不必知其所以然,是谓“抽象”。如此例:
double cube(double x) {
return square(x)*x;
}
此间,cube 乃一新定义之函数,可求立方,其定义之中引用 square 函数,只需直呼其名,不必知晓其定义也。
是以,程序员可将世间任何繁复逻辑庖丁解牛,分为函数,大函数又可分为小函数,直至简无可简,不必再分。此乃抽象之道,程序设计之第一要义也。世间一切编程技巧,皆为抽象,切记,切记。
某曰:吾欲将数组中各数皆换为彼之平方,何解?
答:循环可解。
// {1, 2, 3, …} ⇒ {1, 4, 9, …}
void square_all(
std::vector<double>& values) {
for (double& v : values) {
v = square(v);
}
}
某又曰:吾欲将数组中各数皆换为彼之立方,又何解?
答:可依前次之计策类推。
// {1, 2, 3, …} ⇒ {1, 8, 27, …}
void cube_all(
std::vector<double>& values) {
for (double& v : values) {
v = cube(v);
}
}
噫,此解虽正,过于唠叨也。吾生也有涯,对数组元素所做之操作也无涯,若每一操作需新写一循环,恰似以有涯随无涯,殆已!
欲破此局,必当以函数为参数。如此:
void transform_all(
std::function<double(double)> transform,
std::vector<double>& values) {
for (double& v : values) {
v = transform(v);
}
}
此间之 std::function<double(double)> 乃 C++11 新引入之类型,意为“参数为 double 类型,返回值亦为 double 类型之函数”。依次类推,std::function<std::string(bool, int)> 为“参数为 bool 与 int 类型,返回值为 std::string 类型之函数“。
既有 transform_all 将循环抽象,便不必每次手写循环,代码可简化为:
transform_all(square, values);
transform_all(cube, values);
…
此 transform_all 非同寻常:其参数 transform 本身亦为函数也。凡参数抑或返回值为函数之函数,谓之“高阶函数(higher-order function)”。使用高阶函数之编程范式,即函数式程序设计。
较之普通编程,函数式编程更为抽象,亦更为简洁,其代码常如秋水文章不染尘,鲜能藏污纳垢。虽初练者难得其门而入,一经掌握融会贯通,便有打通大小周天之通畅,威力无穷。
在 C++03 之中函数式编程,非不能也,处处掣肘举步维艰耳。C++11 引入 std::function 与 lambda,方使函数式编程扬眉吐气,一跃而成一等公民。
那 std::function 我等已见过,lambda 又是何物?匿名之函数也。它有何用?试举一例:设吾等欲将数组中各元素增长 N 倍,而编程之时不知此 N 之值,是以需将其设为函数之一参数也:
void times(
std::vector<double>& values,
double multiplier) {
transform_all(???, values);
}
试问:以上代码中之???部分当为何?
若无 lambda,需定义一函子(functor)方可解:
struct multiply {
double operator()(double x) {
return x*multiplier;
}
double multiplier;
};
void times(
std::vector<double>& values,
double multiplier) {
transform_all(
multiply{multiplier}, values);
}
若以 C++11 之 lambda 重写,则代码可简化如此:
void times(
std::vector<double>& values,
double multiplier) {
transform_all(
[=](double x) { return x*multiplier; },
values);
}
孰美孰丑,一目了然。
再看 C++11 引入之另一伟大变革:搬家语义(move semantics)。此为何物,何以重要?
试想吾等欲定义一函数,给定人名,返回其称号。若已有一函数 get_user_info 可返回关于某人之信息,其中包含人物称号,则可轻松解出:
std::string get_title(
const std::string& name) {
UserInfo info = get_user_info(name);
return info.title;
}
惜乎,此解尚不完美:return 语句会将 info.title 复制一份至返回值中,既费工夫,又费内存。若遇称号冗长之人,尴尬矣:
C++11 之搬家大法,可避免此无用拷贝:
std::string get_title(
const std::string& name) {
UserInfo info = get_user_info(name);
return std::move(info.title);
}
此间 std::move(info.title) 可将 info.title 之内容轻松转移至返回值中,无需复制。如图:
搬家语义之功,不止于程序执行效率之提升,于程序之正确性亦大有裨益。
为何?只因 C++ 之头号大坑乃指针(pointer)之正确使用。
C++ 程序员常需创建新对象。一经创建,需以指针对之,至对象不再有用之时,需借助指针删除之:
// 创建对象。
Object* ptr = new Object;
...
// 使用对象。
ptr->do_something();
...
// 手动销毁对象。
delete ptr;
此之谓始乱终弃三部曲也。
初看之下,此模式平淡无奇。然无数先烈竞相折腰于此,皆因常有忘记销毁对象或重复销毁同一对象之事。前者将致内存泄漏,或令程序崩溃。后者尤为可怖,因其使程序跑飞,如癫人骑疯马,又好比特斯拉司机将刹车错踩成油门,后果不堪设想。
或曰,何不用共享指针(std::shared_ptr),自动记录对象引用者数,待其为零时自动删除对象即可?
此计虽好,却非万全。一则共享指针传递时需调整引用计数,代价较高;二则共享指针常不能及时删除对象,徒靡内存。故 C++11 引入唯一指针(std::unique_ptr),任一时间,仅许一枚 unique_ptr 指向同一对象。
// 创建对象。
std::unique_ptr<Object> ptr =
std::make_unique<Object>();
...
// 使用对象。
ptr->do_something();
// 下一行无法编译:unique_ptr不能复制。
std::unique_ptr<Object> ptr2 = ptr;
// 但可以搬家。
std::unique_ptr<Object> ptr3 =
std::move(ptr);
// 搬家之后ptr将为空,ptr3将指向对象。
...
// ptr3出局时自动销毁对象。
因其无需引用计数,亦不可复制,传递 unique_ptr 相当高效。又因无多枚 shared_ptr 人为替对象续命,删除及时,并无浪费内存之虞。是故,程序员当舍 shared_ptr 而取 unique_ptr 也。
如前所述,C++11 改进之处逾百,不再冗述。诸君有意,或可谷歌之。
自 C++11 始,比雅尼立志三年一更,遂有 C++11,C++14, C++17,C++20。C++23亦不远矣。欲知此系列新版 C++ 手感如何,且听下回分解。
~~~~~~~~~~
猜你会喜欢:
谷歌新语言 Carbon 能干翻 C++ 吗?- 深入浅出分析 Carbon
程序员护发秘籍 - 掌握这些工作技巧,包你不脱发
程序员的核心技能 - 以脱口秀的方式讲解程序员最重要的技能
如何做出保鲜十年的软件 - 老码农冒死披露行业内幕系列
dongbei 语言满月记事 - 一种基于东北方言的娱乐式程序设计语言
我在谷歌弄啥咧之十四 - 拿奖到手软
~~~~~~~~~~
关注老万故事会公众号:
本公众号不开赞赏不放广告。如果喜欢这篇文章,欢迎点赞、在看、转发。谢谢大家🙏