阅读视图

发现新文章,点击刷新页面。

这些资源帮助你深入学习C++

这些年来,很多人都向我寻求学习 C++的帮助。 我算不上什么 C++专家, 但是作为一个从事 C++多年的人, 我想在这分享一些高质量并且同时适合初学者的 C++资源。 希望这些资源对您有所帮助。

当有人问我有关使用 C++的指导时, 我总是首先问他们已有的编程经验经验。 有些人刚开始学习编程,并决定学习 C++作为他们的第一门编程语言; 有些人已经掌握了少量的 C++,并且想要学习更多; 而有些人已经使用了其他语言编程多年,然后尝试学习一些 C++。 因为不同的人有不同的背景以及不同的学习目标, 所以我会推荐一些不同的材料。

不过,我想提到的一件事是,仅仅阅读书籍或观看视频并不是学习的最佳策略。 无论您处于什么阶段,把学到的知识用到实践中都非常重要,因此开始进行一些编程项目会对你的学习很有帮助。

另外一件我想提到的是, 我在这里推荐到资源基本上都是英文资源。 我强烈建议您试图通过英文资源来学习, 因为您只通过中文来学习编程, 那么您将失去使用绝大多是好的学习资料的机会。

如果我刚刚开始学习编程并选择 C++作为我的第一门编程语言,我该怎么做?

对于编程初学者来说,我推荐 Bjarne Stroustrup(C++之父)的《C++程序设计:原理与实践》第二版(Programming: Principles and Practice Using C++ 2nd edition)。 这本书有中文翻译,不过就像我之前说的,如果您有一点的英文阅读能力,我建议您阅读原版。 因为这本书很厚,所以您不一定能够坚持看完整本,但是无论您看了多少页你都能学到东西。

如果您不想要看书, C++专家 Kate Gregory 在 Pluralsight 网站上提供了不少的视频教程。 其中她的入门教程是Learn to Program with C++。 如果你加入#include<c++> discord 服务器, 你可以在服务器内直接为她要一份试用码。

如果我以前已经学习过一些 C++并且想更深入地学习,我该怎么做?

也许您已经从大学数据结构课程中使用过一些 C++, 又或者您学习了一些使用 C++的在线教程, 接下来该做什么?

根据我的个人经历以及我所听闻的, 大多数大学编程课程或那些在线教程的质量都偏低,而且讲师通常对 C++一知半解。 您可能会被之前的学习资源所误导,并且学习到了一些错误的实践或者是对概念的误解。 因此,选择正确的学习资料是对高效学习十分重要的一点。

在这种情况下,我同样会推荐 Bjarne Stroustrup 的《C++程序设计:原理与实践》第二版。 你可以看书看得比纯粹初学者更快一些,不过使用该书来系统地查漏补缺依然很有好处。 如果您更喜欢视频教程, 可以从 Kate Gregory 的C++ Fundamentals Including C++17开始。

如果我是另一门语言的资深人士并想学习 C++,该怎么办?

如果您已经精通了某个其他的编程语言,并且想开始学习一些 C++, 您可以直接选择更加进阶的材料。

对于书来说, 我建议阅读 Bjarne Stroustrup 的《C++程序设计语言》第四版(The C++ Programming Language (4th Edition))。 这本书是我读过的最好的技术书籍之一。 不过这本书也同样相当得厚。如果您没有时间阅读该书并且想要有一个简短的 C++介绍, 您可以购买《A Tour of C++》第二版。

我认为我对 C++有一定的了解了。 下一步是什么?

如果您花了数月的时间学习上述资料, 并觉得您对 C++基本概念有相当的了解。 接下来该做什么?

如果您达到了这个阶段,那么您应该对下列的多数话题有相当的熟悉程度:

  • 如何正确使用const
  • 模板(templates)
  • 引用(references)以及指针(pointers)
  • 对标准库的熟练使用,尤其是迭代器(iterators)以及标准算法(algorithms)
  • RAII
  • 析构函数(destructor)
  • 复制/移动构造函数以及复制/移动赋值运算符
  • 移动语义(move semantics)
  • 运算符重载(operator overloading)
  • lambda 表达式以及函数对象
  • 未定义行为(undefined behaviors)

如果你已经到了这个阶段, 那么为 C++找到实际用途或许比学习 C++语言本身更重要了。 C++被用于许多不同的用途, 而您也可以开始考虑如何把 C++应用在您感兴趣的领域上。

同样,现在是学习 C++生态系统的好时机,您可以花一些时间来深入学习例如Catch2等单元测试库,CMake等构建系统, 以及Conan等包管理器。

另外一个可以考虑的事是开始学习另一门编程语言, 尤其是如果您目前仅了解 C++一门语言。 下一个不错的选择是与 C++截然不同的语言,例如 Javascript,Python 或 Lisp 等动态类型的语言。

