技术附录
如果你在 2024 年之后很久才阅读本书,这份技术附录应当已经与你无关:届时理应能使用毫无缺漏地实现 C23 的开发框架。否则,某些 C23 特性尚未实现,可能造成困难。cppreference 的编译器支持页面会追踪多套工具链对 C23 的符合进度。
作者的 C 编程经验几乎全部来自开源项目和开源编译器(GCC 与 Clang),并集中在特定操作系统上,也就是以 glibc/Linux 和 musl/Linux 形态呈现的 POSIX(2024)。如果环境与此不同,而且运行本书代码时遇到严重问题,就需要自行在网上寻找解决办法;作者以及本附录余下内容恐怕帮不上太多忙。
A. 过渡代码
C23 带来大量新特性,最初不能指望所有编译器都迅速跟进,完整实现每项内容。不过,其中许多彼此密切相关的新特性已经以扩展形式存在于编译器中。
因此,本书提供的示例代码包含头文件 <c23-fallback.h>,旨在绕开可能遇到的大多数困难。所有示例都会系统地包含它,所以一般而言,即使在旧环境中也应当能够编译。
尽管如此,仍然要求环境支持以下 C23 特性:
- 数字分隔符,例如
0xAB'CD; - 二进制整数字面量,例如
0b1010或0B0101; - 新属性语法,例如
[[deprecated]]。
前两项不容易绕开,所以没有相应支持时,或许根本不应尝试使用面向 C23 的代码。
属性语法拥有特性检验 __has_c_attribute。可以把它与 defined(或 #ifdef)结合,检验语法本身;也可以通过 #if 和实参检验单项属性:
#ifndef __has_c_attribute
#define __has_c_attribute(X) 0
#endif2
3
另一项特性检验是 __has_include,同样可以先检验预处理器是否提供该特性,再检验特定头文件是否可用。它可以用于:
- 检验
<complex.h>、<threads.h>和<stdatomic.h>等可选头文件。原则上,这使我们能够借助相应特性检验宏__STDC_NO_COMPLEX__、__STDC_NO_ATOMICS__和__STDC_NO_THREADS__,把编译器对特性的支持情况与库接口是否可用分开检验; - 检验 C23 新增的
<stdckdint.h>和<stdbit.h>:
#ifdef __has_include
#if __has_include(<stdckdint.h>)
#include <stdckdint.h>
#endif
#endif2
3
4
5
后备头文件始终按这种方式使用该特性,并保证每次检验具体文件时,外层都有对特性本身的检验。许多早于 C23 的编译器已经提供 __has_include,所以它不会造成太大问题。
该头文件还无条件包含一批 C 库头文件,并尽可能为它们补充 C23 特性。不要自行包含这些头文件,以便确定最终取得的内容;这也包括前面见过的新 C23 头文件 <stdckdint.h>。为了模拟缺失特性,还可能按条件包含 <threads.h> 和 <stdatomic.h> 等其他头文件。
模拟新特性时,编译器可能给出虚假警告,说某些属性位置错误或已被忽略。这显然意味着属性并未得到完整考虑,预期分析也尚未由编译器完整实现。如果此类诊断铺天盖地,可以定义宏 C23_FALLBACK_SILENT,关闭其中一部分,例如向编译器提供命令行实参 -DC23_FALLBACK_SILENT。
不要忘记,该头文件只是一项过渡设施,不能指望它永远有效。
要点 A-1
只在过渡阶段使用头文件 c23-fallback.h,直到平台完整支持 C23。
要点 A-2
头文件 c23-fallback.h 只能以受限能力模拟部分 C23 特性。
所以,起步阶段使用该头文件可能是好主意;编译器完整支持 C23 后,就应丢掉这根拐杖。
该头文件还提供一些特性,尤其是特性检验。要检验新的 __VA_OPT__ 特性,可以使用:
__has_va_opt
对 __VA_OPT__ 特性进行预处理检验。C23 以前的编译器可能没有实现 __VA_OPT__。该宏应当始终求值为 0 或 1。
Clang 提供一组相当便利的特性检验:
__has_feature:检验是否实现某项特性;__has_extension:检验是否以扩展形式实现某项特性;__is_identifier:检验某个词是标识符还是关键字。
头文件还以 C23 扩展形式提供了一批较小的特性:
iscomplex
isimaginary
isdecimalfloating
isstandardrealfloating
isstandardfloating
isfloating
iscompatible
is_potentially_negative
is_const_target
is_volatile_target
is_const
is_volatile
is_null_pointer_constant
is_zero_ice
isinteger
issigned
isunsigned
isice
isvla
isxwide
is_pointer_nvla
is_pointer_vla
is_pointer
is_array
is_fla
is_void_pointer2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
如果对此感兴趣,请查看后备头文件源代码,了解它们的用途与实现方式。
B. C 编译器
写作本书时(2024 年 3 月),最新版 GCC 和 Clang 已经实现 C23 的大多数新语言特性,但仍缺少一些重要内容:Clang 18 缺少 constexpr 存储类说明符;Clang 18 与 GCC 14 都缺少 [[unsequenced]] 和 [[reproducible]] 属性。
较旧的编译器同样可以处理大部分内容,但版本越旧,问题越多,尤其是需要使用 128 位类型时。从 GCC 10 和 Clang 14 开始,对 C23 的支持似乎已经较为合理。
按照说明使用后备头文件时,本书代码应当能够编译。无论如何,最好始终使用平台所支持的最新编译器版本。它不仅会更好地实现本书为支持 C23 所需的 C 标准内容,也会提供更好的优化,并更充分地利用现代硬件。
要点 B-1
使用最新的编译器版本。
B.1 属性
对于 [[unsequenced]] 和 [[reproducible]] 属性,两款编译器都实现了与之相近的扩展 __gnu__::__const__ 和 __gnu__::__pure__。可以通过 GCC 过去使用的属性扩展语法 __attribute__((__const__)) 和 __attribute__((__pure__)) 使用,也可以采用带 gnu:: 前缀的新 C23 语法。
后备头文件没有在原位置直接替换它们,因为扩展与 C 标准属性的语法位置不同:标准属性施用于类型,扩展则施用于声明。不过,头文件提供 c23_unsequenced 和 c23_reproducible,作为这些扩展的快捷形式。
B.2 缺少 #embed
遗憾的是,截至 2024 年 3 月,现有编译器似乎尚未实现 #embed。若想提前试验,可以较为方便地使用 Cedro 项目模拟它。
示例代码目录附带的 Makefile 给出了怎样利用该程序提供 #embed 功能的示例。
B.3 缺少 constexpr
对于 Clang 整数类型缺少 constexpr 的问题,可以利用它对整数常量表达式更宽泛的定义加以替代。Clang 会把受 const 限定、具有编译时初始化器的对象名称也接受为整数常量表达式。因此,对大量应用而言,基本只需针对该编译器采用以下变通手段:
#if __is_identifier(constexpr)
#ifndef C23_FALLBACK_SILENT
#warning "constexpr keyword is not supported, emulating as static const"
#endif
#define constexpr static const
#endif2
3
4
5
6
__is_identifier 是一项便利的 Clang 扩展,用于判断其实参——这里是 constexpr——属于标识符(结果为 1)还是关键字(结果为 0)。Clang 准备好支持该关键字并返回 0 后——很可能出现在第 18 或第 19 版——这段代码会自动忽略。头文件还提供代码,为其他编译器模拟 __is_identifier。感兴趣时,可以查看后备头文件。
B.4 缺少 128 位整数支持
即使平台硬件类型只有 64 位,GCC 也早已部分支持 128 位整数类型。C23 以前,它们无法纳入标准支持的整数类型,因为其宽度超过早先选定的 [u]intmax_t 类型。
如果满足全部前置条件,C23 会把这些类型公开为 [u]int128_t:
<stdint.h>中的宏与类型;<inttypes.h>中的宏;printf和scanf函数对"%w"、"%wf"长度说明符的支持。
显然,后两项缺失时无法通过后备头文件修复;但在可能范围内,头文件会尝试提供第一组类型和宏。库支持另见下文。
这里有一项必须注意的特殊警告。
要点 B.4-1
Clang 18 以前的版本会禁用 [u]int128_t 支持。
原因是旧版 Clang 存在两项严重不兼容,若与 GCC 编译的代码混用会相当危险:
- 在某些平台上,
[u]int128_t类型的对齐不符合平台 ABI; - 在某些平台上,传递
[u]int128_t形参时,可能把一半放入硬件寄存器,另一半放入栈。
因此,如果想要或必须使用 128 位整数类型,至少应当升级到 Clang 18;你应当不会后悔。
C. C 库
并不令人意外,目前的 C 库实现缺少 C23 的大部分支持。下面讨论 C23 带来的一些新增内容。最后的第 C.7 节会介绍一个面向 Linux 系统的项目,在 C 实现完整支持 C23 库部分之前,可以临时使用它。
C.1 从 POSIX 或类似系统借用的函数
以下函数已与 POSIX 协调一致:
strftime;gmtime_r;localtime_r;memccpy;strdup;strndup。
因此,如果使用 Linux 或 macOS 等此类系统,或者在兼容环境中编程,应当已经拥有这些函数。timegm 也是很可能早已存在于这些系统上的另一个候选函数。
C.2 改进 UTF-8 支持
C23 提供新函数 mbrtoc8 和 c8rtomb,作用与 mbrtoc32 和 c32rtomb 等函数相似,只是采用 UTF-8 而不是 UTF-32。
对于经验较为丰富的读者,实现这些接口实际上是一项很好的练习。UTF-8 是一套精巧而出色的编码方案,能让你学到许多知识;从编程难度和理解国际标准两方面看,正确实现这些函数也具有恰当的挑战程度。
C.3 位工具
新头文件 <stdbit.h> 为无符号整数类型上的位操作提供大量接口(见第 8.2 节)。其中大多数还提供相当容易使用和接入的类型泛型版本。
部分相同或相似函数其实已经作为内建功能存在于主流编译器实现中,所以只要能够轻松做到,后备头文件就会提供它们。后备头文件提供的是类型泛型接口。
glibc 2.39 已经提供完整实现。
C.4 经检查的整数算术
C23 头文件 <stdckdint.h> 为经检查的算术提供三个类型泛型宏(见第 8.2 节)。许多编译器已经提供等价工具,所以后备头文件会在已知可行的环境中公开它们。
C.5 格式化输入输出
printf 和 scanf 函数族增加对 w 与 wf 宽度说明符,以及 b 与 B 二进制数格式的支持。
glibc 2.39 已经提供支持标准整数类型的完整实现。
扩展类型——尤其是 128 位整数类型——的支持仍然缺失。库不仅无法输入或输出这些类型,编译器看到带 "%w128" 或 "%wf128" 说明符的格式字符串时,还会发出令人困惑的警告。因此,即使使用正确支持这些类型的 C 库,也可能淹没在格式警告之中。
C.6 数学函数
C23 在 <math.h> 中增加了许多函数,其中一部分可能只对较小的专业群体有意义。各发行版恐怕还需要一段时间才能提供全部完整实现。
CORE-MATH 项目提供了一部分缺失特性,尤其是“pi”函数。这些三角函数及其反函数以半周为单位计算实参和返回值,范围为
C.7 面向 musl libc 的参考实现
对 Linux 使用者而言,代码目录还包含脚本文件 build-musl。足够勇敢的读者可以用它编译自己的 musl libc 版本,其中支持 C23 的大部分内容。脚本开头有简短说明,并提供从何处下载这一升级版 musl 源代码的信息。
这组补丁尤其会在拥有 GCC __int128 扩展的体系结构上,为 [u]int128_t 类型增加完整支持,包括对齐与调用约定。如果迫切想按照 C23 设想使用这些类型,它正适合你。
不过,这些补丁很可能尚未经过充分测试,可能仍有缺陷。完成妥善评审后,希望其中大部分能够合入 musl 主线。