《新程序员》编辑部

《新程序员》编辑部

【CSDN 编者按】提到C++,不少人会想起Linus吐槽其是一门糟糕的编程语言,也有人觉得不断发版的C++,越来越像一门“新”语言。

本文主人公祁宇从事C++开发工作已有14年,从结缘再到一头扎进去深耕,对C++的兴趣和热爱随着版本的更迭代有增无减。在《新程序员004》的“我是程序员”专题中,祁宇回顾了自己与C++结缘、痴迷其中一路深耕的故事。这既是他的程序人生,也是C++的发展演进历程。

“越来越像新语言的 C++,我与它结缘、痴迷、深耕的 14 年!”-LMLPHP

缘起C++11

2008年我毕业后第一份工作就是C++开发,那时候是经典C++98/03标准,C++新标准还没有出来。而我对C++谈不上热爱,甚至相比之下,觉得C#用起来比C++要舒服得多,比如C#有GC(Garbage Collector)机制,没有内存泄漏的烦恼;有Lambda,代码也很简洁,不像C++那样需要定义函数对象;C#还有线程和锁,而C++多线程还只能用系统API,导致写跨平台程序很麻烦……诸多问题。

在我写了三年C++代码后,ß∂C++的一个里程碑——C++11诞生了。C++11提供的新特性有一百多项,让人眼花缭乱。C++11解决了之前C++98/03的大部分问题,比如用智能指针避免内存泄漏,Lambda摆脱了函数对象,std::bind统一了之前的bind1st、bind2nd,提供了线程库和锁,还有魔法般的可变模版参数等等。总之,C++11让C++焕然一新,变得很“现代”了,正如C++之父Bjarne Stroustrup所言:“C++11看起来像一门新的语言。”

C++11相比C#、Java等其他语言,从易用性角度来说已经毫不逊色了。而我也被这些新特性深深地吸引住,乐此不疲地研究和实践它们。在这个过程中,我对现代C++的兴趣被激发出来,第一次发现原来它的世界里有这么多好用又有趣的东西。

比如,C++11可变模版参数,对参数做了泛化,表示任意类型和任意个数的参数,这是C++11之前没有的。那么,该如何解析可变模版参数包里的参数呢?一种经典的写法是通过模版递归来实现。

void print(){}
template<typename T>
void print(T t)
{
 cout << t << endl;
}
template<typename T, typename... Args>
void print(T t, Args... args)
{
 print(t);
 print(args...);
}
int main() {
 print(1);
 print(1, 2);
 print(1, 2, 3);
}

这种解析方法比较繁琐,需要定义递归开始和递归终止函数。当时我看到了另一种有些怪异的展开方法,惊呆了!


template<typename... Args>
void print(Args... args)
{
 std::initializer_list<int>{([&]{cout << args 
<< endl; }(), 0)...};
}

我觉得这个写法看不懂,看了好久都没有看懂,查阅了一些资料后才恍然大悟。原来这种方法是借助初始化列表和逗号表达式来展开参数包,具体来说就是在生成初始化列表的过程中展开参数包,这个initializer_list数组是完全无用的,最终的元素都是0,它仅仅是一个帮助展开参数包的“工具人”。这种写法实在太巧妙了,也不知道是谁想出来的。

除此之外,C++还有很多有趣的黑魔法,比如借助SFINAE(模版替换失败不是一个错误)可以在编译期检查某个方法是否存在,如果存在就调用,不存在则忽略。这在其他语言里是无法实现的,正是这些好玩又很酷的东西强烈地吸引了我。

当然,也有很多人吐槽这些是让人费解的C++奇技淫巧,殊不知这些技巧在开发库的时候往往能化腐朽为神奇,优雅地解决问题,甚至在后面的C++14和C++17里已经变成常规写法了。再回过头来看,有种“旧时王谢堂前燕,飞入寻常百姓家”的感觉,这也证明C++是一门历史悠久、不断发展和充满活力的语言。