话虽这么说,仍然有无尽得关于 C++语言本身的知识可以学习。 我将尝试在以下列出一些我喜欢的资源:

书籍

如果你仍然没有阅读《C++程序设计语言》第四版(The C++ Programming Language (4th Edition)的话, 这本书仍然是一个非常好的选择。除此之外,我还有一些其他的书可以推荐:

  • Scott Mayer 的《Effective Modern C++》
  • Jason Turner 的《C++ Best Practices》
  • Nicolai M. Josuttis 的《C++17 - The Complete Guide》

还有一些书籍会关注于某些特定的方向,例如:

  • David Vandevoorde、Nicolai M. Josuttis、以及 Douglas Gregor 的《C++ Templates - The Complete Guide, 2nd Edition》
  • Arthur O'Dwyer 的《Mastering the C++17 STL》
  • Ivan Čukić 的《Functional Programming in C++》
  • Anthony Williams 的《C++ Concurrency in Action, 2nd edition》

大会讲话视频

大会讲话同样是学习 C++的绝佳资源。下列是一些我个人喜欢并且适合初学者的讲话:

社群

加入编程社群有非常多的好处,你可以向专家提问,知道他人的动态,讨论有关工作的信息,甚至交到一些好朋友。

#include<C++>

#include<C++>是一个非常不错的 C++社群, 它提供了一个友好的讨论环境,并且你在里面可以找到多个在 C++界知名的人物。 您可以加入它的 discord 服务器并且和大家一起讨论 C++。

见面会(Meetups)

加入North Denver Metro C++ Meetup是我大学阶段做出的最好决定之一。如果您有时间的话,参加一些本地的 C++见面会是一个非常不错的选择。(因为新冠的原因,现在绝大多数见面会以及大会都在网上召开,这有利有弊,但一个很大的优势是您现在可以参加全球的见面会)您可以在meetup.com网站上搜索本地的见面会。

参加大会

如果您认真对待 C++,那么大会是结识志趣相投的人的好地方。 这是我知道的一些重复举办的 C++会议:

除此之外,ISO C++网站上有一个大会列表

播客

网上有不少 C++的播客,尤其是 2020 年有不少新的播客涌现。当然,所有的这些播客都需要较好的英语听力水平:

博客

我推荐使用 RSS 来关注各种技术博客。 我个人关注超过 200 个关于 C++或者其他技术话题的博客, 下列是一些我个人认为最好的 C++博客:

需要注意的是某些博文会讨论非常高深的话题,因此您并不一定需要读懂每一篇博文。

其他资料

下面是一些其他有用的 C++资源:

  • cppreference是最好的 C++语言以及标准库 API 文档网站
  • Compiler Explorer一个在线编码环境,支持 ++和许多其他语言。 它可以编译后的汇编码以及运行程序。
  • Quick C++ benchmark是一个可以快速对 C++代码进行测速的网站。

引用以及扩展阅读

涿州之灾:“保卫北京”?极端气候下谁影响灾难的流向?

「未来面对日益无常的极端气候天气,政府要如何救灾才是负责任的?」

郑昶人

2023年8月1日,中国涿州,救灾人员正在疏散村内被困市民。摄:Zhai Yujia/China News Service/VCG via Getty Images
2023年8月1日,中国涿州,救灾人员正在疏散村内被困市民。摄:Zhai Yujia/China News Service/VCG via Getty Images

2023年,中国华北水灾如此严重,究竟是为什么?

这个问题简单。归根结底,这是一次极其严重的极端气象事件,在两个台风系统的水汽输送下,太行山和燕山脚下的京津冀地区在几天之内达到了一年多甚至两年的降水量,形成百年难得一遇的洪水。这种短时间内的大量降水不同于流域内长时间降水形成的逐渐涨水,后者即便基建不足,还可以通过加固堤坝、修建临时分洪道等等方法避免决堤、溃坝等等,而这次的洪水来了就是来了,全看平时有没有足够的水利基建。

特别是距离山脉更近的北京门头沟区、河北涿州城区,原本就容易形成急速下冲的山洪,而史所罕见的暴雨在几个小时内就快速涨水成灾,更加难以防备。实际上,由于气候变化和经济发展导致的用水增加,华北在过去几十年面对的更多是干旱而非洪涝,很多以前的发达水系现在都退化成了季节性的,夏天下雨就有水,更多的时候可能和断流没有差别。包括白洋淀,若非雄安新区重建生态,可能已经干涸。

可以很清楚地看到,除了社会意义,政府也非常清晰地理解灾难应对的政治意义。

但这个问题也很复杂。任何灾难都不止是单次的自然事件,而是前前后后的过程,防灾、救灾都是灾难过程的一部分。而这些人类的部分,通常都能够追溯到政府层面。人类社会之所以形成复杂组织,很重要的原因就是为了应对灾难,集中资源的政府是天然的最终责任人。特别是自古水患严重的地区,专门有历史假说认为古代集权政府的诞生,是为了集中资源修建大型水利设施,中国就是其中一例。

应对灾难的水平可能直接影响民众对政府的信任水平。任何天灾,无论致灾的自然因素极端水平有多么严重,政府都不可能推脱责任。用习近平自己的话说,“防灾减灾救灾事关人民生命财产安全,事关社会和谐稳定,是衡量执政党领导力、检验政府执行力、评判国家动员力、体现民族凝聚力的一个重要方面。”可以很清楚地看到,除了社会意义,政府也非常清晰地理解灾难应对的政治意义。中国政府一直以家长式的保护角色自居,可是,这次从应急到救援政府反应为何那么差?未来面对日益无常的极端气候,政府要如何救灾才是负责任的?以及,这只是中国的责任吗?

2023年8月4日,中国北京,桥梁因强降雨引发山洪后倒塌。摄:Kevin Frayer/Getty Images

2023年8月4日,中国北京,桥梁因强降雨引发山洪后倒塌。摄:Kevin Frayer/Getty Images

保卫北京?保卫北京

很多北京人拿着流域图说事,其实忽略了根本问题:凭什么上游的河北人就得保卫下游?难道不是因为北京意欲如此?

此次灾后最大的话题之一是“保卫北京”。必须承认,水文上来说,受灾严重的涿州等河北地区真不是北京的受害者——北京西南水系向河北分为数条河流,最终汇合在海河,河北中部地区保卫的是下游处于低洼的雄安新区,以及作为海河入海口的天津。有雄安公众号说,如果不是新区建设,恐怕这次要淹的就是白洋淀周围的三县了。此言不差。

水往低处走,但是低地的定义不仅仅是自然的。水的流向是人为可以控制的。北京在上游,既不容易被淹也不能被淹,下游的天津容易被淹却不能被淹,中间的雄安海拔最低,却也不再能被淹。地势的“高”与“低”在华北是一种政治概念,水只能去政治地位低的地方。

1963年海河大洪水时,为了防止下游的天津城被淹,白洋淀作为其中一个蓄洪区,疏散了周边几个未来划在雄安境内的县城。即便如此,当年北京的朝阳等区依然受灾严重。在白洋淀周边,由于1963年泄洪疏散后居民迁走,周围经济发展导致用水增加、来水减少,很多原本留给蓄滞洪水的河道、湖区干涸,变为荒地。这些为了保卫人口稠密地区的大局才形成的荒地,阴差阳错又成为了疏散非首都功能大局的雄安新区的选址。

为了保卫人口稠密地区的大局才形成的荒地,阴差阳错又成为了疏散非首都功能大局的雄安新区的选址。

从自然角度上,北京、河北、天津同处一片古代冲积平原,被水网联系在了一起。古代,河流上下游的关系就是经济的流向,但是自从北京成为首都,这几个城市之间的联系就转为了政治的。很长一段时间内,上游的北京在政治上一直压过下游的天津,但在清朝,作为漕运门户和海运港口的天津被外国占作租界城市,逐渐从农耕时代的卫星府城变为了华北经济重镇。两座城市相互依存,也相互竞争。竞争什么呢?谁能从河北拿到更多的空间。

周边的河北本质上就是京津两城的奶牛,周边县城不断被划入两个直辖市,还要为京津供水、供电、供人力,给他们打下手。比如涿州为何集聚如此多的图书仓库,是因为北京从2017年开始驱逐低端人口、清理周边小仓储,低成本的河北像承接北京不要的重工业一样承接了北京抛弃的产业。政治上的安排改变了华北平原的自然地貌联系,这片区域中的政治中心北京成为最高地,天津是次高地,雄安在逐渐垫高,其它地方都是低地。资源的流向和水恰恰相反,哪里高,他们去哪里。

2023年8月3日,中国保定,救援队疏散当地被困居民。摄:CFOTO/Future Publishing via Getty Images

2023年8月3日,中国保定,救援队疏散当地被困居民。摄:CFOTO/Future Publishing via Getty Images

所以这次救灾出现令人咋舌的诸种不畅,例如泄洪过于突然、民间进入救援却要官方发邀请函、用于救援的橡皮艇不够等等,除去突发极端气象、华北近年水灾救灾经验少以外,根本上是因为这就是泄洪区的政治生态:总是做好被放弃的准备,总是以上游的安排为基本行动指南。这个上游以前有时是水文意义上的下游天津,现在有时是雄安,但是永远是北京。很多北京人拿着流域图说事,其实忽略了根本问题:凭什么上游的河北人就得保卫下游?难道不是因为北京意欲如此?

泄洪区是一种普遍的政治结构,永远处于灾难的预备状态,处于永续的生态紧急状态,行政权力在根本上主导全部资源分配,自然组织的调控是不必要且暂时的。

虽然说世界各国都有保护下游城市的泄洪区安排,但是中国水患多,人口多,无法做到完全禁止泄洪区住人,这也就形成中国泄洪区的特殊政治模式。出身淮北的学者马俊亚研究为什么自己家乡所在的黄泛区从文明摇篮变成了“穷山恶水”,写出了《被牺牲的“局部”》。他的结论非常直白:“淮北社会问题的根源,历来是权力积累的不平等,从而导致经济积累方面的不平等,并由此造成社会的不公。”明清两代,黄淮水患频发,为保北京粮食的漕运通畅,有司治理黄河时牺牲了淮河,在淮河流域蓄积湖水冲刷黄河泥沙。但是因为黄高淮低的水势,这一保证漕运的举措反而导致淮北饱受洪灾之苦。原本与北京没有流域关系的淮北,反而变成了北京的下游,要为北京承担水患。这种政治不平等带来的高差,才真正决定了洪水的流向。

河北是一样的。流域的上下游并不是灾难的核心。比如兰沟洼蓄滞洪区,原本是白洋淀的上游,但是在雄安防洪关闸之后,洪水经由新修的水利枢纽直通东淀和天津的静海,这句话提到的所有地方原本是同一流域,但这次除了雄安全都承担了泄洪任务。在泄洪区,最好的情况下是有人提醒你,你的土地会被淹没,你的财产会被赔偿,你的生活要等待一个势必到来的灾难,最坏的情况是——连你自己都选择遗忘。

泄洪区是一种普遍的政治结构,永远处于灾难的预备状态,处于永续的生态紧急状态,行政权力在根本上主导全部资源分配,自然组织的调控是不必要且暂时的。所以为什么官僚会对民间介入那么不知所措?一来当然是新时代以来对民间组织讳莫如深,并没有形成固定的合作模式,二来是泄洪区不能有民间自发。试问,如果泄洪区组织起来反对炸坝泄洪,那泄洪区不就没用了?所以才有河北霸州市男子擅自在泄洪区大堤上停留观望被行政拘留。很可能,和很多泄洪区村民一样,他们在堤坝上守着只是为了家园不被淹没。

2023年8月3日,中国涿州,一名当地居民在水深及胸的洪水下,涉水走向一艘救援船。摄:Kevin Frayer/Getty Images

2023年8月3日,中国涿州,一名当地居民在水深及胸的洪水下,涉水走向一艘救援船。摄:Kevin Frayer/Getty Images

“人类世”的灾难分配

任何情况下,中国的问题必然和党有着这样那样的关系,但这并不是问题的全部。气候问题是真的有不讲政治的部分。

不仅仅是华北,东北、西北也正遭遇洪水,看起来并不是每个地方都有河北如此经典的泄洪区政治。我们可能要回到最根本的问题——谁在决定灾难的流向?

我可以再做一些微观的政治学总结,或者是宏观的政治经济分析,对新时代现状导致的救灾不力批判一番,可是批判中国政治太简单了。东西南北中、党政军民学,党是领导一切的,任何情况下,中国的问题必然和党有着这样那样的关系,但这并不是问题的全部。

在我看来这不应该仅仅被视作是一个中国的政治事件。气候问题是真的有不讲政治的部分。蒙古的风沙会吹来北京,在山东登陆的台风可能会在朝鲜带来暴风雨,造成暴雨的厄尔尼诺现象会让美国加州更多山火。在京津冀,党有能力决定水流,可是同样的水灾以后会频繁地在全世界发生,那时德国或者美国的某个保留泄洪区会被灌满,城市也会被淹没。回到根源,如果全球气候变暖按照现在的趋势发展下去,气候问题将会演变为全球性的灾难,任何政治问题都将只是气候灾难的次生灾害。如果把气候事件割裂地理解为单个国家的政治问题,只会阻碍我们理解人类的普遍责任。

我很想说,没有人应该有决定灾难流向的权力,但这是非常幼稚和不现实的说法。如果是人类刀耕火种的年代,神力——或自然的伟力是必然的悲剧,但是到现在,人类对地球的影响已经大到形成了“人类世”,人类没有任何理由说自己对自然没有干预能力。就是这种能力在无意识的情况下,促成了1945年开始的“大加速”,气候变化的速率急速上升。战后普遍的经济发展让人类在地球的碳、氮循环中获得了主导地位,核辐射、微塑料、碳排放等等人类痕迹已经让地球进入了一个和前一个地质时代完全不同的世代。

这个地质时代最明显的特征我们可能正在见证,灾难将会成为日常经验。可大多数人根本没有这种心理准备。事实上,现在根本没有什么城市或者工程是以气候紊乱为前提建设的。

这个地质时代最明显的特征我们可能正在见证:急速的气候变化、频发的极端气候事件,一切都说明气候稳定的时代已经终结了。我们已经踏入了“乱纪元”,灾难将会成为日常经验。可大多数人根本没有这种心理准备。

在过去,人类的历法建立于稳定环境,四季有定时,有物候标准的节气。但是今天,北京没进伏天就有四十度,南美高原冬天还有三十度,很难想象如果这样的气候变成三年一度甚至一年一度,我们这样对环境的稳定周期认知还有没有意义。我们还在用全新世的气候数据来判定某种现象在气象意义上“十年一遇”、“百年一遇”,并建基于此进行城市建设。事实上,现在根本没有什么城市或者工程是以气候紊乱为前提建设的。有些人根据门头沟不久前修建海绵城市的政府宣传,来嘲笑门头沟在本次暴雨中几乎全境街道都在行洪。但是海绵城市本来就是以一年一遇级别的降水下渗为标准去设计的,并不能用来应对百年一遇的极端降水。

想要减灾,极端气候必须极端规划。很不幸,我们要学习的榜样可能是那个霸占了白洋淀沿岸的雄安。正是因为所有人都知道雄安是一个活该被淹但是不能轻易被淹的地方,雄安拥有200年一遇级别的防洪水利措施,能够让雄安在这次洪灾中不可思议地——也并不应该地——做到了独善其身。这种意义上,雄安真的是一个歪打正着的千年大计,一个极端年代的未来城市。可问题就在于,雄安不可复制,不在于修建雄安的经济效率太低,而在于雄安是独一无二的政治安排,它的存在本身就是要让首都变得更像首都,而首都永远只有一个。更糟的是,如果气候变化的幅度和频率进一步变大、变快,极端规划还来得及吗?十年前的百年一遇,可能在未来只是十年一遇,那纵使是雄安也自身难保了。

2023年8月3日,中国涿州,救援人员在被洪水淹没的地区寻找当地居民。摄:Kevin Frayer/Getty Images

2023年8月3日,中国涿州,救援人员在被洪水淹没的地区寻找当地居民。摄:Kevin Frayer/Getty Images

有些时候是政治上的高差引导灾难的流向,有时则是经济。比如,在日渐严重的美国加州山火中,富人能够购买每天几千美元的私人消防员保护私宅。根据加州大学圣巴巴拉分校的一项研究,美国西部山火接近房屋时被灭的可能性会提高16%,靠近的房屋价值更高,火更有可能被扑灭。按研究者的说法,“假设一场大火正在烧向价值一千万美元的房屋:当大火接近十栋价值一百万美元的房屋时,它被扑灭的可能性要比接近一百栋价值十万美元的房屋大”。这同样是一种人为控制的流向。

在灾难时代,如果我们不在泄洪区,那要对潜在的泄洪区负起怎么样的责任?不管那是一个中国的政治低洼地、还是国际上的气候政治低洼地。

而更宏观地说,人类社会中更发达、更有权力决定全球环境变化水平的国家,可能可以决定未来海平面上涨的幅度、森林的面积、控制下游水域的水利设施等等,而灾难对他们的影响将会被引导向其它国家。这样下去,普遍化的极端气候将带来普遍化的以邻为壑,今后将会出现跨国意义上的雄安和泄洪区。对于低海拔国家来说,他们是高排放国家的泄洪区;对于缺水国家来说,他们是上游修坝国家的泄洪区。如果这些泄洪区国家有强力政治组织,他们(也可能是我们)会反抗,而如果没有,他们会和淮北、涿州一样陷入悲惨的境地。政治和经济的不平等秩序进一步加重受灾程度的不均匀分配,而国与国之间的分裂会让这种不均匀变成次生灾难。

我很难想象,未来我们需要什么样的激进国际政治安排——或者说,在灾难时代,如果我们不在泄洪区,那要对潜在的泄洪区负起怎么样的责任?不管那是一个中国的政治低洼地、还是国际上的气候政治低洼地。这就好像人类总是面临着电车难题,每个人都绑在车轨上,改变车轨道的杆子可能每个人手里都有一根,不是你拉就是别人拉。其中一种方案是发行某种平衡责任的债券——真正意义上的赎罪券。

但或许对所有人来说,最简单也是最难的方案,就是在步入这样的极端时代之前,从个人生活、公共生活层面上,都尽力去参与阻止气候变化加速的一切行动。同样,我们也需要同等水平和规模的政治和经济改革,才能在一切都太晚之前保证我们都负起同等的责任、有平等的生存权利。

这也是一种现实:在处处皆可能黄泛区的未来,若没有次次生还的胜券和信心,更多的关心和参与则是对自己负责的政治选择。

A Quicker Study on Tokenising

I recently stumbled upon this nice blog post by Josh Barczak, comparing the performance of various C++ string tokenisation routines. The crux of it is that by writing low-level C-like code, Josh was able to get better performance than by using Boost or either of the standard library solutions he tried.

This post is meant as a rebuttal, showing that by using the STL properly we can get simple, elegant, generic, reusable code that still performs better than the hand-coded solution.

The problem

So let’s take look at the problem. In his code, Josh takes a reasonably large (~20MB) text file, splits it up into tokens, and then copies those tokens to an output file. The final hand-coded method looks like this:

static bool IsDelim( char tst )
{
    const char* DELIMS = " \n\t\r\f";
    do // Delimiter string cannot be empty, so don't check for it
    {
        if( tst == *DELIMS )
            return true;
        ++DELIMS;
    } while( *DELIMS );

    return false;
}

void DoJoshsWay( std::ofstream& cout, std::string& str)
{
    char* pMutableString = (char*) malloc( str.size()+1 );
    strcpy( pMutableString, str.c_str() );

    char* p = pMutableString;

    // skip leading delimiters
    while( *p && IsDelim(*p) )
        ++p;

    while( *p )
    {
        // note start of token
        char* pTok = p;

        do// skip non-delimiters
        {
            ++p;
        } while( !IsDelim(*p) && *p );

        // clobber trailing delimiter with null
        *p = 0;
        cout << pTok; // send the token

        do // skip null, and any subsequent trailing delimiters
        {
            ++p;
        } while( *p && IsDelim(*p) );
    }

    free(pMutableString);
}

Now I don’t want to pick on Josh, because this code works, and it’s faster than anything else he tried. But… well, let’s just say it’s not a style of code I would enjoy working with. Let’s see how we can come up with something better.

Evolving a splitting algorithm

First, let’s take a step back and look at things from a mile-high view: given an input string str and a set of delimiters delim, how would you describe to someone in plain English how to split the string? Bear in mind that although it’s not required in this case, for other uses we we may want to keep empty tokens which occur when we have two consecutive delimiters.

It turns out this isn’t so easy. My effort is the following:

“If the string is empty, then you’re done. Otherwise, take the first character of str, and call it first. If first is a delimiter, then the string begins with an empty token; save the token, remove first from the string and start again. If first is not a delimiter, then scan along the string from first until you find another character which is a delimiter, or else reach the end of the string. Now the interval [first, last) consists of one token; save that token. Remove the closed interval [first, last] from the string and restart.”

Translating this naively into C++, we get something this:

// Version 1
bool is_delimiter(char value, const string& delims)
{
       for (auto d : delims) {
           if (d == value) return true;
       }
       return false;
}

vector<string>
split(string str, string delims)
{
    vector<string> output;

    while (str.size() > 0) {
        if (is_delimiter(str[0], delims)) {
            output.push_back("");
            str = str.substr(1);
        } else {
            int i = 1;
            while (i < str.size() &&
                   !is_delimiter(str[i], delims))  {
                i++;
            }
            output.emplace_back(str.begin(), str.begin() + i);
            if (i + 1 < str.size()) {
                str =  str.substr(i + 1);
            } else {
                str = "";
            }
        }
    }

    return output;
}

This algorithm actually works, provided you’ve got enough patience – with all the string copies going on, it’s very, very slow. (Though if you use std::experimental::string_view, which doesn’t copy but just updates a couple of pointers, then this actually performs respectably – but we can still do better.)

Now this isn’t great, but it’s at least something to start with. Let’s iterate on it and see where we get. The first thing we want to do is to stop making all the string copies. That’s actually not too difficult. Instead of chopping the front off the string every time we go through the loop, we’ll use a variable to keep track of the “start”. Having done that, and with a minor tidy-up, we arrive at:

// Version 2
bool is_delimiter(char value, const string& delims)
{
       for (auto d : delims) {
           if (d == value) return true;
       }
       return false;
}

vector<string>
split(const string& str, const string& delims)
{
    vector<string> output;
    int first = 0;

    while (str.size() - first > 0) {
        if (is_delimiter(str[first], delims)) {
            output.push_back("");
            ++first;
        } else {
            int second = first + 1;
            while (second < str.size() &&
                   !is_delimiter(str[second], delims))  {
                ++second;
            }
            output.emplace_back(str.begin() + first, str.begin() + second);
            if (second == str.size()) {
                break;
            }
            first =  second + 1;
        }
    }

    return output;
}

Again, this works, and it’s much faster than before. But… well, it’s ugly. We have two different cases depending on whether str[first] is a delimiter or not, and we’re calling is_delimiter() twice in many cases. Can we collapse these cases down to a single one?

It turns out we can, with just a minor change: instead of defining second to start at first + 1, we just initialize it with first instead. Now, if first is a delimiter then the inner while loop will exit immediately and second will never be incremented, so we end up emplacing an empty string in the vector just as we’d like. Once we collapse down the two cases, we end up with version 3 of our algorithm:

// Version 3
bool is_delimiter(char value, const string& delims)
{
       for (auto d : delims) {
           if (d == value) return true;
       }
       return false;
}

vector<string>
split(const string& str, const string& delims)
{
    vector<string> output;
    int first = 0;

    while (first < str.size()) {
        int second = first;
        while (second < str.size() &&
               !is_delimiter(str[second], delims))  {
            ++second;
        }
        output.emplace_back(str.begin() + first, str.begin() + second);
        if (second == str.size()) {
            break;
        }
        first =  second + 1;
    }

    return output;
}

We’ve only removed 4 lines, but this is already beginning to look much better.

Enter the iterator

Up until now, you’ll notice that we haven’t used iterators, but rather first and second have been integer indices into the string. That was deliberate, because a lot of people seem to be put off by iterators. They needn’t be: an iterator is really just an index into some set. All we need to do is to change first = 0 to first = std::cbegin(str), and the str.size() checks into checks against std::cend(str):

// Version 4
bool is_delimiter(char value, const string& delims)
{
       for (auto d : delims) {
           if (d == value) return true;
       }
       return false;
}

vector<string>
split(const string& str, const string& delims)
{
    vector<string> output;
    auto first = cbegin(str);

    while (first != cend(str)) {
        auto second = first;
        while (second != cend(str) &&
               !is_delimiter(*second, delims))  {
            ++second;
        }
        output.emplace_back(first, second);
        if (second == cend(str)) {
            break;
        }
        first =  next(second);
    }

    return output;
}

As you can see, the code is barely any different, and performs identically.

Now, let’s turn our attention to the inner while loop. Slightly reorganised, this is:

auto second = first;
while (second != cend(str) {
       if (is_delimiter(*second, delims))  {
           return second;
       }
    ++second;
}

What is this snippet really doing? It’s saying “find the first element in the interval [first, end()) which is a delimiter”. Now, “is a delimiter” means “is a member of the set of delimiters”, so if we put these together then the while loop is saying

“Find the first element of the interval [first, end()) which is also in the interval [delims.begin(), delims.end())

Fortunately for us, there is a stardard algorithm that does exactly this: it’s called std::find_first_of(). Let’s update our code to use this algorithm, which gives us the (almost) final version:

// Version 5
vector<string>
split(const string& str, const string& delims)
{
    vector<string> output;
    auto first = cbegin(str);

    while (first != cend(str)) {
        const auto second = find_first_of(first, cend(str),
                                          cbegin(delims), cend(delims));
        output.emplace_back(first, second);
        if (second == cend(str)) break;
        first =  next(second);
    }

    return output;
}

This version still adds empty strings to the output when it comes across two consecutive delimiters. This is sometimes what people want, but sometimes not, so let’s make it an option. Also, the most common case for splitting is to use a single space as a delimiter, so we’ll use that as a default parameter. Making these changes, and putting back the std:: directives that we have so far elided, we our final string splitter:

// Final version
std::vector<std::string>
split(const std::string& str, const std::string& delims = " ",
      bool skip_empty = true)
{
    std::vector<std::string> output;
    auto first = std::cbegin(str);

    while (first != std::cend(str)) {
        const auto second = std::find_first_of(first, std::cend(str),
                                               std::cbegin(delims), std::cend(delims));
        if (first != second || !skip_empty) {
            output.emplace_back(first, second);
        }
        if (second == std::cend(str)) break;
        first =  std::next(second);
    }

    return output;
}

This code is simple enough that we don’t even need to add comments. The core of it is the find_first_of() call, which is easily looked up even if you can’t guess what it does from the name. But we can do better yet.

A more generic tokeniser

It’s long been a criticism of those coming to C++ from other languages that there is no split() function for strings in the standard library. The reason is that doing so in a generic way is pretty tricky. Let’s have a try at it now:

// Bad generic split
template <class Input, class Delims>
vector<Input>
split(const Input& input, const Delims& delims,
      bool skip_empty = true)
{
    vector<typename Input> output;
    auto first = cbegin(input);

    while (first != cend(input)) {
        const auto second = find_first_of(first, cend(input),
                                          cbegin(delims), cend(delims));
        if (first != second || !skip_empty) {
            output.emplace_back(first, second);
        }
        if (second == cend(input)) break;
        first =  next(second);
    }

    return output;
}

Unfortunately, this falls apart as soon as we try to call it with a string literal, because the compiler will complain that it cannot intantiate a vector<const char[17]> (or something similar). Also, what if we don’t want to output a vector? A generic solution should surely let us use whatever container we like. What if we are streaming in the input via istream_iterator? How do we pass the output to an ostream?

This problem is pretty tricky, but it’s not insurmountable. Our splitting algorithm is sound – it will work for anything that models the InputIterator concept. The problem is, what do we do with the tokens once we’ve found them? Actually, the answer is obvious: we should let the caller do whatever they like, by letting them pass in a function which we will call every time we find a token.

Then our generic solution then looks like this:

// Good generic "split"
template <class InputIt, class ForwardIt, class BinOp>
void for_each_token(InputIt first, InputIt last,
                    ForwardIt s_first, ForwardIt s_last,
                    BinOp binary_op)
{
    while (first != last) {
        const auto pos = std::find_first_of(first, last, s_first, s_last);
        binary_op(first, pos);
        if (pos == last) break;
        first = std::next(pos);
    }
}

This simply calls the given function for each token (hence the name), passing the first and one-past-the-end iterators we’ve found. Writing our split() for strings in terms of this generic function is trivial:

vector<string>
split(const string& str, const string& delims = " ",
      bool skip_empty = true)
{
    vector<string> output;
    for_each_token(cbegin(str), cend(str),
                   cbegin(delims), cend(delims),
                   [&output] (auto first, auto second) {
        if (first != last || !skip_empty) {
            output.emplace_back(first, second);
        }
    });
    return output;
}

Our generic for_each_token() is simple, elegant, and it shows off very nicely the power of the STL. All of which is very nice, but pointless if it isn’t fast. Is it fast?

Yes. Yes it is.

Performance

In order to measure performance, we’ll use Josh’s original microbenchmark from here, slightly modified to use a timer based on std::chrono::high_performance_clock rather than boost::timer. Re-running the original tests on my system (GCC 5.3 with -O3 on a Macbook Pro running OS X El Capitan), and taking the average of 5 runs for each algorithm, I get the following profile:

Original

As you can see, the results are almost the same as Josh’s, except that Boost does slightly better this time. Josh’s approach is still fastest, with strtok() a close second.

Now let’s add our method, using the generic algorithm above. This is the code I added:

// 5 statements
template <class InputIt, class ForwardIt, class BinOp>
void for_each_token(InputIt first, InputIt last,
                    ForwardIt s_first, ForwardIt s_last,
                    BinOp binary_op)
{
    while (first != last) {
        const auto pos = find_first_of(first, last, s_first, s_last);
        binary_op(first, pos);
        if (pos == last) break;
        first = next(pos);
    }
}

// 2 statements
void DoTristansWay(std::ofstream& cout, std::string str)
{
    constexpr char delims[] = " \n\t\r\f";
    for_each_token(cbegin(str), cend(str),
                   cbegin(delims), cend(delims),
                   [&cout] (auto first, auto second) {
                       if (first != second)
                           cout << string(first, second);
                   });
}

The full code is available in this gist.

This time, the results look like this:

Modified

As you can see, our algorithm is by far the fastest of all the options. The average runtime for the generic algorithm on my system is 220ms, against 285ms for the next best – that’s a 1.3x speed-up.

Not only that, but we’ve done it with just seven statements (using the metric from the original post), as opposed to 21 for the low-level version. 1.3x the performance with 1/3rd of the code? I’ll take that any day of the week. That’s the power of the STL.

Discussion

In the original post, Josh presents the “conventional wisdom” as being the following:

You shouldn’t roll your own code. The standard library was written by gurus and mere mortals won’t do better. Even if you do, you’ll write bugs, and you’ll end up spending more time fixing them than it’s worth.

This is all true, but I would put it slightly differently: I would say that if a standard library tool exists, then use it, but make sure you pick the right tool for the job.

In this case, despite the prevalence of suggestions on the internet, std::stringstream is the wrong tool to use for string splitting. The right tool is std::find_first_of. Once we use that, then not only do we get simple code, but it turns out to be much faster too.

The beauty of the STL is that it provides composable, low-level algorithms from which you can build up complex behavior. Sean Parent makes this argument far better than me; in his fantastic C++ Seasoning talk at Going Native 2013, he shows how you can use the STL algorithms in places where it’s not obvious. The video is very highly recommended.

Sean advocates that every C++ programmer should share at least one useful algorithm publicly per year. Hopefully I’ve now met my quota for 2016.

MySQL自治平台建设的内核原理及实践(下)

本文整理自主题分享《美团数据库自治服务平台建设》,系超大规模数据库集群保稳系列的第四篇文章。本文作者在演讲后根据同学们的反馈,补充了很多技术细节,跟演讲(视频)相比,内容更加丰富。文章分成上、下两篇,上篇将介绍数据库的异常发现跟诊断方面的内容,下篇将介绍内核可观测性建设、全量SQL、异常处理以及索引优化建议与SQL治理方面的内容。希望能够对大家有所帮助或启发。
❌