传统上,C++一直被许多人(你知道自己是谁)视为糟糕透顶:代码冗长得无法阅读,错误消息无法破译,它不安全,编译耗时很长,等等。事实上,对一些人来说,取笑C++甚至是一种有趣的消遣。所有这一切在90年代肯定是正确的,甚至在10年前也是如此。但情况还是这样吗?确定这一点的好方法是什么?
让我们写一个相当简单的程序来解决某种现实世界的问题,看看会有什么结果。我们要解决的问题是:
在当前目录的子目录中递归查找扩展名为.txt的所有文件,统计每个单词在其中出现的次数,并打印最常用的十个单词及其使用次数。
我们不会追求极端的性能或处理所有可能的角落情况,而是追求合理的实现。点击此链接可以找到完整的代码。
我们首先需要一种方法来检测具有给定扩展名的文件和文本中的单词。这需要正则表达式:
这可能有点冗长,但可读性很好。这只适用于ASCII文本,但对于本实验而言,这就足够了。要计算每个单词出现的次数,我们需要一个哈希表:
这有点冗长,但它负责对同一输入字符串多次运行正则表达式所需的所有状态跟踪。手工执行相同的操作相当繁琐且容易出错。现在我们有了匹配项,我们需要将它们转换为独立的小写单词。
诚然,小写字符串有点麻烦,但这至少是相当可读的。然后继续递增字数。
这就是计算单词所需的全部内容。这不能直接在散列映射中完成,所以我们需要将数据转换为一个AN数组。它需要一些设置代码。
由于我们知道数组中将有多少条目,因此可以提前为其预留足够的空间。然后,我们需要迭代所有条目,并将它们添加到数组中。
这使用了新的结构化绑定特性,这比处理迭代器对象或对的第一次和第二次以及所有这些恐怖的旧方式可读性强得多。幸运的是没有了。
获取10个最常用条目的简单方法是对数组进行排序,然后获取前10个元素。让我们做些稍微夸张一点的事吧[0]。我们将进行部分排序,并丢弃10之后的所有条目。为此,我们需要一个降序排序标准作为lambda。
Auto count_order_desc=[](const wordcount&;w1,const wordcount&;w2){return w2.count<;w1.count;};
由于安全和安保是当前软件开发中的重要功能,让我们来检查一下该程序的安全性。有两个主要的安全点:线程安全和内存安全。因为这个程序只有一个线程,所以可以正式证明它是线程安全的。内存安全还分为两个主要注意事项:在释放(或悬空引用)之后使用和越界数组访问。
在这个特定的程序中,没有悬而未决的引用。所有数据都是值类型,唯一的引用是迭代器。它们都有作用域,所以它们的寿命永远不会超过它们的用法。大多数甚至没有存储到命名变量中,而是直接作为函数参数传递(特别请参阅对PARTIAL_SORT的调用)。因此,它们在过期后无法访问。没有任何类型的手动资源管理。所有资源都会自动释放。在Valgrind下运行代码会产生零错误和零内存泄漏。
在代码中有一个地方可以进行越界访问:当创建指向数组开头之后的10个元素的迭代器时[2]。如果您忘记检查数组是否至少有10个元素,这将是一个直接的错误。幸运的是,大多数标准库都有一种方法来启用对此的检查。如果你弄错了,这里是GCC在运行时报告的内容:
错误:试图将可取消引用(序列开始)迭代器前进100000步,这超出了其有效范围。
这并不是一个完美的安全记录,但总体来说,它相当不错,而且肯定比通过手动索引数组来实现同样的记录要好得多。
编译程序需要几秒钟,这不是很好。这在很大程度上是由于regex标头,众所周知它是重量级的。在开发过程中遇到的一些编译器消息是不必要的冗长。不过,实际的错误相当愚蠢,比如以错误的顺序将参数传递给函数。这绝对可以更好。据报道,C++20中概念的使用将解决这个问题的大部分,但目前还没有实际的使用报告。
此代码仅使用操作系统供应商提供的默认工具链在所有主要平台上编译、运行和工作[1],没有任何外部依赖或下载。这是到目前为止没有其他编程语言可以提供的功能。您能得到的最接近的是纯C语言,但是它的标准库没有必要的功能。编译代码也很简单,只需一条命令即可完成:
如果我们试着用客观的眼光来看待最终的结果,这似乎是相当好的。整个实现只需不到60行代码。没有什么可怕的东西会立即冒出来。每一步发生的事情都是相当容易理解和阅读的。按照传统,没有什么东西会让人立即因为自己的可怕而憎恨和鄙视。
这并不是说你仍然可以不讨厌C++。如果你愿意,那就去吧。这当然是有原因的。但如果你选择这样做,确保你讨厌它是出于今天真正的实际原因,而不是几十年前损坏但后来被修复的东西。
[0]还有一种使用不需要中间数组的优先级队列的更有效的解决方案。将其实现留给读者作为练习。
[1]读者请注意,我只是证明了这段代码是可移植的,还没有试过。
[2][0]中的实现没有这个问题,因为没有任何迭代器是手动修改的。