在2012—2014年期间,我花了大量时间去研究C++11,并用于工作当中,平时也会把对于C++11新特性的一些心得放到博客上分享出来。初衷很简单,我希望将C++11的好东西分享出来,让更多人了解和使用。这些文章很受欢迎,也让我体会到了技术分享的乐趣,从此一发不可收拾。

忽然有一天,冒出了写一本C++11书的想法,当时介绍C++11的书很少,我相信有很多人希望有一本书来讲解如何在实际开发中使用C++11。这是很有意义的一件事,于是我下定决心开始写书。在业余时间写书是一件苦差事,经常要熬夜到很晚,忍受孤独与枯燥,中途差点放弃,不过最终坚持下来,历时8个月写完,《深入应用C++11》在2015年出版,至今已经印刷12次了。很庆幸我能坚持把书写完,而对于C++的兴趣和热爱才是我坚持下去的动力。

所以,能发现并追寻自己的兴趣是一件难得的事。当你对一件事有着强烈的兴趣时,学习、研究和掌握它也是一件简单的事。

我经常跟一些C++新人讲学习C++的经验和方法最重要的是兴趣,如果仅仅把它当作一个工具,感受不到它带来的乐趣,是不容易掌握好它的。如果不是C++11让我感受到现代C++之美,感受到现代C++的魅力,我可能也找不到自己的人生乐趣,感谢C++11!也许这就是我和现代C++的缘分。

“越来越像新语言的 C++,我与它结缘、痴迷、深耕的 14 年!”-LMLPHP

本文节选自《新程序员004》纸刊+电子刊同步上市

创建现代C++开源社区

现代C++虽然在语言层面的开发效率不逊于其他语言,但在基础库方面就差太多了。在2014年,C++连个像样的HTTP库都没有,而这在Java、C#等语言中早就有了。正是这些基础库的缺失,极大地阻碍了C++的开发效率。出于对C++的热爱,我希望能开发很多实用的基础库来完善它的生态圈。于是在众多粉丝的支持下,2015年我决定成立一个现代C++开源社区——purecpp.org。在社区中开发C++基础库,期望来提升C++开发效率。我陆续开发了HTTP库cinatra、RPC库rest_rpc、序列化库iguana、ORM库ormpp、Web框架Feather等等,后来还在GitHub上创建了purecpp-org项目列表,把社区里的优秀开源项目聚合起来,便于C++开发者选择。

完善C++生态圈,让更多人用上好的C++基础库,这是我创办C++开源社区的初衷。也欣喜地看到社区开源项目被越来越多的公司采用,比如cinatra已经有十几家公司实名使用;rest_rpc被博世汽车车联网团队使用,已经跑在数百台汽车的嵌入式系统中;一些券商在使用ormpp访问数据库等等。

purecpp是一个很纯粹的C++社区,创办至今没有做任何的商业化,连广告都没有,专注于C++新技术分享和C++开源项目。从2018年开始,每年都会组织Pure C++大会,大会都是免费的,相关费用主要靠赞助,有时候是自己出资解决,社区大会也得到了很多热心朋友的帮助与支持,正所谓“C++不孤,必有邻”!

C++新标准与技术创新

有很多人对C++新标准不“感冒”,对新技术也持怀疑态度,甚至认为C++新标准不过是加了一些语法糖而已,认为新标准的一些写法是奇技淫巧,这其实是非常大的误解。诚然,C++新标准确实引入了一些语法糖来简化代码的编写,但更多是弥补了语言上的一些缺失,提高C++的易用性,提升安全性和性能,甚至改变编程理念。

一个典型的例子就是constexpr特性,它是在C++11中被引入的,用作编译期计算。通过它,可以用很简单的方式实现编译期计算,不用再像以前一样需要通过模版元编程来实现,使用难度大大降低。

