
CppCoreGuidelines 深度解析:C 核心准则的规则体系、Profiles 强制机制与 GSL 支持库【免费下载链接】CppCoreGuidelinesThe C Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about coding in C项目地址: https://gitcode.com/gh_mirrors/cp/CppCoreGuidelinesCppCoreGuidelines.md 是整个 C Core Guidelines 项目的主文档,一个超过 2 万行、由 Bjarne Stroustrup 与 Herb Sutter 主编的活文档,定义了数百条可被静态分析工具机械检查的编码规则。本文以该文档为骨架,完整梳理它的章节体系、规则解剖结构、哲学层原则、三大安全 Profile 的强制机制与 GSL 支持库的全部组件,并结合仓库中的脚本、站点配置与配套教程,说明如何在真实项目中落地这套准则。1. 文档定位:一份可被工具执行的规范文档头部明确了自己的状态与许可(见 CppCoreGuidelines.md 开篇):这是一份持续改进的活文档,若按开源代码项目衡量相当于 0.8 版本,内容采用 MIT 风格许可授权复制、修改和衍生,贡献则需签署 Contributor License(细则见 LICENSE);当前版本日期为 2024 年 10 月 3 日。README.md 补充说明:该文档刻意使用简单、接近纯 ASCII 的 GitHub 风格 Markdown,以便自动后处理(翻译、重排);版本号的递增由编辑者在评审后提交,对应提交在 git 中以 tag 形式标记为 release。Abstract 一节(CppCoreGuidelines.md)给出了核心定位:目标是帮助开发者有效使用现代 C——当前指 C20,且几乎全部建议同样适用于 C17、C14、C11;规则聚焦于接口、资源管理、内存管理、并发等相对高层的问题,影响应用架构与库设计;遵循规则将得到静态类型安全、无资源泄漏的代码,并能捕获比当今常见代码多得多的逻辑错误,同时跑得足够快,你负担得起做对事的成本;规则被设计为供分析工具使用:违规处会被标记并附上指向对应规则的引用,这些准则可以被视为一份恰好人类可读的工具规范;规则支持渐进式引入到既有代码库,而不是一次性整体转换。1.1 目标读者与非目标In.target 的目标读者是所有 C 程序员,包括原本考虑用 C 的程序员(In.aims 还给出著名的心法 In.0:Dont panic! 先花时间理解规则对自身程序的影响)。In.not(Non-aims,CppCoreGuidelines.md)用近十段话划清了边界,值得完整理解:规则不追求最小化或正交性;通用规则可能简单但难以强制执行,专门规则易理解易执行但脱离通用规则就只是特例清单;规则不是给人逐章读完的书,主要用途是作为工具的靶子:工具查找违规并返回链接,规则则提供理由、后果示例与补救建议;不是 C 教程的替代品,也不是旧代码转新代码的操作手册(现代化思路见附录 B,且规则本身支持渐进采用);不定义贫乏的 C 子集,更毫不含糊地不打算定义类似 Java 的子集,价值观上明确重视表达力与不受损的性能;规则不价值中立:它们有意抑制那些合法但统计上与错误、虚假复杂度、糟糕性能相关的 C 写法;规则也不假装完美,目标是为多数程序员做最大的好事;若某条规则让你无法接受,可以反对它、忽略它,但不要把它稀释到没有意义——并建议提出改进。1.2 文档结构:16 个主章节 13 个支撑章节In.sec 一节给出的完整目录(与 CppCoreGuidelines.md 顶部导航一致)是:主章节(缩写同时用于规则编号):缩写主题In引言 IntroductionP哲学 PhilosophyI接口 InterfacesF函数 FunctionsC类与类层次 Classes and class hierarchiesEnum枚举 EnumerationsR资源管理 Resource managementES表达式与语句 Expressions and statementsPer性能 PerformanceCP并发与并行 Concurrency and parallelismE错误处理 Error handlingCon常量与不可变性 Constants and immutabilityT模板与泛型编程 Templates and generic programmingCPLC 风格编程 C-style programmingSF源文件 Source filesSL标准库 The Standard Library支撑章节:缩写主题A架构思想 Architectural ideasNR非规则与迷思 Non-Rules and mythsRF参考资料 ReferencesPro安全档案 ProfilesGSL准则支持库 Guidelines support libraryNL命名与排版建议 Naming and layout suggestionsFAQ常见问题解答Appendix A库 LibrariesAppendix B代码现代化 Modernizing codeAppendix C讨论 DiscussionAppendix D辅助工具 Supporting toolsGlossary术语表To-do未分类的原型规则 Unclassified proto-rules文档还特别标注:这些章节并不正交;每个章节与子章节都有缩写便于搜索(如 C.hier 表示类层次),主章节缩写直接进入规则编号(如 C.11 Make concrete types regular)。2. 规则的解剖:每条准则的标准构成In.struct 一节(CppCoreGuidelines.md)规定了每条规则可包含的组成部分:规则本身—— 如 no nakednew;规则引用编号—— 如 C.7(与类相关的第 7 条规则);由于主章节本身无固有顺序,引用号以字母开头;编号中故意留有空隙,以便增删规则时减少扰动;Reason(理由)—— 因为程序员很难遵循他们不理解的东西;Example(示例)—— 因为规则在抽象层面难以理解;可以是正面或反例;Alternative(替代方案)—— 针对不要做 X类规则;Exception(例外)—— 规则大多适用但非普遍适用,例外必须列出;Enforcement(强制手段)—— 如何机械地检查该规则;See also(参见)—— 相关规则与进一步讨论;Note(备注)—— 不属于上述分类但需要说的话;Discussion(讨论)—— 放在规则主列表之外的扩展论证与示例。关于编号空隙的设计意图,文档开头 Problems 一节也说明:规则集合尚未完成完整性、一致性、可执行性的全面检查,三问号???标记已知缺失信息,未分类的原型规则集中在 To-do: Unclassified proto-rules(位于 CppCoreGuidelines.md 附近)中——读者若发现某条规则缺少讨论,被明确鼓励通过仓库 Issue/PR 补全。文档对示例的书写约定同样值得注意:示例不为生产质量负责,大量使用f、base、x这类名字;好示例应尽量自身符合 Core Guidelines;注释常用来标注规则(真实代码中会显得多余);并且假定读者熟悉标准库,例如直接使用vector而非std::vector。3. 哲学层(P 章):13 条统领全局的原则P 章(CppCoreGuidelines.md)是全部具体规则的根基。13 条规则为:P.1 在代码中直接表达想法:编译器(和许多程序员)不读注释;有明确语义的代码可被工具检查;P.2 用 ISO 标准 C 编写:扩展(如访问系统资源)应局部化、封装、可编译移除;标准 C 不保证可移植性,须避开未定义行为;P.3 表达意图:说要做什么,而不是只写怎么做的;P.4 理想情况下程序应静态类型安全:承认联合体、转换、数组退化、范围错误、缩窄转换是无法彻底静态保证的问题区,并逐一给出替代技术;P.5 优先编译期检查,而非运行期检查;P.6 编译期检查不了的,应可在运行期检查;P.7 尽早捕获运行期错误;P.8 不泄漏任何资源;P.9 不浪费时间和空间;P.10 优先不可变数据而非可变数据;P.11 封装混乱结构,而不是让它们散落代码各处;P.12 酌情使用辅助工具;P.13 酌情使用支持库。文档强调:哲学规则本身通常无法机械检查,但体现这些哲学主题的具体规则可以;没有哲学基础,具体/可检查的规则就缺少理由支撑。下面用文档中的代表性示例说明其论证方式。3.1 P.1 示例:用类型表达语义class Date { public: Month month() const; // do int month(); // dont // ... };第一个声明明确返回Month且不修改对象状态;第二个版本让读者猜测,并留下更多未被捕获的 bug 空间。同类思想还体现在参数上:change_speed(double s); // bad: what does s signify? change_speed(Speed s); // better: the meaning of s is specified change_speed(23_m / 10s); // meters per second3.2 P.3 示例:从索引循环到算法表达文档指出while (i v.size())形式的循环暴露了实现细节、i的生命周期超出循环作用域,更好的表达依次是 range-for,再到命名算法:for (const auto x : v) { /* do something with the value of x */ } for (auto x : v) { /* modify x */ } for_each(v, [](int x) { /* do something with the value of x */ }); for_each(par, v, [](int x) { /* do something with the value of x */ });最后一种用并行执行策略par明确表达了不关心元素处理顺序的意图。3.3 P.5 示例:把运行期检查挪到编译期// 反例:运行期循环数位数,且因溢出未定义而根本达不到目的 int bits 0; for (Int i 1; i; i 1) bits; if (bits 32) cerr Int too small\n; // 正例 static_assert(sizeof(Int) 4); // compile-time check // 或直接用类型系统:以 int32_t 取代 Int范围问题的标准答案是span:void read(int* p, int n); // 读最多 n 个整数到 *p int a[100]; read(a, 1000); // bad, off the end void read(spanint r); // 读入整数序列 r int a[100]; read(a); // better: 让编译器推得元素个数3.4 P.6 示例:指针 数量接口的四代演进这是文档中论证信息必须作为整体对象传递的核心例子。f(int* p)完全不传元素个数,静态分析失效、动态检查困难;f2(int* p, int n)稍好但一次手误(g2里把n写成m)即可引入严重错误,且谁负责delete是隐式的;即便f3(unique_ptrint[], int n)也只是把所有权和大小分开传。文档给出的正解是把指针与数量绑定为一个对象:extern void f4(vectorint); // 传引用,保留所有权 extern void f4(spanint); // 传视图,保留所有权 void g3(int n) { vectorint v(n); f4(v); // pass a reference, retain ownership f4(spanint{v}); // pass a view, retain ownership }并顺带区分了转移所有权的三种方式:返回vectorint(好,可移动)、返回unique_ptrint[](坏,丢失 n)、返回ownerint*(坏,丢失 n 且可能忘记delete)。3.5 P.7 与 P.8 示例:尽早检查与 RAIIP.7 用increment1(int* p, int n)说明:(指针, 数量)接口让函数无法自我防御,越界要到p[10]被访问才暴露;换成increment2(spanint p)后,m n可以在调用点(早)被检查,而若意图本就是用整个数组,increment2(a)直接消除了错误的可能。P.8 则是经典的文件句柄泄漏示例:void f(char* name) { FILE* input fopen(name, r); // ... if (something) return; // bad: 文件句柄泄漏 // ... fclose(input); } // 优选 RAII: void f(char* name) { ifstream input {name}; // ... if (something) return; // OK: no leak // ... }文档同时给出一条重要限定:泄漏更精确的定义是再也无法清理的东西(如丢失了最后一个指针的堆对象),而非一切未清理的东西;依赖进程退出时的系统级清理(如关闭文件、释放内存)是可接受的简化,但依赖隐式清理的抽象往往同样简单且更安全。3.6 超集的子集原则In.aims 用 [Stroustrup05] 的 subset of superset 原则概括整体方法论:准则不是定义一个受限的 C 子集,而是强烈推荐使用少量简单扩展(即 GSL 库组件),让 C 中最易出错的特性变得冗余、从而可以在规则集中被禁用。规则因此强调静态类型安全与资源安全:范围检查、避免解引用nullptr、避免悬垂指针、通过 RAII 系统化地使用异常。4. 强制机制:Enforcement 与三大安全 Profile4.1 为什么需要 ProfileIn.force 一节(CppCoreGuidelines.md)列出了强制机制要解决的矛盾:没有强制的规则在大型代码库中不可管理,但可完全强制的只有很小很弱的规则集;人们不喜欢读很多规则、记不住很多规则、需求各异;而任意的规则子集化又会导致混乱。文档的解法是用工具 Profile(档案):每条规则都有一个Enforcement小节,列出可能的强制手段:代码评审、静态分析、编译器、运行期检查;尽可能优先机械检查(人慢、易错、易疲倦)与静态检查;运行期检查仅在别无选择时建议,避免引入distributed bloat;规则按相关度分组为Profile,一条规则可属于多个 Profile,也可不属于任何 Profile。Pro 章(CppCoreGuidelines.md)给出了 Profile 的严格定义:一组确定的、可移植强制的规则子集,旨在达成特定保证。确定意味着只需局部分析、可以由编译器实现;可移植强制意味着如同语言规则,不同强制工具对同一代码给出相同答案。用某个 Profile 做到零警告的代码即符合该 Profile,被视为构造上安全——就该 Profile 针对的安全性质而言,它不会成为错误的根源(尽管错误可能由其他代码、库或外部环境引入)。文档还解释了引入 Profile 的现实动因:大代码库常跨越数十年、不可能一步到位,必须先解决某一类问题(如无越界),其余留待以后;孤立地强制随机规则对代码库的破坏性往往大于收益。4.2 三个安全 Profile 的完整检查项type(类型安全)——防止把T通过转换、联合体、变参重解释为U。完整检查清单:Type.1: 避免转换 —— 禁用reinterpret_cast;算术类型禁用static_cast;禁止源类型与目标类型相同的指针转换;禁止本可隐式完成的指针类型转换;Type.2: 禁止用static_cast向下转换(应用dynamic_cast);Type.3: 禁止用const_cast去除const(即完全禁止);Type.4: 禁止 C 风格(T)expr与函数式T(expr)转换,优先构造、命名转换或T{expr};Type.5: 禁止使用未初始化变量;Type.6: 总是初始化数据成员(可用默认构造或默认成员初始化器);Type.7: 避免裸union(改用variant);Type.8: 避免va_arg变参。文档特别说明:类型安全只有与 Bounds、Lifetime 两个 Profile组合时才是完整的。bounds(边界安全)——限制指针只能指向单个对象而非数组,聚焦指针算术与数组下标两大越界来源:Bounds.1: 禁用指针算术,使用span;Bounds.2: 仅允许用常量表达式对数组取下标;Bounds.3: 禁止数组到指针的退化;Bounds.4: 禁用未做边界检查的标准库函数与类型。文档指出边界安全直接消除缓冲区溢出这类著名错误,既关闭安全漏洞也消除内存损坏来源;即便是只是读的越界,也会导致不变式违背与神秘值。lifetime(生命周期安全)——针对空指针、越界、指向已删除对象等指针:Lifetime.1: 禁止解引用可能无效的指针(检测或避免)。Pro.lifetime 一节给出完整强制后的收益:消除 C 最恶性的错误来源之一、消除主要安全违规来源、通过消除冗余偏执检查提升性能、增强对代码正确性的信心、通过强制关键语言规则避免未定义行为。其设计规格文档即仓库中的 docs/Lifetime.pdf,同目录还有 docs/Introduction to type and resource safety.pdf 与 docs/P0122R4.pdf 可作为背景资料。未来候选 Profile 包括:安全算术(缩窄提升/转换、负浮点到无符号整型转换)、选定未定义行为(Gabriel Dos Reis 为 WG21 研究组整理的 UB 清单)、选定未指定行为、const违规检测等。4.3 抑制规则检查的语法实现这些规则的工具必须尊重文档规定的显式抑制语法(来自 In.force):[[gsl::suppress(tag)]] // 可附带理由(遵循 C11 标准属性语法): [[gsl::suppress(tag, justification: message)]]其中tag可以是:规则出现处的锚点名(如 C.134 对应 Rh-public)、Profile 组名(type、bounds、lifetime),或 Profile 内的具体规则(如 type.4、bounds.2);其余文本应被工具拒绝。Pro 章给出了按 Profile 整体抑制的示例:[[suppress(bounds)]] char* raw_find(char* p, int n, char x) // find x in p[0]..p[n - 1] { // ... }文档同时警告:此时raw_find()可以为所欲为地破坏内存,显然,抑制应当非常罕见。Profile 的启用方式是实现定义的,通常在分析工具中配置。5. 主章节速览:规则体系的核心内容以下按文档章节顺序,摘取各章最具实践价值的规则(规则编号均与 CppCoreGuidelines.md 中锚点一致,可据此检索原文)。5.1 I:接口I 章共给出 I.1–I.13 与 I.22–I.30 等规则,核心主张包括:I.1 让接口显式;I.2 避免非const全局变量;I.3 避免单例;I.4 让接口精确且强类型;I.5/I.6 声明前置条件,优先用Expects()表达;I.7/I.8 声明后置条件,优先用Ensures()表达;I.9 模板接口用 concepts 描述参数;I.10 用异常表示无法完成必需任务的失败;I.11 绝不通过裸指针(T*)或引用(T)转移所有权;I.12 必须非空的指针声明为not_null;I.13 不要把数组当作单个指针传递;I.22 避免全局对象的复杂初始化;I.23 控制函数参数数量;I.24 避免两个相邻参数可被同一组实参以两种顺序调用却含义不同;I.25 类层次接口优先用空抽象类;I.26 需要跨编译器 ABI 时使用 C 风格子集;I.27 稳定库 ABI 考虑 Pimpl 惯用法;I.30 封装规则违规(把违反准则的代码隔离在可控边界内)。5.2 F:函数与参数传递F 章覆盖函数定义(F.1–F.11:把有意义的操作打包为命名函数、单一逻辑操作、短而简单、可编译期求值则用constexpr、noexcept、纯函数、未用参数不命名等)与参数传递(F.call,F.15–F.27、F.42–F.56)。F.15 是参数传递的总纲:优先简单而常规的传递方式。奇怪而聪明的技巧会制造惊喜、拖慢他人理解、诱发 bug;确需超越常规技巧的优化时,先测量、再注释说明。该节用两张表总结 F.16–F.21 的建议(正常参数传递表与进阶参数传递表),并规定进阶技巧只有在证明必要时才使用,并在注释中记录该必要:两张表对应的具体规则为:F.16 in 参数:可廉价复制的类型按值传,其余按const传。廉价取决于架构,通常两三个机器字(双精度、指针、引用)按值最佳;小对象按值传比引用更快(免去一次间接访问)。文档示例:void f1(const string s); // OK: const 引用;总是廉价 void f2(string s); // bad: 潜在昂贵 void f3(int x); // OK: 无可挑剔 void f4(const int x); // bad: f4() 内访问有额外开销 int multiply(int, int); // 纯输入 int,按值 string concatenate(string, const string suffix); void sink(unique_ptrwidget); // 输入并转移 widget 所有权注意避免以T传递参数以求效率这类玄学技巧;对 rvalue 的高级处理见 F.18/F.19。F.17 in-out 参数按非const引用传,明确告知调用方对象会被修改;但T同时可入可出,本身是错误源(s New York;式的意外覆盖);廉价可复制类型(如spanT、迭代器)可按值传递并保持可变引用语义;F.18 will-move-from 参数按X传并std::move;F.19 forward 参数按TP传且只std::forward;F.20 out 值优先返回值而非输出参数;F.21 返回多个 out 值优先返回结构体;F.22T*/ownerT*指向单个对象;F.23not_nullT表示 null 非法;F.24spanT/span_pT表示半开序列;F.25zstring/czstring表示 C 风格字符串;F.26unique_ptrT转移所有权;F.27shared_ptrT共享所有权;返回相关:F.42T*只用于表示位置;F.43 绝不返回指向局部对象的指针/引用;F.44 复制代价大且无需无对象时返回T;F.45 不要返回T;F.46main()返回int;F.48 不要return std::move(local);F.49 不要返回const T;Lambda 捕获三原则:F.52 局部使用的 lambda 优先按引用捕获;F.53 非局部使用(返回、堆上、跨线程)避免引用捕获;F.54 捕获this或成员时禁用[]默认捕获;以及F.55 禁用va_arg、F.56 避免不必要的条件嵌套。5.3 C:类与类层次C 章是篇幅最大的章节,关键规则脉络:基本设计:C.1 相关数据组织进struct/class;C.2有不变式用class,数据成员可独立变化用struct;C.3 用类表达接口/实现的区分;C.4 只有需要直接访问类表示的函数才做成员函数;C.8 存在非公有成员时用class;C.9 最小化成员暴露;具体类型:C.10 具体类型优先于类层次;C.11 让具体类型成为 regular 类型(复制、移动、默认操作齐全);C.12 可复制/可移动类型中不要使用const数据成员或引用成员;默认操作:C.20 能避免定义默认操作就避免;C.21 定义或delete了复制/移动/析构中的任何一个,就全部定义或delete;C.22 让默认操作保持一致;C.80 需要明确时用default;C.81 想禁用且无替代时用delete;析构:C.30 需要显式析构动作才定义析构函数;C.31 类获取的所有资源必须由析构函数释放;C.35 基类析构要么公有且虚,要么保护且非虚;C.36 析构函数不得失败;C.37 析构函数noexcept;构造:C.40 有不变式就有构造函数;C.41 构造出完全初始化的对象;C.42 无法构造合法对象就抛异常;C.43/44 可复制类要有默认构造,且默认构造宜简单、不抛;C.45 不为初始化成员而定义默认构造,改用默认成员初始化器;C.46 单参构造默认explicit;C.47 按声明顺序定义并初始化成员;C.49 初始化优先于赋值;C.50 需要初始化期的虚行为用工厂函数;C.51/52 委托构造与继承构造;复制与移动:C.60–C.65 复制/移动赋值的签名规范、自赋值安全、移动后源对象处于合法状态;C.66 移动操作noexcept;C.67 多态类应禁止公有复制/移动;C.83–C.85 值类型考虑提供noexcept swap,且 swap 不得失败;C.86对称且noexcept;C.89hashnoexcept;C.90 依赖构造函数与赋值运算符,而非memset/memcpy;容器:C.100 遵循 STL 约定定义容器;C.101 容器给值语义;C.102 给移动操作;C.103 给初始化列表构造;C.104 默认构造置空;层次(OOP):C.120 类层次只用于表达固有的层次概念;C.121 用作接口的基类做成纯抽象类;C.122 需要接口实现完全分离时用抽象类;C.127 有虚函数的类应有虚或保护析构;C.128 虚函数恰好写virtual、override、final之一;C.129 区分实现继承与接口继承;C.130 多态类的深拷贝优先虚clone;C.131 避免琐碎 getter/setter;C.133 避免protected数据;C.134 所有非const数据成员保持同一访问级别;C.135 多重继承用于表达多个不同接口;C.146–C.148dynamic_cast的两种用法(失败即错误用引用版,失败是合法分支用指针版);C.149/150unique_ptr/shared_ptr避免忘记delete,配make_unique/make_shared;C.153 优先虚函数而非转换;运算符重载:C.160 按惯例语义定义;C.161 对称运算符用非成员函数;C.164 避免隐式转换运算符;C.165using作定制点;C.167 运算符只用于其惯例含义;C.168 在操作数的命名空间内定义重载;联合体:C.180 用union省内存;C.181 避免裸union;C.182 匿名union实现带标签联合体;C.183 不用union做类型双关。5.4 Enum、R 与 ESEnum 章给出 8 条规则:Enum.1 优先枚举而非宏;Enum.2 用枚举表示一组相关命名常量;Enum.3 优先类枚举(enum class);Enum.4 为枚举定义安全简单的操作;Enum.5 枚举器不用ALL_CAPS;Enum.6 避免无名枚举;Enum.7/8 仅在必要时指定底层类型/枚举器值。R 章(资源管理)(CppCoreGuidelines.md 起)的核心:R.1 用资源句柄与 RAII 自动管理资源——全章的基石;R.2 接口中裸指针只表示单个对象;R.3 裸指针(T*)非拥有——其 Enforcement 讨论了裸指针在无法升级为资源句柄的场景(转换成本、ABI、资源句柄实现内部)如何用ownerT*标记;R.4 裸引用(T)非拥有;R.5 优先栈对象,勿滥用堆分配;R.6 避免非const全局;分配与释放:R.10 避免malloc/free;R.11 避免显式new/delete;R.12 显式分配的结果立即交给管理器对象;R.13 单条表达式语句至多一次显式分配;R.14 避免[]参数,优先span;R.15 成对重载匹配的分配/释放;智能指针:R.20unique_ptr/shared_ptr表示所有权;R.21 非确需共享所有权时unique_ptr优先于shared_ptr;R.22/R.23 用make_shared/make_unique;R.24 用std::weak_ptr打破shared_ptr环;智能指针参数语义全家桶:R.30 仅在需显式表达生命周期语义时才把智能指针当参数;R.31 非std智能指针遵循std基本模式;R.32 按unique_ptrwidget传参表示函数接管所有权;R.33 按unique_ptrwidget传参表示可能重新安放;R.34 按shared_ptrwidget传参表示共享所有权;R.35 按shared_ptrwidget传参表示可能重新安放;R.36const shared_ptrwidget表示可能保留引用计数;R.37 不传递从别名智能指针取得的指针/引用。**ES 章(表达式与语句)**规则密度极高,选摘与工程实践最相关的:ES.1 优先标准库;ES.2 优先合适抽象而非直接使用语言特性;ES.3 DRY;ES.5 缩小作用域;ES.6 在 for 的初始化与条件中声明以限定名字作用域;命名:ES.7 常用且局部的名字短,少用且非局部的名字长;ES.8 避免形近名;ES.9 避免ALL_CAPS;ES.10 每个声明只声明一个名字;ES.12 不在嵌套作用域复用名字;初始化与常量:ES.20 总是初始化对象;ES.21/22 需要前不引入变量、有值才声明变量;ES.23 优先{}初始化;ES.25 除非打算修改,否则声明为const/constexpr;ES.26 一个变量不用于两个不相关目的;容器与数组:ES.24 用unique_ptrT持有指针;ES.27 栈上数组用std::array/stack_array;宏:ES.30 不用宏操纵程序文本;ES.31 不用宏定义常量或函数;ES.32 必须用宏时名字ALL_CAPS;ES.33 宏名保证唯一;ES.34 不定义 C 风格变参函数;表达式安全:ES.40 避免复杂表达式;ES.41 优先级存疑就加括号;ES.42 指针用法保持简单直接;ES.43 避免求值顺序未定义的表达式;ES.44 不依赖函数参数求值顺序;ES.45 避免魔法常量;ES.46 避免有损(缩窄、截断)算术转换;ES.47 用nullptr而非0/NULL;ES.48 避免转换;ES.49 必须转换时用命名转换;ES.50 不转换掉const;语句:ES.55 尽量避免范围检查的需要;ES.56 需要显式跨作用域移动时才写std::move();ES.60new/delete只出现在资源管理函数内;ES.61delete[]配数组、delete配非数组;ES.62 不比较指向不同数组的指针;ES.63 不要切片(slicing);ES.64 构造用T{e};ES.65 不解引用无效指针;循环与控制流:ES.70 有选择时switch优先于if;ES.71 range-for 优先于 for;ES.72/73 for/while 按有无显然的循环变量取舍;ES.76 避免goto;ES.77 最小化break/continue;ES.78 不依赖 switch 隐式穿透;算术(ES.100–ES.107):不混用有符号/无符号;ES.101 位操作用无符号类型;ES.102 算术用有符号类型;不溢出、不下溢、不整型除零;不用unsigned规避负值;ES.107 下标不用unsigned,优先gsl::index。5.5 Per、CP、E、Con、T、CPL、SF、SLPer(性能):Per.1–Per.6 是六条不要——不要无理由优化、不要过早优化、不要优化非关键路径、不要假设复杂代码更快、不要假设底层代码更快、没有测量不要做性能论断;Per.7 设计以支持优化;Per.10 依赖静态类型系统;Per.11 把计算从运行期移到编译期;Per.12/13 消除冗余别名与冗余间接;Per.14–16 最小化分配、临界分支不分配、紧凑数据结构;Per.17–19 时间关键结构体首成员放最常用者、空间即时间、可预测地访问内存;Per.30 临界路径避免上下文切换;CP(并发):CP.1 假设代码运行于多线程程序;CP.2 避免数据竞争;CP.3 最小化可写数据显式共享;CP.4 以任务而非线程思考;CP.8 不用volatile同步;CP.20 用 RAII,绝不裸lock()/unlock();CP.21 多锁用std::lock/std::scoped_lock;CP.22 持锁绝不调用未知代码;CP.23/24 把 join 线程视作有作用域的容器、把线程视作全局容器;CP.25 优先gsl::joining_thread;CP.26 不detach();CP.42 无条件不wait;CP.43 最小化临界区;CP.44lock_guard/unique_lock要命名;CP.50 与被保护的数据一起定义mutex,可能时用synchronized_valueT;协程规则 CP.51–53(不用捕获型协程 lambda、不在挂起点持锁、协程参数不按引用传);CP.100 非万不得已不用无锁编程;CP.110 不自己写双重检查锁定;E(错误处理):E.1 设计早期确定错误处理策略;E.2 函数无法完成分派任务时抛异常;E.3 异常只用于错误处理;E.4/E.5 策略围绕不变式设计,构造失败即抛;E.6 用 RAII 防泄漏;E.7/E.8 声明前置/后置条件;E.12 退出时不可能或不可接受抛异常则noexcept;E.13 作为对象直接所有者时不抛;E.14 异常用专门设计的用户类型而非内建类型;E.15 值抛、引用捕;E.16 析构、释放、swap、异常类型复制/移动绝不失败;E.17 不在每个函数里捕获一切;E.18 最小化显式try/catch;E.19 无合适资源句柄时用final_action表达清理;**E.25–E.28 不能抛异常时的系统性替代:模拟 RAII、考虑快速失败、系统化使用错误码、避免基于全局状态(如errno)的错误处理**;E.30 不用异常规范;E.31catch子句顺序正确;Con(常量与不可变性):Con.1 对象默认不可变;Con.2 成员函数默认const;Con.3 默认传指向const的指针/引用;Con.4 值不变的对象用const定义;Con.5 可编译期计算的值用constexpr;T(模板与泛型):T.1–T.5 用模板提升抽象层次、表达算法/容器/范围/语法树操作,泛型与 OO 技术放大优势而非成本;concepts 规则簇(T.10 为所有模板参数指定 concept、T.11 优先标准 concept、T.20 避免无语义concept、T.21 要求完整的操作集、T.22 为 concept 指定公理、T.25 避免互补约束、T.26 用使用模式而非纯语法定义 concept);模板接口(T.40 用函数对象向算法传操作、T.41 concept 只要求必需性质、T.46 模板参数至少 semiregular、T.47 避免高可见度的无约束常见名模板);定义技巧(T.61 不要过度参数化成员(SCARY)、T.64/65/67 特化与标签分发、T.68 模板内用{}而非()避免歧义、T.69 模板内非限定非成员调用即视为定制点);层次与 ABI(T.80 不要天真地把类层次模板化、T.81 不混合层次与数组、T.84 非模板核心实现提供 ABI 稳定接口);模板元编程克制原则(T.120 只在确实需要时、T.122 类型计算用模板别名、T.123 值计算用constexpr函数);T.150 用static_assert检查类是否满足 concept;CPL(C 风格编程):CPL.1 能 C 就不 C;CPL.2 必须用 C 时用 C 与 C 的公共子集并把 C 代码当 C 编译;CPL.3 必须用 C 接口时,调用方代码用 C;SF(源文件):SF.1 代码.cpp、接口.h(无其他约定时);SF.2 头文件不得含对象定义或非inline函数定义;SF.3 多文件使用的声明放头文件;SF.4 include 先于其他声明;SF.5.cpp必须包含定义其接口的头;SF.6/7using namespace仅限过渡、基础库或局部作用域,头文件全局作用域禁止;SF.8 头文件加#include保护;SF.9 避免循环依赖;SF.10 避免依赖隐式包含的名字;SF.11 头文件自包含;SF.20 用命名空间表达逻辑结构;SF.22 内部实体用匿名命名空间;SL(标准库):SL.1 尽量用库;SL.2 标准库优先于其他库;SL.3 不向std命名空间添加非标准实体;SL.4 类型安全地使用标准库;容器(SL.con.1 优先array/vector而非 C 数组、SL.con.2 默认vector、SL.con.3 避免越界、SL.con.4 不可平凡复制的实参不用memset/memcpy);字符串(SL.str.1 用std::string拥有字符序列、SL.str.2 用std::string_view/spanchar指称、SL.str.3 C 风格串用zstring/czstring、SL.str.11 需要修改串时用spanchar而非string_view);I/O(SL.io.3 优先iostream、SL.io.50 避免endl);SL.C.1 不用setjmp/longjmp。支撑章节中,**A(架构)**给出 A.1 稳定代码与不稳定代码分离、A.2 可复用部分表达为库、A.4 库之间无环;**NR(非规则与迷思)**逐条否定七个常见教条,包括 NR.1 不坚持所有声明置于函数顶部、NR.2 不坚持单return、NR.3 不回避异常、NR.4 不坚持一个类一个文件、NR.5 不用两阶段初始化、NR.6 不搞统一goto exit清理、NR.7 不把所有数据成员设为protected;**附录 D(辅助工具)**点名 Clang-tidy 与 CppCoreCheck 两条工具路线。6. GSL:准则支持库的全景GSL 章(CppCoreGuidelines.md)规定了支持上述规则的小型库设施。文档明确指出:没有这些设施,准则只能对语言细节施加苛刻得多的限制。GSL 定义于命名空间gsl,其中名字可以是标准库或其他知名库名字的别名;通过编译期间接层,允许实验与本地变体;它是header-only,设计为零开销,需要时可用附加功能(如检查)做插桩用于调试。各组件及其文档给出的不变式/语义如下:视图(永远非拥有):T*—— 非拥有、可为 null、假设指向单个元素;T—— 非拥有,且不存在空引用;ownerT*—— 拥有所指对象的T*,可为nullptr;用于无法升级为资源句柄(转换成本、ABI、资源句柄实现内部)的代码,与资源句柄的区别是仍需显式delete,且假设指向堆对象;not_nullT——T通常是指针类型,不得为nullptr(凡nullptr有意义的类型均可);spanT—— 半开区间[p:pn),支持{p, q}与{p, n}构造;除非T为const,所指对象可变;GSL 的span与std::span接口一致,唯一差别是 GSL 版本默认保证边界检查,一旦std::span加入边界检查该类型即可退休;span_pT——{p, predicate},即[p:q),q为使predicate(*p)成立的首元素;zstring/czstring—— 假设是 C 风格字符串的char*/const char*(零终止或nullptr);不假设零终止的字符序列应表示为spanchar。所有权指针与容器:unique_ptrT、shared_ptrT(引用计数)、stack_arrayT(栈上数组,元素数构造时固定)、dyn_arrayT(不增长的动态分配数组,本质是会分配并拥有元素的span)。断言:Expects(前置条件断言)与Ensures(后置条件断言),Expects(p)在p ! true时终止程序,受选项控制(强制开关、错误消息、终止替代动作);当前为宏,未来应移到声明处,文档同时预告了契约提案中的属性语法[[expects: p]]。工具:finally(f)(构造析构时调用f的final_action)、narrow_castT(x)(即static_cast)、narrowT(x)(当static_castT(x) x且无符号提升时等价,否则抛narrowing_error,例如narrowunsigned(-42)抛异常)、[[implicit]](显式标记单参构造非 explicit)、move_owner、joining_thread(RAII 式自动 join 的std::thread)、index(一切容器与数组下标用此类型,当前为ptrdiff_t的别名)。concepts(类型谓词):借用自 Origin 库、Range 提案与 Palo Alto TR,与 C20 进入标准的部分高度对应:Range→std::ranges::range、Sortable→std::sortable、EqualityComparable→std::equality_comparable、Integral→std::integral、SemiRegular→std::semiregular、Regular→std::regular、TotallyOrdered→std::totally_ordered、Function→std::invocable、Predicate→std::predicate、Relation→std::relation等;另有两个智能指针 concept:Pointer(有*、-、与默认构造)与Unique_pointer(满足Pointer、可移动且不可复制)。关于标准里已有类型为什么还要 GSL 类型等问题的回答集中在 FAQ 一节(CppCoreGuidelines.md 起):GSL 是准则中规定的那组类型与别名;仓库 README.md 说明其中一个实现是 Microsoft 贡献的 header-only 库,但文档刻意不在准则内绑定任何具体实现——指定接口而非实现,以鼓励多个并行实现;FAQ.54/55 等条目还澄清了 GSL 与 ISO 标准化进程、string_view/observer_ptr等的关系。7. 在真实项目中落地这套准则7.1 渐进采用与 Profile 策略Pro 章开宗明义:理想是全量遵循,但现实是必须把代码嵌入使用既有库的大型代码库,而这些代码往往写于数十年之前。因此策略是按 Profile 分阶段采用:先选一个 Profile(如 bounds)在工具中启用,把代码做到该 Profile 下零警告,其余规则留待后续;这既适合组织级采纳,也适合针对专用应用领域叠加特化规则。附录 B(Modernizing code,CppCoreGuidelines.md 起)则提供旧代码现代化/翻新/升级的可能路径;文档引言也再次强调规则支持渐进采用:一次性完全转换大型代码库通常不可行。7.2 工具链Clang-tidy与CppCoreCheck是附录 D 点名的两个辅助工具方向;工具对违规的标记应携带指向规则的引用/链接,规则本身提供理由、后果与补救;抑制检查必须走第 4.3 节规定的[[gsl::suppress(...)]]语法,使抑制本身可审计;仓库自身的构建与质量脚本也可作参考:scripts/Makefile 组织文档处理流程,scripts/python/cpplint.py 与 scripts/python/cpplint_wrap.py 对 C 示例做风格 linting,scripts/python/md-split.py 做 Markdown 拆分,scripts/hunspell/isocpp.dic 是为术语表定制的拼写字典,另有 scripts/nodejs/package.json 声明的 remark 处理链;站点由 Jekyll 渲染:_config.yml 仅 includeCppCoreGuidelines.md、排除 docs/talks 等目录,版式在 _layouts/default.html 与 _includes/sidebar.html 中,样式为 public/css 下的 poole/hyde 主题——这解释了为何正文刻意保持简单 Markdown(便于翻译与重排)。7.3 配套学习与贡献仓库 docs/gsl-intro.md 是 Herb Sutter 编写的 GSL 教程兼 FAQ,从gsl::span为何替代(指针, 长度)对讲起,给出了缓冲区溢出反例与 span 的正面示范,是理解第 5.2/5.4 节 span 相关规则的最佳入口;docs/ 下另有 docs/ctor-dtor-raii-popl12.pdf 等背景论文;talks/ 收录了 talks/README.md 所列的 CppCon 2015 主题演讲等资料,与 FAQ.2 记录的C Core Guidelines 于 CppCon 2015 由 Stroustrup 首次公布的史实相互印证;组织级采纳的推荐姿势写在 CONTRIBUTING.md:鼓励 fork 出组织自己的准则版本(标题中注明是 fork 并回链原文档),局部修改若普适则提 Issue/PR 回流;PR 要求范围化,重要变更先经 Issue 讨论并标注规则编号;FAQ 还澄清了两个常见误解(FAQ.6、FAQ.8/9):准则未获 ISO C 标准委员会批准,不属于标准本身,目标是随标准演进保持同步;不存在 C98 版本;准则不提议任何新语言特性,只推荐现有标准特性的最佳用法。8. 结语CppCoreGuidelines 主文档的价值不在于读完,而在于它的工程化设计:每条规则都有编号、理由、正反例、例外与 Enforcement 手段,使其天然可被静态分析工具消费;type/bounds/lifetime 三个 Profile 把安全拆成了可分阶段交付的构造性保证;GSL 则以零开销的少量类型(span、not_null、owner、Expects/Ensures、index等)补齐了语言短板,让禁止裸指针数组、禁止未检查下标这类强规则真正可执行。对一个准备引入现代 C 的团队,合理的起点是:通读本文第 3 节的原则层建立直觉,选定一个 Profile 并在分析工具中启用,配合gsl::span/not_null替换既有(指针, 数量)接口,再按第 5 节各章规则逐步对齐——这正是文档作者反复强调的渐进采用路线。【免费下载链接】CppCoreGuidelinesThe C Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about coding in C项目地址: https://gitcode.com/gh_mirrors/cp/CppCoreGuidelines创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考