颜林林的个人网站

从C++语法特性检查想到的

2024-01-02 03:14
题图
(题图由AI生成)

写在前面:有的观点,可能会被我有意无意地藏起来,所以,如果你对我又有啥不靠谱的新想法感兴趣,建议不要被标题所指内容吓退,万一我有时文不对题呢?

如之前在《当前最新C++标准是哪一版?》一文中提到,“我准备继续开始更新这停了很久很久的C++相关系列”。为此,我开始花时间研究C++标准相关的一些网站,以便为“现学现卖”提供素材。这篇,就谈谈新学到的“feature testing”(我姑且将其翻译为“语法特性检查”),主要起源于这里:https://en.cppreference.com/w/cpp/feature_test

  1. 关于预编译过程

在《软件程序结构演变不完全简史》中,我曾说过自己“特立独行”的计算机语言学习路径:BASIC、汇编、PASCAL、C、C++……

是的,在用解释型语言BASIC入门编程后,我首先学了低级语言汇编,然后才转向学习的高级语言PASCAL和C/C++。汇编语言与(可在电脑上直接运行的二进制)机器语言之间,几乎是一一对应的关系,这就意味着,编写汇编程序时,我几乎总是需要站在机器执行的角度来考虑问题,这培养了我至今仍难以完全摒弃的对C/C++的偏爱。

因此,我在汇编语言实践中,所用到的预编译、编译、链接等过程及概念,自然而然地应用到了C和C++中,这使我当年在学习这两门被誉为“学习曲线最陡峭”的语言时,并没有感到特别困难,一股脑地就接受并“炼成”了。

然而,现在回想起来,“预编译”这个概念实际上是颇为奇怪的。编译和链接的差别很容易理解,编译是把某种能被人类理解的计算机语言,转换成机器指令模块的过程,而链接则是把这些模块串联起来,按操作系统规定的格式,组装成可执行文件,以便系统加载和运行。编译和链接分离成两个独立步骤,使得一个软件程序(这里指最终发布的单一可执行程序)可以由不同语言混合开发而成。

然而预编译与编译之间的界限,就相对不那么清晰了。从结果上看,预编译实际上是在操作源代码文件,生成一个被展开后的新源代码文件(包括展开各种宏、将include文件内容插入进来等)。这个新的源代码文件再继续被送给编译器进行编译。

事实上,预编译器的这些功能,编译器应该也都能实现。时至今日,我个人认为,预编译过程更像是历史问题造成的结果。在软件行业早期发展阶段,由于当时的技术限制,独立的预编译过程在实现上应该确实更简单些。而现在,即便想将“预编译”与“编译”合并,可能也需要考虑到旧程序的兼容性问题,于是只能继续沿用了。

  1. 关于预编译指令

在C/C++中,预编译指令随处可见。最著名的当属“#include”,任何函数在使用前,都几乎必须提前使用该语句包含相应的头文件进来才行。

一旦遇到十几甚至更多条include,繁琐的堆砌,很容易导致重复包含。于是,如下这种(出现在头文件中的)保护性定义便应运而生:

Image

这便是预编译的作用(之一)。

此外,在某些需要同时提供给C和C++使用的头文件中,经常还会有这样的写法:

Image

这里的“__cplusplus”是一个由编译器内置定义的宏,帮助头文件判断,自己究竟是正在被一个C程序包含使用,还是正在被一个C++程序包含使用。对于后者,需要包上一层“extern “C” { … }”的写法,才能确保相应的各种符号,最终在链接阶段能够被正确识别和对接。

再往后,C++推出不同版本的新标准后,需要判断的,就不仅仅是有没有“__cplusplus”了,许多程序还会判断这个宏的取值(其取值是一个长整数,能代表当前编译过程遵循的C++标准版本),用以确定某些C++特性是否可用。

  1. C++标准发布节奏的慢与快

之前就已提到,在经历过漫长的拖延,于千呼万唤中,C++11标准于2011年终于推出,为避免后续再重蹈覆辙,C++委员会决定一改往日作风,将新标准的发布,改为固定的每三年发布一次新版本。