以一个编译期计算阶乘的例子来展示constexpr的作用,通过图1我们可以看到,在C++98/03中,需要通过模版递归来实现编译期计算,代码比较晦涩。而在C++11中可以直接通过一个constexpr函数来实现编译期计算,即用constexpr函数来实现非模版的编译期计算。这个函数除了声明为constexpr外和编写一个普通函数没什么区别,这样就大大降低了编译期计算的难度。

“越来越像新语言的 C++,我与它结缘、痴迷、深耕的 14 年!”-LMLPHP

图1 编译期计算阶乘示例对比

不过,C++11中constexpr函数存在一些约束,它的函数体只能是一个表达式,不能定义变量,不能有循环语句等。在C++14中消除了这些约束,允许定义变量、允许有循环,让constexpr函数真正和普通函数的写法无异。

所以,C++14让constexpr的易用性得到了提升。

我在2021年Pure C++大会上讲过如何使用constexpr编译期计算来提升程序性能,当时分享的一个用constexpr做性能优化的案例,通过constexpr编译期计算将程序性能提升了三十多倍。

管中窥豹,从constexpr这个新特性可以看到C++新标准演进的一个重要趋势:让C++变得更加容易,性能更好。

正如purecpp社区的Slogan“Newer is Better!”我非常推崇使用C++新标准和新技术,因为C++新标准带来的新技术往往会促进技术创新,技术创新带来改变。我在2020年Pure C++大会上介绍了一种新的插件化开发方法,就是通过C++的新技术解决以前经典的C++插件化开发中插件需要派生于某个固定接口的问题。新的插件化方法完全摆脱了继承的约束,可以自由地在插件中写任意方法,这种新技术已经用在开源项目Ray当中了。

关于C++新标准带来的技术创新另外一个典型的例子就是编译期反射,我近期做了一些技术验证,基于C++20实现的编译期反射序列化库的性能是ProtoBuf和Msgpack的十几倍,这个测试结果让我有些吃惊。没想到原来基于编译期反射的序列化性能可以超越被广泛使用的序列化库这么多,这就是C++新标准技术创新的威力!

如果说C++11是C++的一个里程碑,那么C++20就是现代C++的一个里程碑。因为它引入了非常重要的几个特性,而这将改变C++的编程理念和模型,当然也让C++变得更加好用,比如Concepts、Modules、Coroutine、Ranges等。以协程(Coroutine)为例,它可以让我们用同步方式写异步代码,彻底摆脱了回调地狱(Callback Hell),异步回调模型将被协程代替。用C++20新标准,失去的是弯弯绕绕的Callback,得到的是简单直接的协程。协程让异步变得简单,在未来3~5年,C++网络库的协程化将是大势所趋。

Modules让我们不用再引入include头文件了,而是像其他语言一样使用import库就好,同时它还会大大提升编译速度。我们做过一个测试,将async_simple[2] 模块化后,编译速度提升了45%,性能也略有提升。ModulesABI相关的工作也正在做,可以说,Modules带来了C++统一包管理的曙光。

C++2a带来的使命感

C++新标准的演进速度比较快,从C++11开始保持着三年一个版本的迭代速度,后面还有C++23/26/29。而未来C++标准将提供更多好东西,比如Executors、Networking、编译期反射等。虽然新标准推出比较快,但基于新标准的库却跟不上,比如协程库,目前除了async_simple外,能用的无栈协程库几乎没有,这也是C++的遗留问题。

想象一下,如果把协程、编译期反射、Executors结合起来开发一些高性能易用的网络库,那是多么有意义的一件事!我也正在做这方面的工作,希望能尽早推出到C++开源社区当中。

研究C++2a新标准、新技术及其应用场景,创造符合应用需求的基础库让我有一种使命感,我会和C++社区的朋友们一起继续努力推出更多实用的、基于C++新标准的基础库给C++开发者使用,完善C++生态圈。


“越来越像新语言的 C++,我与它结缘、痴迷、深耕的 14 年!”-LMLPHP

09-21 18:57