From a90680f76db789c54f962d5550ff072f48e251d1 Mon Sep 17 00:00:00 2001 From: Yu Lou Date: Tue, 13 Feb 2024 22:18:59 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=20Python=20=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC=E8=A7=84=E8=8C=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../python_language_rules.rst | 59 ++++++++++--------- .../python_style_rules.rst | 38 ++++++------ 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/google-python-styleguide/python_language_rules.rst b/google-python-styleguide/python_language_rules.rst index 55493a0..4d94f1e 100644 --- a/google-python-styleguide/python_language_rules.rst +++ b/google-python-styleguide/python_language_rules.rst @@ -168,14 +168,14 @@ Lint def connect_to_next_port(self, minimum: int) -> int: """连接到下一个可用的端口. - Args: - minimum: 一个大于等于 1024 的端口号. + 参数: + minimum: 一个大于等于 1024 的端口号. - Returns: - 新的最小端口. + 返回: + 新的最小端口. - Raises: - ConnectionError: 没有可用的端口. + 抛出: + ConnectionError: 没有可用的端口. """ if minimum < 1024: # 注意这里抛出 ValueError 的情况没有在文档里说明,因为 API 的 @@ -196,11 +196,11 @@ Lint def connect_to_next_port(self, minimum: int) -> int: """连接到下一个可用的端口. - Args: - minimum: 一个大于等于 1024 的端口号. + 参数: + minimum: 一个大于等于 1024 的端口号. - Returns: - 新的最小端口. + 返回: + 新的最小端口. """ assert minimum >= 1024, '最小端口号至少为 1024.' port = self._find_next_open_port(minimum) @@ -225,7 +225,7 @@ Lint 避免全局变量. 定义: - 在程序运行时可以发生变化的模块级变量和类属性. + 在程序运行时可以发生变化的模块级变量和类属性 (class attribute). 优点: 偶尔有用. @@ -259,7 +259,7 @@ Lint 结论: 可以谨慎使用. 尽量避免使用嵌套函数和嵌套类, 除非需要捕获 ``self`` 和 ``cls`` 以外的局部变量. 不要仅仅为了隐藏一个函数而使用嵌套函数. 应将需要隐藏的函数定义在模块级别, 并给名称加上 ``_`` 前缀, 以便在测试代码中调用此函数. -推导式和生成式 +推导式 (comprehension expression) 和生成式 (generator expression) -------------------------------- .. tip:: @@ -269,7 +269,7 @@ Lint 列表、字典和集合的推导式和生成式可以用于简洁高效地创建容器和迭代器, 而无需借助循环、 ``map()``、 ``filter()``, 或者 ``lambda`` . (译者注: 元组是没有推导式的, ``()`` 内加类似推导式的句式返回的是个生成器) 优点: - 相较于其它创建字段、列表和集合的方法, 简单的列表推导式更加清晰和简洁. 生成器表达式十分高效, 因为无需创建整个列表. + 相较于其它创建字典、列表和集合的方法, 简单的列表推导式更加清晰和简洁. 生成器表达式十分高效, 因为无需创建整个列表. 缺点: 复杂的列表推导式和生成式难以理解. @@ -467,7 +467,7 @@ Lambda函数 结论: 可以使用, 不过有如下注意事项: - 函数和方法的默认值不能是可变对象. + 函数和方法的默认值不能是可变 (mutable) 对象. 正确: @@ -494,7 +494,7 @@ Lambda函数 ... def foo(a, b=_FOO.value): # 此时还没有解析 sys.argv... ... - def foo(a, b: Mapping = {}): # 可能会赋值给未经过静态检查的代码 + def foo(a, b: Mapping = {}): # 可能会赋值给未经过静态检查 (unchecked) 的代码 ... @@ -520,7 +520,7 @@ Lambda函数 #. 子类继承时可能产生困惑. 结论: - 允许使用特性. 但是, 和运算符重载一样, 只能在必要时使用, 并且要模仿常规属性的存取特点. 若无法满足要求, 请参考 :ref:`设置器和写入器 ` 的规则. + 允许使用特性. 但是, 和运算符重载一样, 只能在必要时使用, 并且要模仿常规属性的存取特点. 若无法满足要求, 请参考 :ref:`获取器和写入器 ` 的规则. 举个例子, 一个特性不能仅仅用于获取和设置一个内部属性: 因为不涉及计算, 没有必要用特性 (应该把该属性设为公有). 而用特性来限制属性的访问或者计算 **简单** 的衍生值则是正确的: 这种逻辑简单明了. @@ -548,8 +548,8 @@ True/False的求值 #. 一定要用 ``if foo is None:`` (或者 ``is not None``) 来检测 ``None`` 值. 例如, 如果你要检查某个默认值为 ``None`` 的参数有没有被调用者覆盖, 覆盖的值在布尔语义下可能也是假值! #. 永远不要用 ``==`` 比较一个布尔值是否等于 ``False``. 应该用 ``if not x:`` 代替. 如果你需要区分 ``False`` 和 ``None``, 你应该用复合表达式, 例如 ``if not x and x is not None:``. - #. 多利用空序列(字符串, 列表, 元组)是假值的特性. 因此 ``if not seq:`` 比 ``if len(seq):`` 更好, ``if not seq:`` 比 ``if not len(seq):`` 更好. - #. 处理整数时, 使用隐式false可能会得不偿失(例如不小心将 ``None`` 当做0来处理). 你可以显式比较整型值与0的关系 (``len()`` 的返回值例外). + #. 多利用空序列(字符串, 列表, 元组)是假值的特点. 因此 ``if not seq:`` 比 ``if len(seq):`` 更好, ``if not seq:`` 比 ``if not len(seq):`` 更好. + #. 处理整数时, 使用隐式 False 可能会得不偿失(例如不小心将 ``None`` 当做0来处理). 你可以显式比较整型值与0的关系 (``len()`` 的返回值例外). 正确: @@ -579,7 +579,7 @@ True/False的求值 x = x or [] #. 注意, '0'(字符串, 不是整数)作为布尔值时等于 ``True``. - #. 注意, Numpy 数组转换为布尔值时可能抛出异常. 因此建议用 `.size` 属性检查 ``np.array`` 是否为空 (例如 ``if not users.size``). + #. 注意, 把 Numpy 数组转换为布尔值时可能抛出异常. 因此建议用 `.size` 属性检查 ``np.array`` 是否为空 (例如 ``if not users.size``). 词法作用域(Lexical Scoping, 又名静态作用域) --------------------------------------------- @@ -680,25 +680,25 @@ True/False的求值 选择线程间的数据传递方式时, 应优先考虑 ``queue`` 模块的 ``Queue`` 数据类型. 如果不适用, 则使用 ``threading`` 模块及其提供的锁原语(locking primitives). 如果可行, 应该用条件变量和 ``threading.Condition`` 替代低级的锁. -威力过大的特性 +威力过大的功能 -------------------- .. tip:: - 避开这些特性. + 避开这些功能. 定义: - Python是一种异常灵活的语言, 有大量花哨的特性, 诸如自定义元类(metaclasses), 读取字节码(bytecode), 及时编译(on-the-fly compilation), 动态继承, 对象父类重定义(object reparenting), 导入(import)技巧, 反射(例如 ``getattr()``), 系统内部状态的修改, ``__del__`` 实现的自定义清理等等. + Python是一种异常灵活的语言, 有大量花哨的功能, 诸如自定义元类(metaclasses), 读取字节码(bytecode), 及时编译(on-the-fly compilation), 动态继承, 对象基类重设(object reparenting), 导入(import)技巧, 反射(例如 ``getattr()``), 系统内部状态的修改, ``__del__`` 实现的自定义清理等等. 优点: - 强大的语言特性让代码紧凑. + 强大的语言功能让代码紧凑. 缺点: - 这些很"酷"的特性十分诱人, 但多数情况下没必要使用. 包含奇技淫巧的代码难以阅读、理解和调试. 一开始可能还好(对原作者而言), 但以后回顾代码时, 这种代码通常比那些长而直白的代码更加深奥. + 这些很"酷"的功能十分诱人, 但多数情况下没必要使用. 包含奇技淫巧的代码难以阅读、理解和调试. 一开始可能还好(对原作者而言), 但以后回顾代码时, 这种代码通常比那些长而直白的代码更加深奥. 结论: - 避开这些特性. + 避开这些功能. - 可以使用那些在内部利用了这些特性的标准模块和类, 比如 ``abc.ABCMeta``, ``dataclasses`` 和 ``enum``. + 可以使用那些在内部利用了这些功能的标准模块和类, 比如 ``abc.ABCMeta``, ``dataclasses`` 和 ``enum``. 现代python: from __future__ imports @@ -739,7 +739,8 @@ True/False的求值 .. tip:: 你可以根据 `PEP-484 `_ 来对 python3 代码进行注释,并使用诸如 `pytype `_ 之类的类型检查工具来检查代码. - 类型注释既可以写在源码里,也可以写在 `pyi `_ 中. 推荐尽量写在源码里. 对于第三方代码和扩展包, 请使用 pyi 文件里 + + 类型注释既可以写在源码里,也可以写在 `pyi `_ 中. 推荐尽量写在源码里. 对于第三方代码和扩展包, 请使用 pyi 文件. 定义: 用在函数参数和返回值上: @@ -755,10 +756,10 @@ True/False的求值 a: SomeType = some_func() 优点: - 可以提高代码可读性和可维护性. 类型检查器可以把运行时错误变成编译错误, 并阻止你使用威力过大的特性. + 可以提高代码可读性和可维护性. 类型检查器可以把运行时错误变成编译错误, 并阻止你使用威力过大的功能. 缺点: - 必须时常更新类型声明. 正确的代码也可能有误报. 无法使用威力大的特性. + 必须时常更新类型声明. 正确的代码也可能有误报. 无法使用威力大的功能. 结论: 强烈推荐你在更新代码时启用 python 类型分析. 在添加或修改公开API时, 请添加类型注释, 并在构建系统(build system)中启用 pytype. 由于python静态分析是新功能, 因此一些意外的副作用(例如类型推导错误)可能会阻碍你的项目采纳这一功能. 在这种情况下, 建议作者在 BUILD 文件或者代码中添加一个 TODO 注释或者链接, 描述那些阻碍采用类型注释的问题. diff --git a/google-python-styleguide/python_style_rules.rst b/google-python-styleguide/python_style_rules.rst index a6b2383..3128a00 100644 --- a/google-python-styleguide/python_style_rules.rst +++ b/google-python-styleguide/python_style_rules.rst @@ -36,30 +36,30 @@ Python风格规范 if (width == 0 and height == 0 and color == '红' and emphasis == '加粗'): - (bridge_questions.clarification_on - .average_airspeed_of.unladen_swallow) = '美国的还是欧洲的?' + (bridge_questions.clarification_on + .average_airspeed_of.unladen_swallow) = '美国的还是欧洲的?' - with ( - very_long_first_expression_function() as spam, - very_long_second_expression_function() as beans, - third_thing() as eggs, - ): - place_order(eggs, beans, spam, beans) + with ( + very_long_first_expression_function() as spam, + very_long_second_expression_function() as beans, + third_thing() as eggs, + ): + place_order(eggs, beans, spam, beans) 错误: .. code-block:: python if width == 0 and height == 0 and \ - color == '红' and emphasis == '加粗': + color == '红' and emphasis == '加粗': bridge_questions.clarification_on \ .average_airspeed_of.unladen_swallow = '美国的还是欧洲的?' with very_long_first_expression_function() as spam, \ - very_long_second_expression_function() as beans, \ - third_thing() as eggs: - place_order(eggs, beans, spam, beans) + very_long_second_expression_function() as beans, \ + third_thing() as eggs: + place_order(eggs, beans, spam, beans) 如果字符串的字面量 (literal) 超过一行, 应该用圆括号实现隐式续行: @@ -258,7 +258,7 @@ Shebang行 **文档字符串** - Python 的文档字符串用于注释代码. 文档字符串是位于包、模块、类或函数里第一个语句的字符串. 可以用对象的 ``__doc__`` 成员自动提取这些字符串, 并为 ``pydoc`` 所用. (可以试试在你的模块上运行 ``pydoc`` 并观察结果). 文档字符串一定要用三重双引号 ``"""`` 的格式 (依据 `PEP-257 `_ ). 文档字符串应该是一行概述 (整行不超过 80 个字符), 以句号、问号或感叹号结尾. 如果要写更多注释 (推荐), 那么概述后面必须紧接着一个空行, 然后是剩下的内容, 缩进与文档字符串的第一行的第一个引号对齐. 下面是更多有关文档字符串的格式规范. + Python 的文档字符串用于注释代码. 文档字符串是包、模块、类或函数里作为第一个语句的字符串. 可以用对象的 ``__doc__`` 成员自动提取这些字符串, 并为 ``pydoc`` 所用. (可以试试在你的模块上运行 ``pydoc`` 并观察结果). 文档字符串一定要用三重双引号 ``"""`` 的格式 (依据 `PEP-257 `_ ). 文档字符串应该是一行概述 (整行不超过 80 个字符), 以句号、问号或感叹号结尾. 如果要写更多注释 (推荐), 那么概述后面必须紧接着一个空行, 然后是剩下的内容, 缩进与文档字符串的第一行第一个引号对齐. 下面是更多有关文档字符串的格式规范. **模块** @@ -287,7 +287,7 @@ Shebang行 .. code-block:: python - """这个blaze测试会使用样板文件. + """这个blaze测试会使用样板文件(golden files). 若要更新这些文件, 你可以在 `google3` 文件夹中运行 `blaze run //foo/bar:foo_test -- --update_golden_files` @@ -872,12 +872,12 @@ TODO (待办) 注释 **命名规范** - #. "内部(Internal)"这个词表示仅在模块内可用, 或者在类内是保护/私有的. + #. "内部(Internal)"一词表示仅在模块内可用, 或者在类内是受保护/私有的. #. 在一定程度上, 在名称前加单下划线 (``_``) 可以保护模块变量和函数 (格式检查器会对受保护的成员访问操作发出警告). #. 在实例的变量或方法名称前加双下划线 (``__``, 又名为 dunder) 可以有效地把变量或方法变成类的私有成员 (基于名称修饰 name mangling 机制). 我们不鼓励这种用法, 因为这会严重影响可读性和可测试性, 而且没有 **真正** 实现私有. 建议使用单下划线. #. 应该把相关的类和顶级函数放在同一个模块里. 与Java不同, 不必限制一个模块只有一个类. #. 类名应该使用首字母大写的形式 (如 CapWords), 但是模块名应该用小写加下划线的形式 (如 lower_with_under.py). 尽管有些旧的模块使用类似于 CapWords.py 这样的形式, 现在我们不再鼓励这种命名方式, 因为模块名和类名相同时会让人困惑 ("等等, 我刚刚写的是 ``import StringIO`` 还是 ``from StringIO import StringIO``?"). - #. 新的 **单元测试** 文件应该遵守 PEP 8, 用小写加下划线格式的方法名, 例如 ``test_<被测试的方法名>_<状态>.``. 有些老旧的模块有 ``CapWords`` 这样大写方法名, 为了保持风格一致, 可以在 test 这个词和方法名之后, 用下划线分割名称中不同的逻辑成分. 比如一种可行的格式之一是 ``test<被测试的方法>_<状态>``. + #. 新的 **单元测试** 文件应该遵守 PEP 8, 用小写加下划线格式的方法名, 例如 ``test_<被测试的方法名>_<状态>``. 有些老旧的模块有形如 ``CapWords`` 这样大写的方法名, 为了保持风格一致, 可以在 test 这个词和方法名之后, 用下划线分割名称中不同的逻辑成分. 比如一种可行的格式之一是 ``test<被测试的方法>_<状态>``. **文件名** @@ -1176,7 +1176,7 @@ TODO (待办) 注释 **NoneType** - 在 Python 的类型系统中, ``NoneType`` 是 "一等" 类型. 在类型注解中, ``None`` 是 ``NoneType`` 的别名. 如果一个变量可能为 ``None``, 则必须声明这种情况! 你可以使用 ``|`` 这样的联合 (union) 类型表达式 (推荐在新的 Python 3.10+ 代码中使用) 或者老的 ``Optional`` 和 ``Union`` 语法. + 在 Python 的类型系统中, ``NoneType`` 是 "一等" 类型. 在类型注解中, ``None`` 是 ``NoneType`` 的别名. 如果一个变量可能为 ``None``, 则必须声明这种情况! 你可以使用 ``|`` 这样的并集 (union) 类型表达式 (推荐在新的 Python 3.10+ 代码中使用) 或者老的 ``Optional`` 和 ``Union`` 语法. 应该用显式的 ``X | None`` 替代隐式声明. 早期的 PEP 484 允许将 ``a: str = None`` 解释为 ``a: str | None = None``, 但这不再是推荐的行为. @@ -1184,7 +1184,7 @@ TODO (待办) 注释 .. code-block:: python - # 现代的联合写法. + # 现代的并集写法. def modern_or_union(a: str | int | None, b: str | None = None) -> str: ... # 采用 Union / Optional. @@ -1376,7 +1376,7 @@ TODO (待办) 注释 **有条件的导入** - 仅在一些特殊情况下, 比如必须在运行时避免导入类型检查所需的模块时, 才能有条件地导入. 不推荐这种写法. 替代方案是重构代码, 使类型检查所需的模块可以在顶层导入. + 仅在一些特殊情况下, 比如在运行时必须避免导入类型检查所需的模块, 才能有条件地导入. 不推荐这种写法. 替代方案是重构代码, 使类型检查所需的模块可以在顶层导入. 可以把仅用于类型注解的导入放在 ``if TYPE_CHECKING:`` 语句块内. From b61cb972fa7be4555b2a68049af2b5710e625efc Mon Sep 17 00:00:00 2001 From: Yu Lou Date: Thu, 22 Feb 2024 20:27:35 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=E9=87=8D=E6=96=B0=E7=BF=BB=E8=AF=91=20C++?= =?UTF-8?q?=20=E9=A3=8E=E6=A0=BC=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- google-cpp-styleguide/headers.rst | 178 +++++++++------- google-cpp-styleguide/index.rst | 72 ++++++- google-cpp-styleguide/scoping.rst | 340 +++++++++++++++++++++--------- 3 files changed, 403 insertions(+), 187 deletions(-) diff --git a/google-cpp-styleguide/headers.rst b/google-cpp-styleguide/headers.rst index 269f5dc..8d3abec 100644 --- a/google-cpp-styleguide/headers.rst +++ b/google-cpp-styleguide/headers.rst @@ -1,187 +1,207 @@ 1. 头文件 ---------------- -通常每一个 ``.cc`` 文件都有一个对应的 ``.h`` 文件. 也有一些常见例外, 如单元测试代码和只包含 ``main()`` 函数的 ``.cc`` 文件. +通常每个 ``.cc`` 文件应该有一个配套的 ``.h`` 文件. 常见的例外情况包括单元测试和仅有 ``main()`` 函数的 ``.cc`` 文件. -正确使用头文件可令代码在可读性、文件大小和性能上大为改观. +正确使用头文件会大大改善代码的可读性和执行文件的大小、性能. -下面的规则将引导你规避使用头文件时的各种陷阱. +下面的规则将带你规避头文件的各种误区. .. _self-contained-headers: -1.1. Self-contained 头文件 +1.1. 自给自足的头文件 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 ``.h`` 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 ``.inc`` 结尾。不允许分离出 ``-inl.h`` 头文件的做法. + 头文件应该自给自足 (self-contained, 也就是可以独立编译), 并以 ``.h`` 为扩展名. 给需要被导入 (include) 但不属于头文件的文件设置为 ``.inc`` 扩展名, 并尽量避免使用. -所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 :ref:`define-guard`,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols. +所有头文件应该自给自足, 也就是头文件的使用者和重构工具在导入文件时无需任何特殊的前提条件. 具体来说, 头文件要有头文件防护符 (header guards, :ref:`define-guard`),并导入它所需的所有其它头文件. -不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 ``.inc`` 文件扩展名。 +若头文件声明了内联函数 (inline function) 或模版 (template), 而且头文件的使用者需要实例化 (instantiate) 这些组件时, 头文件必须直接或通过导入的文件间接提供这些组件的实现 (definition). 不要把这些实现放到另一个头文件里 (例如 ``-inl.h`` 文件) 再导入进来; 这是过去的惯例, 但现在被禁止了. 若模版的所有实例化过程都仅出现在一个 ``.cc`` 文件中, 比如采用显式 (explicit) 实例化, 或者只有这个 ``.cc`` 文件会用到模版定义 (definition), 那么可以把模版的定义放在这个文件里. -如果 ``.h`` 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 ``.cc`` 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 ``-inl.h`` 文件里(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。 - -有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 ``.cc`` 文件里。 +在少数情况下, 用于导入的文件不能自给自足. 它们通常是要在特殊的地方导入, 例如另一个文件中间的某处. 此类文件不需要使用头文件防护符, 也不需要导入它的依赖 (prerequisite). 此类文件应该使用 ``.inc`` 扩展名. 尽量少用这种文件, 可行时应该采用自给自足的头文件. .. _define-guard: -1.2. #define 保护 +1.2. #define 防护符 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 所有头文件都应该有 ``#define`` 保护来防止头文件被多重包含, 命名格式当是: ``___H_`` . + 所有头文件都应该用 ``#define`` 防护符来防止重复导入. 防护符的格式是: ``<项目>_<路径>_<文件名>_H_`` . -为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 ``foo`` 中的头文件 ``foo/src/bar/baz.h`` 可按如下方式保护: +为了保证符号的唯一性, 防护符的名称应该基于该文件在项目目录中的完整文件路径. 例如, ``foo`` 项目中的文件 ``foo/src/bar/baz.h`` 应该有如下防护: .. code-block:: c++ #ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... - #endif // FOO_BAR_BAZ_H_ + #endif // FOO_BAR_BAZ_H_ + + +.. _include-what-you-use: + +1.3. 导入你的依赖 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. tip:: + + 若代码文件或头文件引用了其他地方定义的符号 (symbol), 该文件应该直接导入 (include) 提供该符号的声明 (declaration) 或者定义 (definition) 的头文件. 不应该为了其他原因而导入头文件. + +不要依赖间接导入. 这样, 人们删除不再需要的 ``#include`` 语句时, 才不会影响使用者. 此规则也适用于配套的文件: 若 ``foo.cc`` 使用了 ``bar.h`` 的符号, 就需要导入 ``bar.h``, 即使 ``foo.h`` 已经导入了 ``bar.h``. .. _forward-declarations: -1.3. 前置声明 +1.4. 前向声明 ~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 尽可能地避免使用前置声明。使用 ``#include`` 包含需要的头文件即可。 + 尽量避免使用前向声明. 应该 :ref:`导入你所需的头文件 `。 **定义:** - 所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义. + 前向声明 (forward declaration) 是没有对应定义 (definition) 的声明. + + .. code-block:: c++ + + // 在 C++ 源码文件中: + class B; + void FuncInB(); + extern int variable_in_b; + ABSL_DECLARE_FLAG(flag_in_b); **优点:** - * 前置声明能够节省编译时间,多余的 ``#include`` 会迫使编译器展开更多的文件,处理更多的输入。 - * 前置声明能够节省不必要的重新编译的时间。 ``#include`` 使代码因为头文件中无关的改动而被重新编译多次。 + * 使用前向声明能节约编译时间, 因为 ``#include`` 会迫使编译器打开更多的文件并处理更多的输入. + * 使用前向声明能避免不必要的重复编译. 若使用 ``#include``, 头文件中无关的改动也会触发重新编译. **缺点:** - * 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。 - * 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。 - * 前置声明来自命名空间 ``std::`` 的 symbol 时,其行为未定义。 - * 很难判断什么时候该用前置声明,什么时候该用 ``#include`` 。极端情况下,用前置声明代替 ``#include`` 甚至都会暗暗地改变代码的含义: + * 前向声明隐藏了依赖关系, 可能会让人忽略头文件变化后必要的重新编译过程. + * 相比 ``#include``, 前向声明的存在会让自动化工具难以发现定义该符号的模块. + * 修改库 (library) 时可能破坏前向声明. 函数或模板的前向声明会阻碍头文件的负责人修改 API, 例如拓宽 (widening) 参数类型, 为模版参数添加默认值, 迁移到新的命名空间等等, 而这些操作本是无碍的. + * 为 ``std::`` 命名空间的符号提供前向声明会产生未定义行为 (undefined behavior). + * 很难判断什么时候该用前向声明, 什么时候该用 ``#include`` . 用前向声明代替 ``#include`` 时, 可能会悄然改变代码的含义: - .. code-block:: c++ + .. code-block:: c++ - // b.h: - struct B {}; - struct D : B {}; + // b.h: + struct B {}; + struct D : B {}; - // good_user.cc: - #include "b.h" - void f(B*); - void f(void*); - void test(D* x) { f(x); } // calls f(B*) + // good_user.cc: + #include "b.h" + void f(B*); + void f(void*); + void test(D* x) { f(x); } // 调用 f(B*) - 如果 ``#include`` 被 ``B`` 和 ``D`` 的前置声明替代, ``test()`` 就会调用 ``f(void*)`` . + 若用 ``B`` 和 ``D`` 的前向声明替代 ``#include``, ``test()`` 会调用 ``f(void*)`` . - * 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 ``include`` 冗长。 - * 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂. + * 为多个符号添加前向声明比直接 ``#include`` 更冗长. + * 为兼容前向声明而设计的代码 (比如用指针成员代替对象成员) 更慢更复杂. **结论:** - * 尽量避免前置声明那些定义在其他项目中的实体. - * 函数:总是使用 ``#include``. - * 类模板:优先使用 ``#include``. - -至于什么时候包含头文件,参见 :ref:`name-and-order-of-includes` 。 +尽量避免为其他项目定义的实体提供前向声明. .. _inline-functions: -1.4. 内联函数 +1.5. 内联函数 ~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 只有当函数只有 10 行甚至更少时才将其定义为内联函数. + 只把 10 行以下的小函数定义为内联 (inline). **定义:** - 当函数被声明为内联函数之后, 函数有可能会被内联(到底是否内联,由编译器自行决定)。 + 你可以通过声明让编译器展开内联函数, 而不是使用正常的函数调用机制. **优点:** - 只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联. + 只要内联函数体积较小, 内联函数可以令目标代码 (object code) 更加高效. 我们鼓励对存取函数 (accessors)、变异函数 (mutators) 和其它短小且影响性能的函数使用内联展开. **缺点:** - 滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。 + 滥用内联将拖慢程序. 根据函数体积, 内联可能会增加或减少代码体积. 通常, 内联展开非常短小的存取函数会减少代码大小, 但内联一个巨大的函数将显著增加代码大小. 在现代处理器上, 通常代码越小执行越快, 因为指令缓存利用率高. **结论:** - 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用! + 合理的经验法则是不要内联超过 10 行的函数. 谨慎对待析构函数. 析构函数往往比表面上更长, 因为会暗中调用成员和基类的析构函数! - 另一个实用的经验准则: 内联那些包含循环或 ``switch`` 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 ``switch`` 语句从不被执行). + 另一个实用的经验准则: 内联那些有循环或 ``switch`` 语句的函数通常得不偿失 (除非这些循环或 ``switch`` 语句通常不执行). - 有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数. + 应该注意, 即使函数被声明为内联函数, 也不一定真的会被内联; 比如, 通常虚函数和递归函数不会被内联. 通常不应该声明递归函数为内联. (YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 为虚函数声明内联的主要目的是在类 (class) 中定义该函数, 以便于使用该函数或注释其行为. 这常用于存取函数和变异函数. .. _name-and-order-of-includes: -1.5. ``#include`` 的路径及顺序 +1.6. ``#include`` 的路径及顺序 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 `.h`, 本项目内的 `.h`. -项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 ``.`` (当前目录) 或 ``..`` (上级目录). 例如, ``google-awesome-project/src/base/logging.h`` 应该按如下方式包含: + 推荐按照以下顺序导入头文件: 配套的头文件, C 语言系统库头文件, C++ 标准库头文件, 其他库的头文件, 本项目的头文件. + +头文件的路径应相对于项目源码目录, 不能出现 UNIX 目录别名 (alias) ``.`` (当前目录) 或 ``..`` (上级目录). 例如, 应该按如下方式导入 ``google-awesome-project/src/base/logging.h``: .. code-block:: c++ #include "base/logging.h" -又如, ``dir/foo.cc`` 或 ``dir/foo_test.cc`` 的主要作用是实现或测试 ``dir2/foo2.h`` 的功能, ``foo.cc`` 中包含头文件的次序如下: +在 ``dir/foo.cc`` 或 ``dir/foo_test.cc`` 这两个实现或测试 ``dir2/foo2.h`` 内容的文件中, 按如下顺序导入头文件: - #. ``dir2/foo2.h`` (优先位置, 详情如下) - #. C 系统文件 - #. C++ 系统文件 - #. 其他库的 ``.h`` 文件 - #. 本项目内 ``.h`` 文件 + #. ``dir2/foo2.h``. + #. 空行 + #. C 语言系统文件 (确切地说: 用使用方括号和 ``.h`` 扩展名的头文件), 例如 ```` 和 ````. + #. 空行 + #. C++ 标准库头文件 (不含扩展名), 例如 ```` 和 ````. + #. 空行 + #. 其他库的 ``.h`` 文件. + #. 空行 + #. 本项目的 ``.h`` 文件. -这种优先的顺序排序保证当 ``dir2/foo2.h`` 遗漏某些必要的库时, ``dir/foo.cc`` 或 ``dir/foo_test.cc`` 的构建会立刻中止。因此这一条规则保证维护dir这些文件的人们首先看到构建中止的消息,而不是维护其他包的人们。 +每个非空的分组之间用空行隔开. -``dir/foo.cc`` 和 ``dir2/foo2.h`` 通常位于同一目录下 (如 ``base/basictypes_unittest.cc`` 和 ``base/basictypes.h``), 但也可以放在不同目录下. +这种顺序可以确保在 ``dir2/foo2.h`` 缺少必要的导入时, 构建 (build) ``dir/foo.cc`` 或 ``dir/foo_test.cc`` 会失败. 这样维护这些文件的人会首先发现构建失败, 而不是维护其他库的无辜的人. -按字母顺序分别对每种类型的头文件进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。 +``dir/foo.cc`` 和 ``dir2/foo2.h`` 通常位于同一目录下 (如 ``base/basictypes_unittest.cc`` 和 ``base/basictypes.h``), 但有时也放在不同目录下. -您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,`前置声明`__ (forward declarations) 情况除外。比如您要用到 ``bar.h`` 中的某个符号, 哪怕您所包含的 ``foo.h`` 已经包含了 ``bar.h``, 也照样得包含 ``bar.h``, 除非 ``foo.h`` 有明确说明它会自动向您提供 ``bar.h`` 中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 ``foo.cc`` 只包含 ``foo.h`` 就够了,不用再管后者所包含的其它内容。 +注意 C 语言头文件 (如 ``stddef.h``) 和对应的 C++ 头文件 (``cstddef``) 是等效的. 两种风格都可以接受, 但是最好和现有代码保持一致. -__ forward-declarations_ +每个分组内部的导入语句应该按字母序排列. 注意旧代码可能没有遵守这条规则, 应该在方便时进行修正. -举例来说, ``google-awesome-project/src/foo/internal/fooserver.cc`` 的包含次序如下: +举例来说, ``google-awesome-project/src/foo/internal/fooserver.cc`` 的导入语句如下: - .. code-block:: c++ + .. code-block:: c++ - #include "foo/public/fooserver.h" // 优先位置 + #include "foo/server/fooserver.h" - #include - #include + #include + #include - #include - #include + #include + #include - #include "base/basictypes.h" - #include "base/commandlineflags.h" - #include "foo/public/bar.h" + #include "base/basictypes.h" + #include "foo/server/bar.h" + #include "third_party/absl/flags/flag.h" **例外:** -有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如: +有时平台相关的 (system-specific) 代码需要有条件地导入 (conditional include),此时可以在其他导入语句后放置条件导入语句. 当然, 尽量保持平台相关的代码简洁且影响范围小. 例如: - .. code-block:: c++ + .. code-block:: c++ - #include "foo/public/fooserver.h" + #include "foo/public/fooserver.h" - #include "base/port.h" // For LANG_CXX11. + #include "base/port.h" // 为了 LANG_CXX11. - #ifdef LANG_CXX11 - #include - #endif // LANG_CXX11 + #ifdef LANG_CXX11 + #include + #endif // LANG_CXX11 译者 (YuleFox) 笔记 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/google-cpp-styleguide/index.rst b/google-cpp-styleguide/index.rst index 50d0c18..d69dda4 100644 --- a/google-cpp-styleguide/index.rst +++ b/google-cpp-styleguide/index.rst @@ -1,7 +1,7 @@ -0. 扉页 +扉页 ============ -:版本: 4.45 +:版本: 2024/02/18 :原作者: @@ -21,13 +21,14 @@ `Yang.Y `_ `acgtyrant `_ `lilinsanity `_ + `楼宇 `_ :项目主页: - `Google Style Guide `_ - `Google 开源项目风格指南 - 中文版 `_ -0.1 译者前言 +译者前言 -------------------- Google 经常会发布一些开源项目, 意味着会接受来自其他代码贡献者的代码. 但是如果代码贡献者的编程风格与 Google 的不一致, 会给代码阅读者和其他代码提交者造成不小的困扰. Google 因此发布了这份自己的编程风格指南, 使所有提交代码的人都能获知 Google 的编程风格. @@ -60,20 +61,69 @@ Google 经常会发布一些开源项目, 意味着会接受来自其他代码 - 2008-07 1.0 : 出自 `YuleFox 的 Blog `_, 很多地方摘录的也是该版本. +以下是正文. -0.2 背景 +背景 -------------- -C++ 是 Google 大部分开源项目的主要编程语言. 正如每个 C++ 程序员都知道的, C++ 有很多强大的特性, 但这种强大不可避免的导致它走向复杂,使代码更容易产生 bug, 难以阅读和维护. +C++ 是谷歌的开源项目所采用的主要编程语言之一. C++ 程序员都知道, 该语言有很多强大的特性 (feature), 但强大之处也伴随着复杂性, 让代码容易出错, 难以阅读、维护. -本指南的目的是通过详细阐述 C++ 注意事项来驾驭其复杂性. 这些规则在保证代码易于管理的同时, 也能高效使用 C++ 的语言特性. +本指南的目标是详述 C++ 的注意事项来控制复杂性. 这些规则会在保持代码易于管理的同时, 不影响程序员高效地使用 C++ 的语言特性. -*风格*, 亦被称作可读性, 也就是指导 C++ 编程的约定. 使用术语 "风格" 有些用词不当, 因为这些习惯远不止源代码文件格式化这么简单. +风格 (style, 亦称作可读性 (readability)) 是用于管理 C++ 代码的惯例. "风格" 这一术语略有不准确, 因为这些惯例并非仅仅囊括代码格式. -使代码易于管理的方法之一是加强代码一致性. 让任何程序员都可以快速读懂你的代码这点非常重要. 保持统一编程风格并遵守约定意味着可以很容易根据 "模式匹配" 规则来推断各种标识符的含义. 创建通用, 必需的习惯用语和模式可以使代码更容易理解. 在一些情况下可能有充分的理由改变某些编程风格, 但我们还是应该遵循一致性原则,尽量不这么做. +谷歌主导的大部分开源项目遵守本指南的要求. -本指南的另一个观点是 C++ 特性的臃肿. C++ 是一门包含大量高级特性的庞大语言. 某些情况下, 我们会限制甚至禁止使用某些特性. 这么做是为了保持代码清爽, 避免这些特性可能导致的各种问题. 指南中列举了这类特性, 并解释为什么这些特性被限制使用. +注意: 本指南并非 C++ 教程, 我们假定读者已经非常熟悉 C++. -Google 主导的开源项目均符合本指南的规定. +本指南的目标 +------------------ -注意: 本指南并非 C++ 教程, 我们假定读者已经对 C++ 非常熟悉. +为什么编写这份文档? + +我们认为该指南应该实现以下核心目标. 这些目标是每条规则背后的基本 **依据** . 我们希望公开这些想法, 作为讨论的基础, 让广大社区了解每条规则和特定决策背后的来由. 在理解规则所服务的目标以后, 所有人都应该清楚某条规则在哪些情况下可以忽略 (有些规则可以忽略), 以及改变规则时需要提供怎样的论据和替代品. + +我们认为风格指南当前的目标如下: + +风格规则应该有影响力 + + 一条风格规则应该具备足够大的好处, 以至于值得所有工程师铭记. 所谓好处是相对于当前代码库的状态而言的, 所以即使某一习惯十分恶劣, 如果人们很少使用, 那么禁止这一习惯的好处依然很小. 这样可以解释为什么我们没有写下某些规则. 例如, ``goto`` 语句违背了许多原则, 但是现在已经很少出现, 所以风格指南不会讨论它. + +为读者优化, 而非为作者优化 + + 我们的代码库 (以及其中的每个组件) 应该会存在很长时间. 因此, 我们读代码的时间比写代码的时间更长. 我们明确地选择优化平均水平的软件工程师阅读、维护和调试代码的体验, 而非编写代码的舒适度. "为读者留下线索" 是这一理念的一个方面. 如果代码中有特殊的情况 (例如指针所有权转移), 在此处给读者留下的文字提示很有价值 (在代码中使用 ``std::unique_ptr`` 就明确地表达了所有权转移). + +和现有代码保持一致 + + 我们让代码库的风格保持整体一致, 就能聚焦在其他 (更有价值的) 问题上. 一致性也会帮助自动化: 那些格式化代码或者调整 ``#include`` 顺序的工具, 只能在你的代码符合预期时才能正常工作. 很多时候, 那些用于 "保持一致" 的规则本质上就是 "任选其一并停止内耗"; 在这些问题上, 争论的成本超过了提供自由度的价值. 不过, 一致性原则也有局限性. 在没有清晰的技术性论据和长远方向时, 这才是很好的打破平局的方式. 这一原则适合局部使用 (一个文件内, 或者一组关联性强的接口). 不应该为了一致性而采用旧风格, 忽视新风格的好处. 应该考虑到代码库会随时间推移而过渡到新风格. + +恰当时与广大 C++ 社区保持一致 + + 与其他组织保持一致性是有价值的, 这和我们保持内部一致性的原因一样. 如果 C++ 标准中的特性解决了某个问题, 或者某一范式被广泛采用, 这就是采纳它们的依据. 不过, 有时标准的特性和范式有缺陷, 或者在设计上没有考虑我们代码库的需求. 此时 (正如下文所描述的) 应该限制或者禁止这些标准特性. 有时, 相较于 C++ 标准库, 我们偏向于自研库或某些第三方库. 一般这是因为我们所选择的库具有优越性, 或者迁移到标准库的价值不值得那些工作量. + +避免使用奇特或危险的语法结构 + + 有些 C++ 的特性比表面上更加奇特或危险. 风格指南中的一些限制就是为了防止掉入这些陷阱. 你需要达到很高的标准才能豁免这些限制, 因为忽略这些规则就很可能直接引起程序错误. + +避免使用那些正常水平的 C++ 程序员认为棘手或难以维护的语法结构 + + 有些 C++ 特性会给代码带来复杂性, 因此通常不适合使用. 在用途广泛的代码中, 我们可以接受更巧妙的语法结构. 这是因为复杂的实现方式所带来的收益会被众多使用者放大, 而且在编写新代码时, 也不需要重新解读这些复杂的语法. 如有疑问, 可以请求项目主管豁免这些规则. 这对我们的代码库至关重要, 因为代码负责人和团队成员会变化: 即使所有现在修改这段代码的人都理解代码, 几年后人们就不一定还能理解了. + +需要注意我们的规模 + + 我们有上亿行代码和成千上万的工程师, 因此一位工程师的失误或者投机取巧的行为会成为很多人的负担. 举例来说, 一定要避免污染全局命名空间 (global namespace): 如果所有人都往全局命名空间里塞东西, 就很难避免上亿行代码之间的符号冲突 (name collision), 也难以修复冲突. + +在必要时为优化让路 + + 即使性能优化的手段会和此文档的其他理念冲突, 有时这些手段也是必要且恰当的. + +此文档的目的是提供最大程度的指导和合理限制. 和往常一样, 你应该追随常理和正常审美. 这里我们特指整个谷歌 C++ 社区所建立的规范, 而不是你个人或者所在团队的偏好. 应该对巧妙或奇特的语法结构保持怀疑和犹豫的态度: 并不是 "法无禁止即可为". 运用你的判断力. 如有疑惑, 请不要犹豫, 随时向项目主管咨询意见. + +C++ 版本 +------------------ + +目前代码的目标版本是 C++20, 所以不应该使用 C++23 的特性. 本指南的 C++ 目标版本会随时间 (激进地) 升级. + +禁止使用非标准扩展. + +在使用 C++17 和 C++20 的特性之前, 需要权衡其他环境的可移植性. diff --git a/google-cpp-styleguide/scoping.rst b/google-cpp-styleguide/scoping.rst index 2912243..db1996e 100644 --- a/google-cpp-styleguide/scoping.rst +++ b/google-cpp-styleguide/scoping.rst @@ -8,142 +8,161 @@ .. tip:: - 鼓励在 ``.cc`` 文件内使用匿名命名空间或 ``static`` 声明. 使用具名的命名空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。 + 除了少数特殊情况, 应该在命名空间 (namespace) 内放置代码. 命名空间应该有独一无二的名字, 其中包含项目名称, 也可以选择性地包含文件路径. 禁止使用 using 指令 (例如 ``using namespace foo``). 禁止使用内联 (inline) 命名空间. 请参见 :ref:`内部链接 ` 中关于匿名命名空间 (unamed namespace) 的内容. **定义:** - 命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突. + 命名空间可以将全局作用域 (global scope) 划分为独立的、有名字的作用域, 因此可以有效防止全局作用域中的命名冲突 (name collision). **优点:** - 虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 命名空间在这基础上又封装了一层. + 命名空间可以避免大型程序中的命名冲突, 同时代码可以继续使用简短的名称. - 举例来说, 两个不同项目的全局作用域都有一个类 ``Foo``, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同命名空间中, ``project1::Foo`` 和 ``project2::Foo`` 作为不同符号自然不会冲突. + 举例来说, 若两个项目的全局作用域中都有一个叫 ``Foo`` 的类 (class), 这两个符号 (symbol) 会在编译或运行时发生冲突. 如果每个项目在不同的命名空间中放置代码, ``project1::Foo`` 和 ``project2::Foo`` 就是截然不同的符号, 不会冲突. - 内联命名空间会自动把内部的标识符放到外层作用域,比如: + 内联命名空间会自动把其中的标识符置入外层作用域, 比如: .. code-block:: c++ - namespace X { - inline namespace Y { - void foo(); - } // namespace Y - } // namespace X + namespace outer { + inline namespace inner { + void foo(); + } // namespace inner + } // namespace outer - ``X::Y::foo()`` 与 ``X::foo()`` 彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。 + 此时表达式 ``outer::inner::foo()`` 与 ``outer::foo()`` 等效. 内联命名空间的主要用途是保持不同 ABI 版本之间的兼容性。 **缺点:** - 命名空间具有迷惑性, 因为它们使得区分两个相同命名所指代的定义更加困难。 + 命名空间让人难以理解, 因为难以找到一个标识符所对应的定义. - 内联命名空间很容易令人迷惑,毕竟其内部的成员不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。 + 内联命名空间更难理解, 因为其中的标识符不仅仅出现在声明它的命名空间中. 因此内联命名空间只能作为版本控制策略的一部分. - 有时候不得不多次引用某个定义在许多嵌套命名空间里的实体,使用完整的命名空间会导致代码的冗长。 - - 在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)). + 部分情景下, 我们必须多次使用完全限定名称 (fully-qualified name) 来引用符号. 此时多层嵌套的命名空间会让代码冗长. **结论:** - 根据下文将要提到的策略合理使用命名空间. + 建议按如下方法使用命名空间: - - 遵守 `命名空间命名 `_ 中的规则。 + - 遵守 `命名空间命名 `_ 规则. - - 像之前的几个例子中一样,在命名空间的最后注释出命名空间的名字。 + - 像前面的例子一样, 用注释给命名空间收尾. (译者注: 注明命名空间的名字.) - - 用命名空间把文件包含, `gflags `_ 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其它命名空间: + - 在导入语句、 `gflags `_ 声明/定义以及其他命名空间的类的前向声明 (forward declaration) 之后, 用命名空间包裹整个源代码文件: .. code-block:: c++ // .h 文件 namespace mynamespace { - // 所有声明都置于命名空间中 - // 注意不要使用缩进 + // 所有声明都位于命名空间中. + // 注意没有缩进. class MyClass { public: ... void Foo(); }; - } // namespace mynamespace + } // namespace mynamespace .. code-block:: c++ // .cc 文件 namespace mynamespace { - // 函数定义都置于命名空间中 + // 函数定义位于命名空间中. void MyClass::Foo() { ... } - } // namespace mynamespace + } // namespace mynamespace - 更复杂的 ``.cc`` 文件包含更多, 更复杂的细节, 比如 gflags 或 using 声明。 + 更复杂的 ``.cc`` 文件有更多细节, 比如旗标 (flag) 或 using 声明. .. code-block:: c++ #include "a.h" - DEFINE_FLAG(bool, someflag, false, "dummy flag"); + DEFINE_FLAG(bool, someflag, false, "某个旗标"); - namespace a { + namespace mynamespace { - ...code for a... // 左对齐 + using ::foo::Bar; - } // namespace a + ...命名空间内的代码... // 代码紧贴左边框. - - 不要在命名空间 ``std`` 内声明任何东西, 包括标准库的类前置声明. 在 ``std`` 命名空间声明实体是未定义的行为, 会导致如不可移植. 声明标准库下的实体, 需要包含对应的头文件. + } // namespace mynamespace - - 不应该使用 *using 指示* 引入整个命名空间的标识符号。 + - 若要将自动生成的 proto 消息代码放入命名空间, 可以在 ``.proto`` 文件中使用 ``package`` 修饰符 (specifier). 参见 `Protocol Buffer 的包 `_. + + - 不要在 ``std`` 命名空间内声明任何东西. 不要前向声明 (forward declare) 标准库的类. 在 ``std`` 命名空间内声明实体是未定义行为 (undefined behavior), 也就是会损害可移植性. 若要声明标准库的实体, 应该导入对应的头文件. + + - 禁止使用 *using 指令* 引入命名空间的所有符号。 .. code-block:: c++ - // 禁止 —— 污染命名空间 + // 禁止: 这会污染命名空间. using namespace foo; - - 不要在头文件中使用 *命名空间别名* 除非显式标记内部命名空间使用。因为任何在头文件中引入的命名空间都会成为公开API的一部分。 + - 除了在明显标注为内部使用的命名空间内, 不要让头文件引入命名空间别名 (namespace alias). 这是因为头文件的命名空间中引入的任何东西都是该文件的公开 API. 正确示例: .. code-block:: c++ - // 在 .cc 中使用别名缩短常用的命名空间 + // 在 .cc 中, 用别名缩略常用的名称. namespace baz = ::foo::bar::baz; .. code-block:: c++ - // 在 .h 中使用别名缩短常用的命名空间 + // 在 .h 中, 用别名缩略常用的命名空间. namespace librarian { - namespace impl { // 仅限内部使用 + namespace impl { // 仅限内部使用, 不是 API. namespace sidetable = ::pipeline_diagnostics::sidetable; } // namespace impl inline void my_inline_function() { - // 限制在一个函数中的命名空间别名 + // 一个函数 (f或方法) 中的局部别名. namespace baz = ::foo::bar::baz; ... } } // namespace librarian - - 禁止用内联命名空间 + - 禁止内联命名空间. -.. _unnamed-namespace-and-static-variables: + - 如果命名空间的名称包含 "internal", 代表用户不应该使用这些 API. -2.2. 匿名命名空间和静态变量 + .. code-block:: c++ + + // Absl 以外的代码不应该使用这一内部符号. + using ::absl::container_internal::ImplementationDetail; + + - 我们鼓励新的代码使用单行的嵌套命名空间声明, 但不强制要求. + + 译者注: 例如 + + .. code-block:: c++ + + namespace foo::bar { + ... + } // namespace foo::bar + +.. _internal-linkage: + +2.2. 内部链接 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. tip:: - 在 ``.cc`` 文件中定义一个不需要被外部引用的变量时,可以将它们放在匿名命名空间或声明为 ``static`` 。但是不要在 ``.h`` 文件中这么做。 + 若其他文件不需要使用 ``.cc`` 文件中的定义, 这些定义可以放入匿名命名空间 (unnamed namespace) 或声明为 ``static``, 以实现内部链接 (internal linkage). 但是不要在 ``.h`` 文件中使用这些手段. **定义:** - 所有置于匿名命名空间的声明都具有内部链接性,函数和变量可以经由声明为 ``static`` 拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。 + 所有放入匿名命名空间中的声明都会内部链接. 声明为 ``static`` 的函数和变量也会内部链接. 这意味着其他文件不能访问你声明的任何事物. 即使另一个文件声明了一模一样的名称, 这两个实体也都是相互独立的. **结论:** - 推荐、鼓励在 ``.cc`` 中对于不需要在其他地方引用的标识符使用内部链接性声明,但是不要在 ``.h`` 中使用。 + 建议 ``.cc`` 文件中所有不需要外部使用的代码采用内部链接. 不要在 ``.h`` 文件中使用内部链接. - 匿名命名空间的声明和具名的格式相同,在最后注释上 ``namespace`` : + 匿名命名空间的声明应与具名命名空间的格式相同. 在末尾的注释中, 不用填写命名空间名称: .. code-block:: c++ @@ -158,44 +177,21 @@ .. tip:: - 使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关. + 建议将非成员 (nonmember) 函数放入命名空间; 尽量不要使用完全全局的函数 (completely global function). 不要仅仅为了给静态成员 (static member) 分组而使用类 (class). 类的静态方法应当和类的实例或静态数据紧密相关. **优点:** - 某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在命名空间内可避免污染全局作用域. + 非成员函数和静态成员函数在某些情况下有用. 若将非成员函数放在命名空间内, 不会污染全局命名空间. **缺点:** - 将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此. + 有时非成员函数和静态成员函数更适合成为一个新的类的成员, 尤其是当它们需要访问外部资源或有明显的依赖关系时. **结论:** - 有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 :ref:`namespaces` 。举例而言,对于头文件 ``myproject/foo_bar.h`` , 应当使用 + 有时我们需要定义一个和类的实例无关的函数. 这样的函数可以定义为静态成员函数或非成员函数. 非成员函数不应该依赖外部变量, 且大部分情况下应该位于命名空间中. 不要仅仅为了给静态成员分组而创建一个新类; 这相当于给所有名称添加一个公共前缀, 而这样的分组通常是不必要的. - .. code-block:: c++ - - namespace myproject { - namespace foo_bar { - void Function1(); - void Function2(); - } // namespace foo_bar - } // namespace myproject - - 而非 - - .. code-block:: c++ - - namespace myproject { - class FooBar { - public: - static void Function1(); - static void Function2(); - }; - } // namespace myproject - - 定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的命名空间内. - - 如果你必须定义非成员函数, 又只是在 ``.cc`` 文件中使用它, 可使用匿名 :ref:`namespaces` 或 ``static`` 链接关键字 (如 ``static int Foo() {...}``) 限定其作用域. + 如果你定义的非成员函数仅供本 ``.cc`` 文件使用, 请用 :ref:`内部链接 ` 限制其作用域. .. _local-variables: @@ -204,52 +200,63 @@ .. tip:: - 将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化. + 应该尽可能缩小函数变量的作用域 (scope), 并在声明的同时初始化. -C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如: +你可以在 C++ 函数的任何位置声明变量. 我们提倡尽可能缩小变量的作用域, 且声明离第一次使用的位置越近越好. 这样读者更容易找到声明, 了解变量的类型和初始值. 特别地, 应该直接初始化变量而非先声明再赋值, 比如: .. code-block:: c++ int i; - i = f(); // 坏——初始化和声明分离 + i = f(); // 不好: 初始化和声明分离. .. code-block:: c++ - int j = g(); // 好——初始化时声明 + int i = f(); // 良好: 声明时初始化​。 + + .. code-block:: c++ + + int jobs = NumJobs(); + // 更多代码... + f(jobs); // 不好: 初始化和使用位置分离. + + .. code-block:: c++ + + int jobs = NumJobs(); + f(jobs); // 良好: 初始化以后立即 (或很快) 使用. .. code-block:: c++ vector v; - v.push_back(1); // 用花括号初始化更好 + v.push_back(1); // 用花括号初始化更好. v.push_back(2); .. code-block:: c++ - vector v = {1, 2}; // 好——v 一开始就初始化 + vector v = {1, 2}; // 良好: 立即初始化 v. -属于 ``if``, ``while`` 和 ``for`` 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了,举例而言: +通常应该在语句内声明用于 ``if``、``while`` 和 ``for`` 语句的变量, 这样会把作用域限制在语句内. 例如: .. code-block:: c++ while (const char* p = strchr(str, '/')) str = p + 1; -.. warning:: 有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低. +需要注意的是, 如果变量是一个对象, 那么它每次进入作用域时会调用构造函数, 每次退出作用域时都会调用析构函数. .. code-block:: c++ - // 低效的实现 + // 低效的实现: for (int i = 0; i < 1000000; ++i) { - Foo f; // 构造函数和析构函数分别调用 1000000 次! + Foo f; // 调用 1000000 次构造函数和析构函数. f.DoSomething(i); } -在循环作用域外面声明这类变量要高效的多: +在循环的作用域外面声明这类变量更高效: .. code-block:: c++ - Foo f; // 构造函数和析构函数只调用 1 次 + Foo f; // 调用 1 次构造函数和析构函数. for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); } @@ -261,30 +268,169 @@ C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的 .. tip:: - 禁止定义静态储存周期非POD变量,禁止使用含有副作用的函数初始化POD全局变量,因为多编译单元中的静态变量执行时的构造和析构顺序是未明确的,这将导致代码的不可移植。 + 禁止使用 `静态储存周期 (static storage duration) `_ 的变量, 除非它们可以 `平凡地析构 (trivially destructible) `_. 简单来说, 就是析构函数 (destructor) 不会做任何事情, 包括成员和基类 (base) 的析构函数. 正式地说, 就是这一类型 (type) 没有用户定义的析构函数或虚析构函数 (virtual destructor), 且所有成员和基类也能平凡地析构. 函数的局部静态变量可以动态地初始化 (dynamic initialization) . 除了少数情况外, 不推荐动态初始化静态类成员变量或命名空间内的变量. 详情参见下文. -禁止使用类的 `静态储存周期 `_ 变量:由于构造和析构函数调用顺序的不确定性,它们会导致难以发现的 bug 。不过 ``constexpr`` 变量除外,毕竟它们又不涉及动态初始化或析构。 +作为经验之谈: 若只看全局变量的声明, 如果该语句可以作为常量表达式 (constexpr), 则满足以上要求. -静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。 +**定义:** -静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是只有部分明确的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数(比如 ``getenv()`` 或 ``getpid()`` )不涉及任何全局变量。函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。 + 每个对象 (object) 都有与生命周期 (linetime) 相关的储存周期 (storage duration). 静态储存周期对象的存活时间是从程序初始化开始, 到程序结束为止. 这些对象可能是命名空间作用域内的变量 (全局变量)、类的静态数据成员或者用 ``static`` 修饰符 (specifier) 声明的函数局部变量. 对于函数局部静态变量, 初始化发生在在控制流第一次经过声明时; 所有其他对象会在程序启动时初始化. 程序退出时会销毁所有静态储存周期的对象 (这发生在未汇合 (join) 的线程终止前). -.. note:: Xris 译注: + 初始化过程可以是动态 (dynamic) 的, 也就是初始化过程中有不平凡 (non-trivial) 的操作. (例如, 会分配内存的构造函数, 或者用当前进程 ID 初始化的变量.) 其他初始化都是静态 (static) 初始化. 二者并非水火不容: 静态储存周期的变量 **一定** 会静态初始化 (初始化为指定常量或给所有字节清零), 必要时会随后再次动态初始化. - 同一个编译单元内是明确的,静态初始化优先于动态初始化,初始化顺序按照声明顺序进行,销毁则逆序。不同的编译单元之间初始化和销毁顺序属于未明确行为 (unspecified behaviour)。 +**优点:** + 全局或静态变量对很多场景有帮助: 具名常量 (named constants)、编译单元 (translation unit) 内部的辅助数据结构、命令行旗标 (flag)、日志、注册机制、后台基础设施等等. -同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 ``main()`` 返回还是对 ``exit()`` 的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。 +**缺点:** -改善以上析构问题的办法之一是用 ``quick_exit()`` 来代替 ``exit()`` 并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 ``atexit()`` 所绑定的任何 handlers. 如果您想在执行 ``quick_exit()`` 来中断时执行某 handler(比如刷新 log),您可以把它绑定到 ``_at_quick_exit()``. 如果您想在 ``exit()`` 和 ``quick_exit()`` 都用上该 handler, 都绑定上去。 + 使用动态初始化或具有非平凡析构函数的全局和静态变量时, 会增加代码复杂度, 容易引发难以察觉的错误. 不同编译单元的动态初始化顺序不确定, 析构顺序也不确定 (只知道析构顺序一定是初始化顺序的逆序). 如果静态变量的初始化代码引用了另一个静态储存周期的变量, 这次访问可能发生在另一变量的生命周期开始前 (或生命周期结束后). 此外, 若有些线程没有在程序结束前汇合, 这些线程可能在静态变量析构后继续访问这些变量. -综上所述,我们只允许 POD 类型的静态变量,即完全禁用 ``vector`` (使用 C 数组替代) 和 ``string`` (使用 ``const char []``)。 +**决定:** -如果您确实需要一个 ``class`` 类型的静态或全局变量,可以考虑在 ``main()`` 函数或 ``pthread_once()`` 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。 + **关于析构的决定** -.. note:: Yang.Y 译注: + 平凡的析构函数不受执行顺序影响 (他们实际上不算"执行"); 其他析构函数则有风险, 可能访问生命周期已结束的对象. 因此, 只有拥有平凡析构函数的对象才能采用静态储存周期. 基本类型 (例如指针和 ``int``) 可以平凡地析构, 可平凡析构的类型所构成的数组也可以平凡地析构. 注意, 用 ``constexpr`` 修饰的变量可以平凡地析构. - 上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量. + .. code-block:: c++ + + const int kNum = 10; // 允许 + + struct X { int n; }; + const X kX[] = {{1}, {2}, {3}}; // 允许 + + void foo() { + static const char* const kMessages[] = {"hello", "world"}; // 允许 + } + + // 允许: constexpr 可以保证析构函数是平凡的. + constexpr std::array kArray = {1, 2, 3}; + + .. code-block:: c++ + + // 不好: 非平凡的析构. + const std::string kFoo = "foo"; + + // 和上面相同的原因, 即使 kBar 是引用 (该规则也适用于生命周期被延长的临时对象). + const std::string& kBar = StrCat("a", "b", "c"); + + void bar() { + // 不好: 非平凡的析构. + static std::map kData = {{1, 0}, {2, 0}, {3, 0}}; + } + + 注意, 引用不是对象, 因此它们的析构函数不受限. 但是, 它们仍需遵守动态初始化的限制. 特别地, 我们允许形如 ``static T& t = *new T;`` 的函数内局部静态引用. + + **关于初始化的决定** + + 初始化是更复杂的话题, 因为我们不仅需要考虑构造函数的执行过程, 也要考虑初始化表达式 (initializer) 的求值过程. + + .. code-block:: c++ + + int n = 5; // 可以 + int m = f(); // ? (依赖 f) + Foo x; // ? (依赖 Foo::Foo) + Bar y = g(); // ? (依赖 g 和 Bar::Bar) + + 除了第一行语句以外, 其他语句都会受到不确定的初始化顺序影响. + + 我们所需的概念在 C++ 标准中的正式称谓是常量初始化 (constant initialization). 这意味着初始化表达式是常量表达式 (constant expression), 并且如果要用构造函数进行初始化, 则该构造函数也必须声明为 ``constexpr``: + + .. code-block:: c++ + + struct Foo { constexpr Foo(int) {} }; + + int n = 5; // 可以, 5 是常量表达式. + Foo x(2); // 可以, 2 是常量表达式且被选中的构造函数也是 constexpr. + Foo a[] = { Foo(1), Foo(2), Foo(3) }; // 可以 + + 可以自由使用常量初始化. 应该用 ``constexpr`` 或 ``constinit`` 标记静态变量的常量初始化过程. 应该假设任何没有这些标记的静态变量都是动态初始化的, 并谨慎地检查这些代码. + + 作为反例, 以下初始化过程有问题: + + .. code-block:: c++ + + // 下文使用了这些声明. + time_t time(time_t*); // 不是 constexpr! + int f(); // 不是 constexpr! + struct Bar { Bar() {} }; + + // 有问题的初始化. + time_t m = time(nullptr); // 初始化表达式不是常量表达式. + Foo y(f()); // 同上 + Bar b; // 被选中的构造函数 Bar::Bar() 不是 constexpr. + + 我们不建议且通常禁止动态地初始化全局变量. 不过, 如果这一初始化过程不依赖于其他初始化过程的顺序, 则可以允许. 若满足这一要求, 则初始化的顺序变化不会产生任何区别. 例如: + + .. code-block:: c++ + + int p = getpid(); // 若其他静态变量不会在初始化过程中使用 p, 则允许. + + 允许动态地初始化静态局部变量 (这是常见的). + + **常用的语法结构** + + - 全局字符串: 如果你需要具名的 (named) 全局或静态字符串常量, 可以采用 ``constexpr`` 修饰的 ``string_view`` 变量、字符数组或指向字符串字面量 (literal) 的字符指针. 字符串字面量具有静态储存周期, 因此通常能满足需要. 参见 `第 140 号每周提示 `_. + - 字典和集合等动态容器 (container): 若你需要用静态变量储存不会改变的数据 (例如用于搜索的集合或查找表), 不要使用标准库的动态容器, 因为这些容器拥有非平凡的析构函数. 可以考虑用平凡类型的数组替代, 例如 ``int`` 数组的数组 (作为把 ``int`` 映射到 ``int`` 的字典) 或者数对 (pair) 的数组 (例如一组 ``int`` 和 ``const char*`` 的数对). 对于少量数据, 线性搜索就足够了, 而且因为具有内存局部性 (memory locality) 而更加高效; 可以使用 `absl/algorithm/container.h `_ 中的工具实现常见操作. 如有需要, 可以保持数据有序并采用二分查找法 (binary search). 如果你确实需要使用标准库的动态容器, 建议使用如下文所述的函数内局部静态指针. + - 智能指针 (smart pointer, 例如 ``std::unique_ptr`` 和 ``std::shared_ptr``) 在析构时有释放资源的操作, 因此不能作为静态变量. 请思考你的情景是否适用于本小节描述的其他模式. 简单的解决方式是, 用裸指针 (plain pointer) 指向动态分配的对象, 并且永远不删除这个对象 (参见最后一点). + - 自定义类型的静态变量: 如果静态数据或常量数据是自定义类型, 请给这一类型设置平凡的析构函数和 ``constexpr`` 修饰的构造函数. + - 若以上都不适用, 你可以采用函数内局部静态指针或引用, 动态分配一个对象且永不删除 (例如 ``static const auto& impl = *new T(args...);``). + +2.6. thread_local 变量 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. tip:: + + 必须使用编译期常量 (compile-time constant) 初始化在函数外定义的 ``thread_local`` 变量, 且必须使用 `ABSL_CONST_INIT `_ 属性来强制执行这一规则. 优先采用 ``thread_local``, 而非其他定义线程内局部数据的方法. + +**定义:** + + 我们可以用 ``thread_local`` 修饰符声明变量: + + .. code-block:: c++ + + thread_local Foo foo = ...; + + 这样的变量本质上其实是一组不同的对象. 不同线程访问该变量时, 会访问各自的对象. ``thread_local`` 变量在很多方面类似于 :ref:`静态储存周期的变量 `. 例如, 可以在命名空间内、函数内或类的静态成员内声明这些变量, 但不能在类的普通成员内声明它们. + + ``thread_local`` 实例与静态变量的初始化过程类似, 区别是 ``thread_local`` 实例会在每个线程启动时初始化, 而非程序启动时初始化. 这意味着函数内的 ``thread_local`` 变量是线程安全的. 若要访问其他 ``thread_local`` 变量, 则有跟静态变量一样的初始化顺序问题 (而且问题更大). + + ``thread_local`` 的变量也有微妙的析构顺序问题: 线程终止时, ``thread_local`` 的销毁顺序是初始化顺序的逆序 (正如 C++ 在其他部分的规则一样). 如果 ``thread_local`` 的变量在析构过程中访问了该线程中已销毁的其他 ``thread_local`` 变量, 就会出现难以调试的释放后使用 (use-after-free, 即野指针) 问题. + +**优点:** + + - 线程的局部数据可以从根本上防止竞态条件 (race) (因为通常只有一个线程访问), 因此 ``thread_local`` 能帮助并行化. + - 在创建线程局部数据的各种方法中, ``thread_local`` 是由语法标准支持的唯一方法. + +**缺点:** + + - 在线程启动或首次使用 ``thread_local`` 变量时, 可能触发很多难以预测、运行时间不可控的其他代码. + - ``thread_local`` 本质上是全局变量. 除了线程安全以外, 它具有全局变量的所有其他缺点. + - 在最坏情况下, ``thread_local`` 变量占用的内存与线程数量成正比, 占用量可能十分巨大. + - 成员数据 (data member) 必须是静态的才能声明为 ``thread_local``. + - 若 ``thread_local`` 变量拥有复杂的析构函数, 我们可能遇到野指针. 特别地, 析构函数不能 (直接或间接地) 访问任何有可能已被销毁的其他 ``thread_local`` 变量. 我们难以检查这一规则. + - 那些用于全局/静态变量的、预防野指针的方法不适用于 ``thread_local``. 展开来说, 我们可以跳过全局或局部变量的析构函数, 因为他们的生命周期会随着程序终止而自然结束. 因此, 操作系统很快就会回收泄露的内存和其他资源. 然而, 若跳过 ``thread_local`` 的析构函数, 那么资源泄漏量和程序运行期间创建的线程数量成正比. + +**决定:** + + 位于类或命名空间中的 ``thread_local`` 变量只能用真正的编译时常量来初始化 (也就是不能动态初始化). 必须用 `ABSL_CONST_INIT `_ 修饰来保证这一点 (也可以用 ``constexpr`` 修饰, 但不常见). + + .. code-block:: c++ + + ABSL_CONST_INIT thread_local Foo foo = ...; + + 函数中的 ``thread_local`` 变量没有初始化的顾虑, 但是在线程退出时有释放后使用的风险. 注意, 你可以用静态方法暴露函数内的 ``thread_local`` 变量, 来模拟类或命名空间中的 ``thread_local`` 变量: + + .. code-block:: c++ + + Foo& MyThreadLocalFoo() { + thread_local Foo result = ComplicatedInitialization(); + return result; + } + + 注意, 线程退出时会销毁 ``thread_local`` 变量. 如果析构函数使用了任何其他 (可能已经销毁的) ``thead_local`` 变量, 我们会遇到难以调试的野指针. 建议使用平凡的类型, 或析构函数中没有自定义代码的类型, 以减少访问其他 ``thread_local`` 变量的可能性. + + 建议优先使用 ``thread_local`` 定义线程的局部数据, 而非其他机制. 译者 (YuleFox) 笔记 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~