这样的“频繁”发布,导致各编译器厂商在跟进实现相应标准上,也不得不卖力追赶。为了尽量如期发布新版本,以满足并支持正式发布的标准,很多编译器其实都会在标准正式发布前,提前进行相应(草案提及的)特性的实现。这个过程也并非没有好处,它能提前对相应实现进行测试,甚至能通过实际使用,来反过来促成或阻止一个特征是否被正式加入后续的某个版本的标准中。

以GCC为例(见如下截图,来自https://gcc.gnu.org/projects/cxx-status.html),就能看出,当前最新版的GCC中,已经把一部分C++26中(很可能会包含的)特性给加上了。

Image

由于C++新标准的内容(包括尚未正式发布的各种提案)非常繁杂,很多新特性在细节方面也和可能会有不同的版本和理解,这导致了各个编译器之间的实现差异,以及它们不同版本中对各特性的实现完成情况的差别。

于是,单纯靠一个__cplusplus宏(或其他编译器版本标识)就已经不足以进行详细的区分了。为此,从C++11起,标准就要求编译器提供一系列的preprocessor macros(预处理宏)来帮助用户进行相应判断。在C++20中,进一步明确了对 __has_cpp_attribute() 宏的要求和规范,来确保相应判断的准确。

在上面提到的CppReference.com的链接(https://en.cppreference.com/w/cpp/feature_test)中,页面接近底部处,还提供了一段可以运行在C++11及以上编译器的代码,用于将所有特性检查的宏及其取值都一一列出。该页面甚至通过在线编译器Coliru(https://coliru.stacked-crooked.com/)的加持,允许学习者在线测试不同版本的主流编译器,在页面上就给出相应的运行结果(相比二十多年前,现在的学习环境真是太优越了)。

Image

  1. 其他展望

读过《Effective C++》的人应该都了解:C++是一个相当复杂和庞大的语言联邦。而C++新标准的快速迭代,各主流编译器的快速跟进(甚至是提前实现),使得全世界的C++使用者面临着一个更加复杂、需要在每个方面都小心追溯和确认的“窘境”。所幸,对于追求技术极致的人们来说,这点负担恐怕并不算什么。

前段时间看过一个有趣的说法,回答这个问题的:为什么在C/C++中,大家都喜欢自己造轮子,而其他语言并不存在这个问题?该回答是,在其他语言中,用户自己造轮子,是不可能超越语言自己提供的标准库的,因为这些标准库都是用C/C++写成;但在C/C++中,自己造的轮子,超越标准库是有可能的,因为标准库也是用C/C++写成,大家的起跑线相同;更重要的是,作为标准库,需要适应各种复杂使用场景,需要具备广泛的支持能力,这成为了沉重的负担,而自己造的轮子,通常只需要应对自己面临的简单环境即可。我想,这就是C/C++永远值得追求的地方,因为最大限度地保留了自己造轮子(且造出“完美”轮子)的可能性和权利。

最后,再往远处扩展说一点:我在医药相关领域,见到大量的知识(如肿瘤NCCN指南等),正在以前所未见的加速度发展,但可惜的是,这些知识的梳理过程,很多还停留在使用相对“原始”的操作流程。我自己经历过软件开发行业的发展,见过关于源代码版本管理和软件研发管理的许多先进方法,最近还在实践和挖掘包括ChatGPT在内的各种AI技术的应用。我毫不怀疑,这些方法如果能有效应用到医药领域来,会对今天爆炸式知识的管理和应用,带来巨大的价值。我自己肯定会往这个方向努力,并会尽可能开放地公开相关信息(包括我自己的理解、思考、进展等),也期望有更多的人比我走得更快更远。因为无论是谁做出贡献,无论贡献多少,获益都将会是所有人。这是个值得期待的良性的未来。

--- END ---

注:本文首发表于“不靠谱颜论”公众号,并同步至本站。

相关文章