说明:百度翻译版,只对条目进行了核对,其它均是翻译
2021年11月5日版
- 本贾尼·斯特劳斯特卢普
- 赫布·萨特
-----------------------------------------------------------------------------------------------------------------
- 内容:导言
- P:哲学
- I:接口
- F:功能
- C:类和类层次结构
- ENUM:枚举
- R:资源管理
- ES:表达式和语句
- Per:性能
- 并行性与并行性
- E:错误处理
- Con:常数和不变性
- T:模板和泛型编程
- CPL:C风格编程
- SF:源文件
- SL:标准库
支持部分:
- A:建筑理念
- NR:非规则和神话
- RF:参考文献
- 赞成:简介
- GSL:指南支持库
- NL:命名和布局规则
- QA:常见问题解答
- 附录A:库
- 附录B:代码现代化
- 附录C:讨论
- 附录D:支持工具
- 术语汇编
- 待办事项:未分类的原始规则
您可以为特定语言功能设置示例规则:
- 赋值:常规类型--首选初始化--复制--移动--其它操作--默认
- 班:数据--不变量--成员--辅助--具体类型--构造函数、=、和DTOR--层次结构--运算符
- 概念:规则--在泛型编程中--模板参数--语义
- 构造函数:不变量--建立不变量--抛出--默认--不需要--显式--委托--虚拟
- 派生:何时使用--作为接口--析构函数--复制--getter和setter--多重继承--重载--切片--dynamic_cast班
- 析构函数:和构造函数--需要时?--决不能失败
- 异常:错误--抛出--仅限错误--无异常--最小化尝试--如果没有异常怎么办?
- 对于:和的范围--for和while--for初始值设定项--空正文--循环变量--循环变量类型???
- 函数:命名--单个操作--无抛出--参数--参数传递--多个返回值--指针--lambdas
- 内联:小函数--在标题中
- 初始化:始终--首选{}--lambdas--类内初始值设定项--类成员--工厂函数
- lambda表达式:何时使用
- 运算符:常规--避免转换运算符--和lambda
- 平民的, 私有的和受保护的: 信息隐藏 -- 一致性 -- 受保护的
- 静态断言:编译时检查——和概念
- 结构:用于组织数据--如果没有不变量,则使用--没有私有成员
- 样板:抽象--容器--概念
- 未签名:and signed--位操作
- 事实上的:接口--非虚拟--析构函数--永不失败
您可以查看用于表达规则的设计概念:
- 断言:???
- 错误:???
- 异常:例外保证(???)
- 失败:???
- 不变量:???
- 泄漏:???
- 库:???
- 前提条件:???
- 后条件:???
- 资源:???
P:哲学
本节中的规则非常笼统。
哲学规则摘要:
- P.1:直接用代码表达想法
- P.2:编写ISO标准C++
- P.3:明确意图
- P.4:理想情况下,程序应该是静态类型安全的
- P.5:首选编译时检查而不是运行时检查
- P.6:编译时无法检查的内容应该在运行时可以检查
- P.7:尽早捕获运行时错误
- P.8:不要泄露任何资源
- P.9:不要浪费时间或空间
- P.10:与可变数据相比,首选不可变数据-----能用尽用const
- P.11:封装混乱的结构,而不是通过代码传播
- P.12:酌情使用辅助工具
•静态分析工具
•并发工具
•测试工具
- P.13:根据需要使用支持库
哲学规则通常是不可机械检查的。然而,反映这些哲学主题的个别规则是不正确的。没有哲学基础,更具体/具体/可检查的规则缺乏理论基础。
I:接口
接口是程序两部分之间的契约。准确说明对服务供应商和服务用户的期望是至关重要的。拥有良好的(易于理解、鼓励高效使用、不易出错、支持测试等)接口可能是代码组织最重要的一个方面。
接口规则摘要:
- I.1:使接口明确
- I.2:避免非常量全局变量
- I.3:避免单例
- I.4:使接口精确且强类型化
- I.5:声明先决条件(如有)
- I.6:表示前置条件的优先用Expects()
- I.7:声明后置条件
- I.8:表示后置条件的优先用Ensures()
- I.9:如果接口是模板,则使用概念记录其参数
- I.10:使用异常来表示未执行所需任务
- I.11:切勿通过原始指针(T*)或引用(T&)转移所有权
- I.12:声明一个不能为空的指针为not_null
- I.13:不要将数组作为单个指针传递
- I.22:避免全局对象的复杂初始化
- I.23:保持函数参数的数量较少
- I.24:避免相邻参数,这些参数可以由相同的参数以不同含义的顺序调用
- I.25:与类层次结构相比,首选使用空抽象类作为接口
- I.26:如果您想要交叉编译器ABI,请使用C风格的子集
*ABI-----Application Binary Interface
- I.27:对于稳定的库ABI,考虑piml风格----在实现类里
- I.30:封装违反规则的行为
另见:
- F:功能
- C.具体:具体类型
- C.hier:类层次结构
- C.over:重载和重载运算符
- C.con:容器和其它资源句柄
- E:错误处理
- T:模板和泛型编程
F:功能
函数指定将系统从一个一致状态转移到下一个一致状态的操作或计算。它是程序的基本组成部分。
应该能够有意义地命名函数,指定其参数的要求,并清楚地说明参数和结果之间的关系。实现不是规范。试着思考一个函数做什么以及它是如何做的。函数是大多数接口中最关键的部分,因此请参见接口规则。
功能规则摘要:
函数定义规则:
- F.1:将有意义的操作“打包”为精心命名的函数
- F.2:函数应执行单个逻辑操作
- F.3:保持函数简短
- F.4:如果可能必须在编译时对函数求值,则将其声明为constexpr
- F.5:如果一个函数非常小且时间紧迫,则将其声明为内联
- F.6:如果您的函数不能抛出异常,则声明它为noexcept
- F.7:对于一般用途,使用T*或T&参数,而不是智能指针
- F.8:首选纯函数
- F.9:未使用的参数应该是未命名的
参数传递表达式规则:
- F.15:喜欢简单和传统的信息传递方式
- F.16:对于“in”参数,通过值传递廉价复制的类型,通过引用传递其它类型给const
- F.17:对于“in-out”参数,通过非const的引用传递
- F.18:对于“will-move-from”参数,通过X&& and std::move传递参数
- F.19:对于“forward”参数,通过TP&&传递,并且仅通过std::forward传递参数
- F.20:对于“out”输出值,选择返回值而不是输出参数
- F.21:要返回多个“out”值,最好返回一个结构或元组
- F.60:当“无参数”是一个有效选项时,选择T*而不是T&
参数传递语义规则:
- F.22:使用T*或owner<T*>指定单个对象
- F.23:使用not_null<T>表示“null”不是有效值
- F.24:使用span<T>或span_p<T>指定半开放序列
- F.25:使用zstring或not_null<zstring>来指定C样式字符串
- F.26:在需要指针的地方,使用unique_ptr<T>来转移所有权
- F.27:使用share_ptr<T>来共享所有权
- F.42:返回一个T*表示一个位置(仅限)
- F.43:绝不(直接或间接)返回指向局部对象的指针或引用
- F.44:当不需要复制并且不需要“不返回对象”时,返回一个T&
- F.45:不要返回T&&
- F.46:int是main()的返回类型
- F.47:从赋值运算符返回T&
- F.48:不返回std::move(local)
其它功能规则:
- F.50:当函数不可用时使用lambda(捕获局部变量或编写局部函数)
- F.51:在有选择的地方,选择默认参数而不是重载
- F.52:首选在局部使用的lambdas中通过引用捕获,包括传递给算法
- F.53:避免在lambda中通过引用捕获非局部使用的对象,包括返回的、存储在堆上的或传递给另一个线程的对象
- F.54:如果捕获了this,则显式捕获所有变量(无默认捕获)
- F.55:不要使用va_arg参数
- F.56:避免不必要的条件嵌套
函数与lambda和函数对象有很强的相似性。
另见:C.lambdas:函数对象和lambdas
C:类和类层次结构
类是用户定义的类型,程序员可以为其定义表示、操作和接口。类层次结构用于将相关类组织到层次结构中。
课程规则摘要:
- C.1:将相关数据组织到结构(结构或类)中
- C.2:如果类具有不变量,则使用类;如果数据成员可以独立变化,请使用struct
- C.3:使用类表示接口和实现之间的区别
- C.4:仅当函数需要直接访问类的表示时,才将其设为成员
- C.5:将辅助函数与其支持的类放在同一命名空间中
- C.7:不要在同一语句中定义类或枚举并声明其类型的变量
- C.8:如果任何成员是非公共的,则使用类而不是结构
- C.9:尽量减少成员的暴露
小节:
- C.具体:具体类型
- 构造函数:构造函数、赋值和析构函数
- C.con:容器和其它资源句柄
- C.lambdas:函数对象和lambdas
- C.hier:类层次结构(OOP)
- C.over:重载和重载运算符
- union:union
C.具体:具体类型
具体类型规则摘要:
- C.10:首选具体类型而不是类层次结构
- C.11:使具体类型规则化
- C.12:不要将数据成员设置为const或引用
构造函数:构造函数、赋值和析构函数
这些功能控制对象的生命周期:创建、复制、移动和销毁。定义构造函数以保证和简化类的初始化。
这些是默认操作:
- 默认构造函数:X()
- 复制构造函数:X(常数X&)
- 复制分配:运算符=(常数X&)
- 移动构造函数:X(X&&)
- 移动作业:运算符=(X&&)
- 析构函数:~X()
默认情况下,如果使用这些操作,编译器将定义它们中的每一个,但是可以取消默认操作。
默认操作是一组相关操作,它们共同实现对象的生命周期语义。默认情况下,C++将类视为值类型,但并非所有类型都是值型的。
默认操作规则集:
- C.20:如果可以避免定义任何默认操作,请执行
- C.21:如果定义或=delete任何复制、移动或析构函数,则全部定义或=delete
- C.22:使默认操作保持一致
析构函数规则:
- C.30:如果类在对象销毁时需要显式操作,则定义析构函数
- C.31:类获取的所有资源必须由类的析构函数释放
- C.32:如果一个类有一个原始指针(t*)或引用(t&),考虑它是否拥有。
- C.33:如果一个类拥有一个指针成员,那么定义一个析构函数
- C.35:基类析构函数应该是公共的和虚拟的,或者是受保护的和非虚拟的
- C.36:析构函数不得失败
- C.37:使析构函数noexcept
构造函数规则:
- C.40:如果一个类有一个不变量,那么定义一个构造函数
- C.41:构造函数应该创建一个完全初始化的对象
- C.42:如果构造函数无法构造有效对象,则抛出异常
- C.43:确保一个可复制的类有一个默认的构造函数
- C.44:首选简单且不抛出异常的默认构造函数
- C.45:不要定义只初始化数据成员的默认构造函数;改用成员初始值设定项
- C.46:默认情况下,显式声明单参数构造函数
- C.47:按照成员声明的顺序定义和初始化成员变量
- C.48:对于常量初始值设定项,在构造函数中优先使用类内初始值设定项而不是成员初始值设定项
- C.49:在构造函数中首选初始化而不是赋值
- C.50:如果在初始化过程中需要“虚拟行为”,请使用工厂函数
- C.51:使用委托构造函数表示类的所有构造函数的公共操作
- C.52:使用继承构造函数将构造函数导入到不需要进一步显式初始化的派生类中
复制和移动规则:
- C.60:使复制赋值为非虚拟,按const&获取参数,并按非const&返回
- C.61:复制操作应该复制
- C.62:使复制分配对自分配安全
- C.63:使移动赋值为非虚拟,通过&&获取参数,并通过非const&返回
- C.64:移动操作应移动并使其源保持有效状态
- C.65:使移动分配对自分配安全
- C.66:移动操作不抛出例外
- C.67:多态类应禁止公共的复制/移动
其它默认操作规则:
- C.80:如果必须明确使用默认语义,请使用=default
- C.81:当想禁用默认行为时使用=delete (不需要替代方案)
- C.82:不要在构造函数和析构函数中调用虚函数
- C.83:对于值类型,考虑提供noexcept的交换函数
- C.84:交换不得失败
- C.85:使交换noexcept
- C.86:使“==”的操作数类型对称,且noexcept
- C.87:注意基类上的==
- C.89:创建一个哈希noexcept
- C.90:依赖构造函数和赋值运算符,而不是memset和memcpy
C.con:容器和其它资源句柄
容器是一个对象,它包含一系列某种类型的对象;是典型的容器。资源句柄是拥有资源的类;是典型的资源句柄;它的资源是它的元素序列。向量向量
集装箱规则摘要:
- C.100:定义容器时遵循STL
- C.101:赋予一个容器值语义
- C.102:给一个容器移动操作
- C.103:给一个容器一个初始化列表构造函数
- C.104:为容器提供一个默认构造函数,将其设置为空
- ???
- C.109:如果资源句柄具有指针语义,则提供*和->
另见:资源
C.lambdas:函数对象和lambdas
函数对象是提供重载的对象,以便您可以调用它。lambda表达式(通俗地称为“lambda”)是用于生成函数对象的符号。函数对象的复制成本应该很低(因此按值传递)。()
总结:
- F.50:当函数无法执行时使用lambda(捕获局部变量或编写局部函数)
- F.52:首选在局部使用的lambdas中通过引用捕获,包括传递给算法
- F.53:避免在lambda中通过引用捕获非局部使用的对象,包括返回的、存储在堆上的或传递给另一个线程的对象
- ES.28:使用lambdas进行复杂初始化,尤其是常量变量的初始化
C.hier:类层次结构(OOP)
类层次结构被构造来表示一组分层组织的概念(仅限)。基类通常充当接口。层次结构有两种主要用途,通常称为实现继承和接口继承。
类层次结构规则摘要:
- C.120:使用类层次结构来表示具有固有层次结构的概念(仅限)
- C.121:如果基类用作接口,则将其设置为纯抽象类
- C.122:当需要完全分离接口和实现时,使用抽象类作为接口
为层次结构摘要中的类设计规则:
- C.126:抽象类通常不需要用户编写的构造函数
- C.127:具有虚函数的类应具有虚或受保护的析构函数
- C.128:虚函数应该准确指定virtual 、override或final 之一
- C.129:在设计类层次结构时,区分实现继承和接口继承
- C.130:为了制作多态类的深度拷贝,我们首选虚拟克隆函数,而不是公共拷贝构造/赋值
- C.131:避免琐碎的getter和setter
- C.132:不要无缘无故地将函数虚拟化
- C.133:避免受保护的数据
- C.134:确保所有非const数据成员具有相同的访问级别
- C.135:使用多重继承来表示多个不同的接口
- C.136:使用多重继承来表示实现属性的联合
- C.137:使用虚拟基类来避免过于通用的基类
- C.138:使用 using为派生类及其基类创建重载集
- C.139:在类上谨慎使用final
- C.140:不要为虚函数和重写器提供不同的默认参数
访问层次结构规则摘要中的对象:
- C.145:通过指针和引用访问多态对象
- C.146:在无法避免类层次结构导航的情况下使用dynamic_cast
- C.147:当找不到所需类被视为错误时,对引用类型使用dynamic_cast
- C.148:当找不到所需的类被认为是有效的替代方法时,对指针类型使用dynamic_cast
- C.149:使用unique_ptr或shared_ptr避免忘记删除使用new创建的对象
- C.150:使用make_unique()构造unique_ptr拥有的对象
- C.151:使用make_shared()构造共享_ptr拥有的对象
- C.152:永远不要将指向派生类对象数组的指针指定给指向其基类的指针
- C.153:首选虚函数而不是强制转换
C.over:重载和重载运算符
可以重载普通函数、函数模板和运算符。不能重载函数对象。
过载规则摘要:
- C.160:定义运算符主要是为了模拟常规用法
- C.161:对称运算符使用非成员函数
- C.162:大致相当的过载操作
- C.163:过载仅适用于大致相同的操作
- C.164:避免隐式转换运算符
- C.165:对于自定义点,使用using
- C.166:过载一元&仅作为智能指针和引用系统的一部分
- C.167:将运算符用于具有常规含义的操作
- C.168:在操作数的命名空间中定义过载运算符
- C.170:如果您想让lambda过载,请使用泛型lambda
C.union:union
A是一个所有成员都从同一地址开始的地址,以便一次只能容纳一个成员。A不跟踪存储的成员,因此程序员必须正确地获取它;这本身就容易出错,但也有补偿的方法。协会结构协会
一个类型加上当前成员所在的指示符,称为带标记的联合、有区别的联合或变体。协会
联合规则摘要:
- C.180:使用unions节省内存
- C.181:避免“裸”unions
- C.182:使用匿名unions实现标记unions
- C.183:不要使用union进行类型双关
- ???
枚举:枚举
枚举用于定义整数值集和定义此类值集的类型。有两种枚举,“普通”和s。枚举类枚举
枚举规则摘要:
- Enum.1:首选枚举而不是宏
- Enum.2:使用枚举来表示相关命名常量集
- Enum.3:首选enum class而不是“普通”枚举
- Enum.4:为安全和简单地使用,定义对枚举的操作
- Enum.5:不要全部大写枚举成员
- Enum.6:避免未命名的枚举
- Enum.7:仅在必要时指定枚举的基础类型
- Enum.8:仅在必要时指定枚举器值
R:资源管理
本节包含与资源相关的规则。资源是必须获取和(显式或隐式)释放的任何东西,例如内存、文件句柄、套接字和锁。必须释放它的原因通常是因为它可能供不应求,所以即使延迟释放也可能造成伤害。基本目标是确保我们不会泄露任何资源,也不会持有超过需要时间的资源。负责释放资源的实体称为所有者。
在一些情况下,泄漏是可以接受的,甚至是最佳的:如果您编写的程序只是根据输入生成输出,并且所需的内存量与输入的大小成正比,那么最佳策略(为了性能和编程的方便性)有时就是决不删除任何内容。如果您有足够的内存来处理最大的输入,请泄漏出去,但如果您错了,请务必给出一个良好的错误消息。在这里,我们忽略了这种情况。
· 资源管理规则摘要:
-
- R.1:使用资源句柄和RAII自动管理资源(资源获取是初始化)
- R.2:在接口中,使用原始指针表示单个对象(仅限)
- R.3:原始指针(a T*)是非所有者
- R.4:原始引用(a T&)是非拥有的
- R.5:首选作用域对象,不要进行不必要的堆分配
- R.6:避免非常量全局变量
· 分配和解除分配规则摘要:
-
- R.10:避免malloc()和free()
- R.11:避免显式调用new和delete
- R.12:立即将显式资源分配的结果提供给manager对象
- R.13:在单个表达式语句中最多执行一次显式资源分配
- R.14:避免[]参数,首选span
- R.15:始终重载匹配的allocation/deallocation对
-
- R.20:使用unique_ptr或shared ptr表示所有权
- R.21:除非您需要共享所有权,否则首选unique_ptr而不是shared ptr
- R.22:使用make_shared()生成shared ptr
- R.23:使用make_unique()生成unique_ptr
- R.24:使用std::weak_ptr中断shared ptr的循环
- R.30:将智能指针作为参数仅用于显式表达生命周期语义
- R.31:如果您有非std智能指针,请遵循std的基本模式
- R.32:使用一个unique_ptr<widget>参数来表示函数拥有一个小部件的所有权
- R.33:使用一个unique_ptr<widget>&参数来表示函数重置小部件
- R.34:获取一个shared_ptr<widget>参数来表示共享所有权
- R.35:使用一个shared_ptr<widget>&参数来表示函数可能会重置共享指针
- R.36:使用const shared_ptr<widget>&参数来表示它可能保留对对象的引用计数???
- R.37:不要传递从别名智能指针获得的指针或引用
ES:表达式和语句
表达式和语句是表示动作和计算的最低和最直接的方式。局部作用域中的声明是语句。
有关命名、注释和缩进规则,请参见NL:命名和布局。
一般规则:
- ES.1:与其它库和“手工编写的代码”相比,首选标准库
- ES.2:喜欢合适的抽象而不是直接使用语言特征
- ES.3:不要重复你自己,避免重复代码
声明规则:
- ES.5:保持范围小
- ES.6:在 for 语句初始值设定项和条件中声明名称以限制范围
- ES.7:保持普通和局部名称简短,保持不常见和非局部名称更长
- ES.8:避免使用类似的名称
- ES.9:避免使用全部大写的名称
- ES.10:每次声明仅声明一个名称
- ES.11:使用auto避免重复类型名
- ES.12:不要在嵌套作用域中重用名称
- ES.20:始终初始化对象
- ES.21:在需要使用变量(或常量)之前,不要引入它
- ES.22:在你有一个值来初始化变量之前,不要声明它
- ES.23:首选{} -initializer 语法
- ES.24:使用unique_ptr<T>保存指针
- ES.25:声明对象const或constexpr,除非以后要修改其值
- ES.26:不要将变量用于两个不相关的目的
- ES.27:对栈上的数组使用std::array或stack_array
- ES.28:使用lambdas进行复杂初始化,尤其是常量变量的初始化
- ES.30:不要将宏用于程序文本操作
- ES.31:不要对常量或“函数”使用宏
- ES.32:所有的宏名称要全部大写
- 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.60:避免在资源管理功能之外新建和删除
- ES.61:使用Delete[]删除数组,使用Delete删除非数组
- ES.62:不要比较指向不同数组的指针
- ES.63:不要切片
- ES.64:使用T{e}符号进行构造
- ES.65:不要取消引用无效指针
定义规则:
- ES.70:当有选择时,首选switch语句而不是if语句
- ES.71:在有选择的情况下,首选for语句的范围而不是for语句
- ES.72:当存在明显的循环变量时,首选for语句而不是while语句
- ES.73:当没有明显的循环变量时,首选while语句而不是for语句
- ES.74:首选在for语句的初始值设定项部分声明循环变量
- ES.75:避免使用do语句
- ES.76:避免跳转
- ES.77:尽量减少使用循环中的break和continue
- ES.78:不要依赖switch语句中的隐式失败
- ES.79:使用默认值处理常见情况(仅限)
- ES.84:不要试图声明没有名字的局部变量
- ES.85:使空语句可见
- ES.86:避免修改原始for循环主体内的循环控制变量
- ES.87:不要添加冗余==或!=条件
算术规则:
- ES.100:不要混合使用有符号和无符号算术
- ES.101:使用无符号类型进行位操作
- ES.102:使用有符号类型进行算术运算
- ES.103:不要溢出
- ES.104:不要下溢
- ES.105:不要除以整数零
- ES.106:不要试图通过使用无符号来避免负值
- ES.107:不要对下标使用unsigned,首选gsl::index
Per:性能
??? 本节是否应包含在主指南中???
本节包含针对需要高性能或低延迟的人员的规则。也就是说,这些规则涉及如何使用尽可能少的时间和资源在可预测的短时间内完成任务。本节中的规则比许多(大多数)应用程序所需的规则更具限制性和侵入性。不要天真地试图在一般代码中遵循它们:实现低延迟的目标需要额外的工作。
绩效规则摘要:
- Per.1:不要无故优化
- Per.2:不要过早优化
- Per.3:不要优化性能不重要的东西
- Per.4:不要假设复杂的代码一定比简单的代码快
- Per.5:不要假设低级代码一定比高级代码快
- Per.6:不要在没有测量的情况下声称性能
- Per.7:实现优化的设计
- Per.10:依靠静态类型系统
- Per.11:将计算从运行时移动到编译时
- Per.12:消除冗余的别名
- Per.13:消除冗余的间接寻址
- Per.14:尽量减少分配和释放的次数
- Per.15:不在关键分支上分配
- Per.16:使用紧凑的数据结构
- Per.17:时间关键型结构中首先声明使用最多的成员
- Per.18:空间就是时间
- Per.19:可预测地访问内存
- Per.30:避免关键路径上的上下文切换
并行性与并行性
我们经常希望计算机同时执行许多任务(或至少看起来是同时执行)。执行这些任务的原因各不相同(例如,仅使用单个处理器等待许多事件、同时处理多个数据流或使用多个硬件设施)同时,我们还介绍了使用并发ISO标准C++工具来表示基本并发性和并行性的原则和规则。
线程是并行和并行编程的机器级基础。线程允许独立运行程序的多个部分,同时共享相同的内存。并发编程是棘手的,因为在线程之间保护共享数据更容易说得比做得更容易。可以像添加或策略性地添加一样简单,否则可能需要完全重写,这取决于原始代码是否以线程友好的方式编写。异步螺纹
本文档中的并发/并行规则设计时考虑了三个目标:
- 帮助编写适合在线程环境中使用的代码
- 显示使用标准库提供的线程原语的干净、安全的方法
- 当并发性和并行性不能提供所需的性能增益时,提供如何操作的指导
另外,C++中的并发是一个未完成的故事,C++ 11引入了许多核心并发原语,C++ 14和C++ 17在这些方面有了很大的改进,而且在C++中编写并发程序的兴趣也更大。
这一部分需要做大量的工作(显然)。请注意,我们从相对非专家的规则开始。真正的专家必须等待一段时间;欢迎贡献,但请考虑大多数程序员,他们正在努力使并发程序正确且性能良好。
并发和并行规则摘要:
- CP.1:假设您的代码将作为多线程程序的一部分运行
- CP.2:避免数据竞争
- CP.3:最小化可写数据的显式共享
- CP.4:从任务而非线程的角度思考
- CP.8:不要尝试使用volatile进行同步
- CP.9:只要可行,就使用工具来验证并发代码
另见:
- CP.con:并发
- CP.coro:协同程序
- CP.par:并行性
- CP.mess:消息传递
- CP.vec:矢量化
- CP.free:无锁编程
- CP.etc:etc.并发规则
CP.con:并发
本节重点介绍通过共享数据进行通信的多线程的相对特殊用途。
- 有关并行算法,请参见并行
- 有关无显式共享的任务间通信,请参阅消息传递
- 有关矢量并行代码,请参见矢量化
- 有关无锁编程,请参见无锁编程
并发规则摘要:
- CP.20:使用RAII,从不使用简单的lock()/unlock()
- CP.21:使用std::lock()或std::scoped_lock获取多个互斥锁
- CP.22:在持有锁(例如回调)时,切勿调用未知代码
- CP.23:将连接线程视为作用域容器
- CP.24:将线程视为全局容器
- CP.25:首选gsl::joining_thread而不是std::thread
- CP.26:不要detach()线程
- CP.31:通过值在线程之间传递少量数据,而不是通过引用或指针
- CP.32:要在不相关的线程之间共享所有权,请使用shared_ptr
- CP.40:最小化上下文切换
- CP.41:尽量减少线程的创建和销毁
- CP.42:不要无条件地等待
- CP.43:尽量减少在临界区花费的时间
- CP.44:记住命名你的lock_guard s 和unique_lock s
- CP.50:定义一个互斥锁及其保护的数据。尽可能使用synchronized_value<T>
- ??? 何时使用spinlock
- ??? 何时使用try_lock()
- ??? 什么时候首选lock_guard 而不是unique_lock
- ??? 时间多路复用
- ??? 何时/如何使用新线程
CP.coro:协同程序
本节重点介绍协同程序的使用。
协同程序规则摘要:
- CP.51:不要使用作为协程的捕获lambda
- CP.52:不要跨挂起点持有锁或其它同步原语
- CP.53:不应通过引用来传递协程的参数
CP.par:并行性
“并行性”指的是对多个数据项同时(“并行”)执行任务(或多或少)。
并行规则摘要:
- ???
- ???
- 在适当的情况下,首选标准库并行算法
- 使用为并行性而设计的算法,而不是不必要地依赖于线性计算的算法
CP.mess:消息传递
标准库设施的级别非常低,主要用于使用s、es、类型等进行接近硬件关键编程的需要。大多数人不应该在这个级别工作:它容易出错,开发速度慢。如果可能,请使用更高级别的工具:消息传递库、并行算法和矢量化。本节介绍如何传递消息,以便程序员不必执行显式同步。线互斥原子的
消息传递规则摘要:
- CP.60:使用future从并发任务返回值
- CP.61:使用async()生成并发任务
- 消息队列
- 消息传递库
???? 是否应该有一个“使用X而不是”的定义,其中X是使用更好的指定线程池的东西?异步
??? 考虑到未来(甚至是现有的库)并行设施,是否值得使用?如果有人想要并行化,例如(在交换性的附加前提下)或合并排序,指南应该推荐什么?异步累积
CP.vec:矢量化
矢量化是一种在不引入显式同步的情况下并发执行多个任务的技术。操作只是并行地应用于数据结构的元素(向量、数组等)。矢量化具有一个有趣的特性,即通常不需要对程序进行非局部更改。然而,矢量化最适合于简单的数据结构和专门设计的算法。
矢量化规则摘要:
- ???
- ???
CP.free:无锁编程
使用es和s进行同步可能相对昂贵。此外,它还可能导致死锁。为了提高性能并消除死锁的可能性,我们有时不得不使用复杂的低级“无锁”功能,这些功能依赖于对内存的短暂独占(“原子”)访问。无锁编程还用于实现更高级别的并发机制,如s和es。互斥条件变量线互斥
无锁编程规则摘要:
- CP.100:除非必须,否则不要使用无锁编程
- CP.101:不要信任您的硬件/编译器组合
- CP.102:仔细研究文献
- 如何/何时使用原子操作
- 避免饥饿
- 使用无锁数据结构,而不是手工制作特定的无锁访问
- CP.110:不要为初始化编写自己的双重检查锁定
- CP.111:如果确实需要双重检查锁定,请使用传统模式
- 如何/何时比较和交换
CP.etc:etc.并发规则
这些规则不符合简单的分类:
- CP.200:仅使用volatile与非C++内存通信
- CP.201:???信号
E:错误处理
错误处理包括:
- 检测错误
- 将有关错误的信息传输到某些处理程序代码
- 保存程序的有效状态
- 避免资源泄漏
不可能从所有错误中恢复。如果无法从错误中恢复,则必须以明确定义的方式快速“退出”。错误处理策略必须简单,否则会导致更严重的错误。未经测试且很少执行的错误处理代码本身就是许多错误的根源。
这些规则旨在帮助避免几种错误:
- 类型冲突(例如,误用s和类型转换)协会
- 资源泄漏(包括内存泄漏)
- 界限误差
- 生存期错误(例如,在is被删除后访问对象)删去
- 复杂性错误(由于思想表达过于复杂而可能导致的逻辑错误)
- 接口错误(例如,通过接口传递了意外值)
错误处理规则摘要:
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:析构函数、释放和交换决不能失败
E.17:不要试图捕捉每个函数中的每个异常
E.18:尽量减少显式try/catch的使用
E.19:如果没有合适的资源句柄可用,则使用final_action对象来表示清理
E.25:如果您不能抛出异常,那么模拟RAII进行资源管理
E.26:如果不能抛出异常,请考虑快速失败。
E.27:如果不能抛出异常,那么系统地使用错误代码
E.28:避免基于全局状态的错误处理(例如errno)
E.30:不要使用异常规范
E.31:正确排序catch子句
Con:常数和不变性
你不能有一个常数的竞赛条件。当许多对象无法更改其值时,更容易对程序进行推理。承诺“不改变”作为参数传递的对象的接口大大提高了可读性。
恒定规则摘要:
- Con.1:默认情况下,使对象不可变
Enforcement:标记未修改的非常量变量(参数除外,以避免许多误报)
- Con.2:默认情况下,将成员函数设为常量
- Con.3:默认情况下,将指针和引用传递给常量
- Con.4:使用const定义对象,这些对象的值在构造后不会更改
- Con.5:对可以在编译时计算的值使用constexpr
T:模板和泛型编程
泛型编程是使用类型、值和算法参数化的类型和算法进行编程。在C++中,泛型编程由语言机制支持。样板
泛型函数的参数的特征是对所涉及的参数类型和值的一系列要求。在C++中,这些要求由编译时间谓词表示为概念。
模板也可用于元编程;也就是说,在编译时编写代码的程序。
泛型编程的中心概念是“概念”;也就是说,对作为编译时谓词表示的模板参数的要求。“概念”在ISO技术规范:概念中定义。在另一个ISO TS:ranges中可以找到一组标准库概念的草稿。GCC 6.1及更高版本支持这些概念。因此,我们在示例中注释掉概念的用法;也就是说,我们仅将它们用作形式化注释。如果使用GCC6.1或更高版本,可以取消注释。
模板使用规则摘要:
- T.1:使用模板提高代码的抽象级别
- T.2:使用模板来表示适用于许多参数类型的算法
- T.3:使用模板表示容器和范围
- T.4:使用模板来表示语法树操作
- T.5:结合通用技术和面向对象技术来增强它们的优势,而不是它们的成本
概念使用规则摘要:
- T.10:为所有模板参数指定概念
- T.11:尽可能使用标准概念
- T.12:局部变量首选概念名称而不是auto
- T.13:对于简单的单一类型参数概念,首选使用简写符号
- ???
概念定义规则摘要:
- T.20:避免没有意义语义的“概念”
- T.21:一个概念需要一套完整操作
- T.22:为概念指定公理
- T.23:通过添加新的使用模式,将改进的概念与其更一般的情况区分开来
- T.24:使用标签类或特征来区分仅在语义上不同的概念
- T.25:避免互补约束
- T.26:首选根据使用模式而不是简单的语法来定义概念
- T.30:谨慎地使用概念否定(!C<T>)来表达细微的差异
- T.31:谨慎使用概念析取(C1<T>| | C2<T>)来表达备选方案
- ???
模板接口规则摘要:
- T.40:使用函数对象将操作传递给算法
- T.41:仅要求模板概念中的基本属性
- T.42:使用模板别名简化符号并隐藏实现细节
- T.43:首选使用typedef来定义别名
- T.44:使用函数模板推断类模板的参数类型(如果可行)
- T.46:要求模板参数至少是半正则的
- T.47:避免使用公共名称的高度可见的无约束模板
- T.48:如果您的编译器不支持概念,请在必要时使用enable_if伪造它们
- T.49:尽可能避免类型擦除
模板定义规则摘要:
- T.60:最小化模板的上下文依赖关系
- T.61:不要过度参数化成员(SCARY)
- T.62:将非依赖类模板成员放置在非模板基类中
- T.64:使用特化来提供类模板的替代实现
- T.65:使用标签调度来提供函数的替代实现
- T.67:使用特化为不规则类型提供替代实现
- T.68:在模板中使用{}而不是()以避免歧义
- T.69:在模板中,不要进行非限定的非成员函数调用,除非您打算将其作为自定义点
模板和层次结构规则摘要:
- T.80:不要天真地模板化类层次结构
- T.81:不要混合层次结构和数组//???“层次结构”中的某个地方
- T.82:当不需要虚函数时,线性化层次结构
- T.83:不要声明成员函数模板为虚拟
- T.84:使用非模板核心实现来提供ABI稳定的接口
- T.??:????
可变模板规则摘要:
- T.100:当您需要一个接受各种类型的可变数量参数的函数时,请使用可变参数模板
- T.101:???如何将参数传递给可变参数模板???
- T.102:???如何处理可变参数模板的参数???
- T.103:不要对同构参数列表使用可变参数模板
- T.??:????
元编程规则摘要:
- T.120:仅当您确实需要时才使用模板元编程
- T.121:主要使用模板元编程来模拟概念
- T.122:在编译时使用模板(通常是模板别名)计算类型
- T.123:在编译时使用constexpr函数计算值
- T.124:首选使用标准库的TMP设施
- T.125:如果您需要超越标准库TMP设施,请使用现有库
- T.??:????
其它模板规则摘要:
- T.140:命名所有可能重复使用的操作
- T.141:如果只在一个地方需要一个简单的函数对象,请使用未命名的lambda
- T.142:使用模板变量简化符号
- T.143:不要无意中编写非泛型代码
- T.144:不要专门化函数模板
- T.150:使用静态断言检查类是否与概念匹配
- T.??:????
CPL:C风格编程
C语言和C++语言是紧密相关的语言。它们都起源于1978年的“经典C”,并从那时起在ISO委员会中发展。为了保持它们的兼容性,人们做了很多尝试,但这两种方法都不是另一种方法的子集。
C规则摘要:
- CPL.1:首选C++,其次才是C语言
- CPL.2:如果必须使用C,使用C和C++的公共子集,并将C代码编译为C++。
- CPL.3:如果必须使用C来进行接口,请在使用此类接口的调用代码中使用 C++
SF:源文件
区分声明(用作接口)和定义(用作实现)。使用头文件表示接口并强调逻辑结构。
源文件规则摘要:
SF.1:如果您的项目尚未遵循其它约定,则代码文件使用.cpp后缀,接口文件使用.h后缀
SF.2:.h文件不能包含对象定义或非内联函数定义
SF.3:对多个源文件中使用的所有声明使用.h文件
SF.4:在文件中的其它声明之前包含.h文件
SF.5:.cpp文件必须包含定义其接口的.h文件
SF 6:使用using命名空间指令进行转换,用于基础库(如STD),或在局部范围内(仅)
SF.7:不要在头文件的全局范围内写入using命名空间
SF.8:对所有.h文件,使用#include防护
SF.9:避免源文件之间的循环依赖
SF.10:避免依赖隐式#included名称
SF.11:头文件应该是自包含的
SF.12:对于与包含文件相关的文件以及其它地方的尖括号形式,更倾向于引用#include的形式
SF.20:使用namespace s 来表达逻辑结构
SF.21:不要在头中使用未命名(匿名)命名空间
SF.22:对所有内部/非导出实体使用未命名(匿名)命名空间
SL:标准库
仅仅使用简单的语言,每项任务都是乏味的(用任何语言)。使用合适的库,任何任务都可以相当简单。
多年来,标准库稳步发展。它在标准中的描述现在比语言特性的描述更大。因此,指南的这个库部分很可能最终会扩大到等于或超过所有其它部分。
<< ??? 我们需要另一个级别的规则编号???>>
C++标准库组件摘要:
- SL.con:集装箱
- str:String
- SL.io:Iostream
- SL.regex:regex
- 时间
- SL.C:C标准库
标准库规则摘要:
- SL.1:尽可能使用库
- SL.2:与其它库相比,首选标准库
- SL.3:不要将非标准实体添加到命名空间std
- SL.4:以类型安全的方式使用标准库
- ???
支持部分:
A:建筑理念
本节包含关于更高层次的体系结构思想和库的思想。
架构规则摘要:
- A.1:将稳定代码与不稳定代码分开
- A.2:将潜在可重用部件表示为库
- A4:库之间不应存在循环
NR:非规则和神话
本节包含一些在某些地方很流行的规则和指导原则,但我们特意不推荐这些规则和指导原则。我们非常清楚,这些规则在某些时候和某些地方是有意义的,我们自己也曾使用过。然而,在我们推荐和支持的编程风格的背景下,这些“非规则”是有害的。
即使在今天,也可能存在规则有意义的情况。例如,缺乏合适的工具支持可能导致异常不适合硬实时系统,但请不要天真地相信“常识”(例如,关于“效率”的不受支持的陈述);这种“智慧”可能是基于几十年的信息或经验,从语言的性质不同的C++(例如,C或java)。
这些非规则的替代方案的积极论点列在作为“替代方案”提供的规则中。
非规则摘要:
- NR.1:不要坚持所有声明都应该在函数的顶部
- NR.2:不要坚持在一个函数中只有一个返回语句
- NR.3:不要避免异常
- NR.4:不要坚持把每个类声明放在它自己的源文件中
- NR.5:不要使用两阶段初始化
- NR.6:不要将所有清理操作放在函数末尾,然后goto exit
- NR.7:不要保护所有的数据成员
==================================================
RF:参考文献
赞成:简介
GSL:指南支持库
NL:命名和布局规则
常见问题解答:常见问题解答
附录A:库
附录B:代码现代化
附录C:讨论
附录D:支持工具
术语汇编
待办事项:未分类的原始规则