查看原文
其他

【老万】后C++演义第三回:蛮生长亟待拨乱返正,勤整治方得服人归心

老万 老万故事会
2024-08-23

后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++ 手感何,且听下回分解。

~~~~~~~~~~

猜你会喜欢:

~~~~~~~~~~

关注老万故事会公众号:

本公众号不开赞赏不放广告。如果喜欢这篇文章,欢迎点赞、在看、转发。谢谢大家🙏

继续滑动看下一个
老万故事会
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存