perf: update cpp style guide

update chapter classes
This commit is contained in:
stephen.luo 2024-02-29 11:30:51 +08:00
parent 782f461f6f
commit 13767cc4f5

View File

@ -32,9 +32,9 @@
**结论**
构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 ``Init()`` 方法或工厂函数.
构造函数不允许调用虚函数. 如果代码允许, 直接终止程序是一个合适的处理错误的方式. 否则, 考虑用 ``Init()`` 方法或工厂函数, 参考 `TotW #42 <https://abseil.io/tips/42>`_.
构造函数不得调用虚函数, 或尝试报告一个非致命错误. 如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法或使用工厂模式. Avoid ``Init()`` methods on objects with no other states that affect which public methods may be called (此类形式的半构造对象有时无法正确工作).
构造函数不得调用虚函数, 或尝试报告一个非致命错误. 如果对象需要进行有意义的 (non-trivial) 初始化, 考虑使用明确的 Init() 方法或使用工厂模式. 避免在没有其他状态或标志位表征某个公有的函数是否可以被调用的类中使用 ``Init()``方法(此类形式的半构造对象有时无法正确工作).
.. _implicit-conversions:
@ -51,7 +51,7 @@
除了语言所定义的隐式类型转换, 用户还可以通过在类定义中添加合适的成员定义自己需要的转换. 在源类型中定义隐式类型转换, 可以通过目的类型名的类型转换运算符实现 (例如 ``operator bool()``). 在目的类型中定义隐式类型转换, 则通过以源类型作为其唯一参数 (或唯一无默认值的参数) 的构造函数实现.
``explicit`` 关键字可以用于构造函数或 (在 C++11 引入) 类型转换运算符, 以保证只有当目的类型在调用点被显式写明时才能进行类型转换, 例如使用 ``cast``. 这不仅作用于隐式类型转换, 还能作用于 C++11 的列表初始化语法:
``explicit`` 关键字可以用于构造函数或类型转换运算符, 以保证只有当目的类型在调用点被显式写明时才能进行类型转换, 例如使用 ``cast``. 这不仅作用于隐式类型转换, 还能作用于初始化列表语法:
.. code-block:: c++
@ -74,7 +74,7 @@
- 有时目的类型名是一目了然的, 通过避免显式地写出类型名, 隐式类型转换可以让一个类型的可用性和表达性更强.
- 隐式类型转换可以简单地取代函数重载.
- 隐式类型转换可以简单地取代函数重载, 例如使用带有一个 ``string_view`` 参数的函数来取代对 ``std::string````const char*`` 的重载函数.
- 在初始化对象时, 列表初始化语法是一种简洁明了的写法.
@ -88,15 +88,17 @@
- 如果单参数构造函数没有加上 ``explicit`` 关键字, 读者无法判断这一函数究竟是要作为隐式类型转换, 还是作者忘了加上 ``explicit`` 标记.
- 并没有明确的方法用来判断哪个类应该提供类型转换, 这会使得代码变得含糊不清.
- 隐式类型转换可能会导致调用点歧义, 尤其是存在双向隐式转换时. 这可能是因为两种类型都提供隐式转换, 或者一种类型同时存在隐式构造函数和隐式转换运算符.
- 如果目的类型是隐式指定的, 那么列表初始化会出现和隐式类型转换一样的问题, 尤其是在列表中只有一个元素的时候.
**结论**
在类型定义中, 类型转换运算符和单参数构造函数都应当用 ``explicit`` 进行标记. 一个例外是, 拷贝和移动构造函数不应当被标记为 ``explicit``, 因为它们并不执行类型转换. 对于设计目的就是用于对其他类型进行透明包装的类来说, 隐式类型转换有时是必要且合适的. 这时应当联系项目组长并说明特殊情况.
在类型定义中, 类型转换运算符和单参数构造函数都应当用 ``explicit`` 进行标记. 一个例外是, 拷贝和移动构造函数不应当被标记为 ``explicit``, 因为它们并不执行类型转换.
不能以一个参数进行调用的构造函数不应当加上 ``explicit``. 接受一个 ``std::initializer_list`` 作为参数的构造函数也应当省略 ``explicit``, 以便支持拷贝初始化 (例如 ``MyType m = {1, 2};``).
对于设计目的就是需要互换的类型来说, 隐式类型转换有时是必要且合适的. 例如, 两种类型的对象只是对同一个值的不同表示. 在这种情况下, 请联系项目负责人说明特殊情况.
单参数构造函数不应当省略 ``explicit``. 接受一个 ``std::initializer_list`` 作为参数的构造函数应当省略 ``explicit``, 以便支持拷贝初始化 (例如 ``MyType m = {1, 2};``).
.. _copyable-and-movable-types:
@ -105,56 +107,80 @@
**总述**
如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用.
一个类的公有接口必须明确这个类是可复制构造/可移动构造/不可复制和移动构造的. 若复制构造和移动构造对于某一类型是清晰且有意义的, 则支持这些操作.
**定义**
拷贝类型允许对象在初始化时得到来自相同类型的另一对象的值, 或在赋值时被赋予相同类型的另一对象的值, 同时不改变源对象的值. 对于用户定义的类型, 拷贝操作一般通过拷贝构造函数与拷贝赋值操作符定义. ``string`` 类型就是一个可拷贝类型的例子.
移动类型允许对象从临时变量进行初始化.
移动类型允许对象在初始化时得到来自相同类型的临时对象的值, 或在赋值时被赋予相同类型的临时对象的值 (因此所有可拷贝对象也是可移动的). ``std::unique_ptr<int>`` 就是一个可移动但不可复制的对象的例子. 对于用户定义的类型, 移动操作一般是通过移动构造函数和移动赋值操作符实现的.
拷贝类型允许对象从另一个相同类型的对象进行初始化 (因此从定义上来说也是可移动的), 同时不改变源对象的值. ``std::unique_ptr<int>`` 就是一个可移动但不可复制的对象的例子 (因为智能指针对应的值必须在拥有所有权的对象中修改). ``int````string`` 为既可移动也可复制的例子 (对于 ``int`` 类型, 移动和复制操作时相同的, 对于 ``std::string`` 类型, 移动操作比复制操作开销更低).
拷贝 / 移动构造函数在某些情况下会被编译器隐式调用. 例如, 通过传值的方式传递对象.
对于用户自定义的类型, 复制行为由复制构造函数和复制构造赋值运算符来定义. 移动行为由移动构造函数和移动构造赋值运算符来定义, 否则由复制构造函数和复制构造赋值运算符来定义.
在某些情况下, 编译器可能会隐式地调用复制/移动构造函数. 例如, 通过传递值的方法传递对象时.
**优点**
可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得 API 更简单, 更安全也更通用. 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确. 这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护. 这样的对象可以和需要传值操作的通用 API 一起使用, 例如大多数容器.
可移动及可拷贝类型的对象可以通过传值的方式进行传递或者返回, 这使得 API 更简单, 更安全也更通用. 与传指针和引用不同, 这样的传递不会造成所有权, 生命周期, 可变性等方面的混乱, 也就没必要在协议中予以明确. 这同时也防止了客户端与实现在非作用域内的交互, 使得它们更容易被理解与维护. 这样的对象可以和需要传值操作的通用 API 一起使用, 例如大多数容器, 并允许它们在类型组合等方面提供额外的灵活性.
拷贝 / 移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如 ``Clone()``, ``CopyFrom()`` or ``Swap()``, 更容易定义, 因为它们能通过编译器产生, 无论是隐式的还是通过 ``= default``. 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时, 对于类似 `省略不必要的拷贝 <http://en.cppreference.com/w/cpp/language/copy_elision>`_ 这样的优化它们也更加合适.
拷贝 / 移动构造函数与赋值操作一般来说要比它们的各种替代方案, 比如 ``Clone()``, ``CopyFrom()`` or ``Swap()``, 更容易定义, 因为它们能通过编译器产生, 无论是隐式的还是通过 ``= default``. 这种方式很简洁, 也保证所有数据成员都会被复制. 拷贝与移动构造函数一般也更高效, 因为它们不需要堆的分配或者是单独的初始化和赋值步骤, 同时, 对于类似 `省略不必要的拷贝 <https://en.cppreference.com/w/cpp/language/copy_elision>`_ 这样的优化它们也更加合适.
移动操作允许隐式且高效地将源数据转移出右值对象. 这有时能让代码风格更加清晰.
**缺点**
许多类型都不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 单件类型 (``Registerer``), 与特定的作用域相关的类型 (``Cleanup``), 与其他对象实体紧耦合的类型 (``Mutex``) 从逻辑上来说都不应该提供拷贝操作. 为基类提供拷贝 / 赋值操作是有害的, 因为在使用它们时会造成 `对象切割 <https://en.wikipedia.org/wiki/Object_slicing>`_ . 默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误.
某些类型并不需要拷贝, 为它们提供拷贝操作会让人迷惑, 也显得荒谬而不合理. 单例对象 (``Registerer``), 或与其他对象实体紧耦合的类型 (``Mutex``) 从逻辑上来说都不应该提供拷贝操作. 为基类提供拷贝 / 赋值操作是危险的, 因为在使用它们时会造成 `对象切割 <https://en.wikipedia.org/wiki/Object_slicing>`_ . 默认的或者随意的拷贝操作实现可能是不正确的, 这往往导致令人困惑并且难以诊断出的错误.
拷贝构造函数是隐式调用的, 也就是说, 这些调用很容易被忽略. 这会让人迷惑, 尤其是对那些所用的语言约定或强制要求传引用的程序员来说更是如此. 同时, 这从一定程度上说会鼓励过度拷贝, 从而导致性能上的问题.
**结论**
如果需要就让你的类型可拷贝 / 可移动. 作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝. 如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然. 如果让类型可移动, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义. 如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作.
每个类的公有接口都应该明确这个类是否支持复制构造/移动构造. 通常应在 ``public`` 字段中显示的声明/删除对应的函数或操作符.
如果定义了拷贝/移动操作, 则要保证这些操作的默认实现是正确的. 记得时刻检查默认操作的正确性, 并且在文档中说明类是可拷贝的且/或可移动的.
通常来说, 一个可复制的类应该显示声明复制操作, 一个只能移动的类应显示声明移动操作, 一个既不能复制也不能移动的类应该显示删除复制操作. 可复制类也应声明移动操作以支持高效的移动. 显示的声明或删除所有4个复制/移动操作是允许的, 但不是必须的. 如果您提供复制或移动赋值运算符, 则必须提供对应的构造函数.
.. code-block:: c++
class Foo {
class Copyable {
public:
Foo(Foo&& other) : field_(other.field) {}
// 差, 只定义了移动构造函数, 而没有定义对应的赋值运算符.
private:
Field field_;
Copyable(const Copyable& other) = default;
Copyable& operator=(const Copyable& other) = default;
// 隐式移动操作会被上面的声明抑制.
// 您可以声明移动操作来支持更高效的移动
};
由于存在对象切割的风险, 不要为任何有可能有派生类的对象提供赋值操作或者拷贝 / 移动构造函数 (当然也不要继承有这样的成员函数的类). 如果你的基类需要可复制属性, 请提供一个 ``public virtual Clone()`` 和一个 ``protected`` 的拷贝构造函数以供派生类实现.
class MoveOnly {
public:
MoveOnly(MoveOnly&& other) = default;
MoveOnly& operator=(MoveOnly&& other) = default;
如果你的类不需要拷贝 / 移动操作, 请显式地通过在 ``public`` 域中使用 ``= delete`` 或其他手段禁用之.
// 复制操作被隐式地删除, 但您可以显式地声明:
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
};
.. code-block:: c++
class NotCopyableOrMovable {
public:
// 既不可复制也不可移动
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&) = delete;
// MyClass is neither copyable nor movable.
MyClass(const MyClass&) = delete;
MyClass& operator=(const MyClass&) = delete;
// 移动操作被隐式地删除, 但您可以显式地声明:
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&) = delete;
};
当且仅当上述声明/删除为显而易见时, 您可以省略它们:
- 如果一个类没有 ``private`` 部分, 例如结构体或仅包含接口的基类, 则可复制性/移动性可以由任意公有属性的数据成员的可复制性/可移动性来确定.
- 如果一个基类是不可移动/复制的, 那么其派生类也不行. 一个隐式保留移动/复制操作的仅包含接口的基类无法说明具体子类的可复制性/可移动性.
- 注意到如果您显式声明/删除复制构造函数或复制操作赋值运算符的其中一个, 另一个也必须显式地声明/删除. 移动操作亦是如此.
如果用户不清楚一个类的复制/移动的含义, 或者不清楚复制/移动操作会产生意料之外的开销, 那么这个类应该是不可复制/移动的. 可复制类型的移动操作严格来说只是一种性能优化, 并且是错误和复杂性的潜在来源, 因此请避免定义它们, 除非它们的实现比复制操作更高效. 如果您的类是可复制的, 建议您在设计中保证默认的构造和赋值运算符是正确的. 请记得像检查您自己的代码一样地检查默认操作的正确性.
为了消除对象切片的风险, 最好将基类抽象化, 通常可以将构造和析构函数声明成protected属性, 或者提供一个或多个纯虚的成员函数. 尽量避免继承一个具体的类.
.. _structs-vs-classes:
@ -167,19 +193,33 @@
**说明**
在 C++ 中 ``struct````class`` 关键字几乎含义一样. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字.
在 C++ 中 ``struct````class`` 关键字的含义几乎是一样的. 我们为这两个关键字添加我们自己的语义理解, 以便为定义的数据类型选择合适的关键字.
``struct`` 用来定义包含数据的被动式对象, 也可以包含相关的常量, 但除了存取数据成员之外, 没有别的函数功能. 并且存取功能是通过直接访问位域, 而非函数调用. 除了构造函数, 析构函数, ``Initialize()``, ``Reset()``, ``Validate()`` 等类似的用于设定数据成员的函数外, 不能提供其它功能的函数.
``struct`` 用来定义包含数据的被动式对象, 也可以包含相关的常量, 并且它们的属性必须是公共的. 由于用户能直接访问 ``struct`` 中的所有字段, 因此 ``struct`` 中不能包含任何表示不同字段关系的不变量. ``struct`` 中可能存在构造函数、析构函数和辅助的函数方法, 这些方法不能要求任何不变量的值.
如果需要更多的函数功能, ``class`` 更适. 如果拿不准, 就用 ``class``.
如果需要更多的函数功能或不变量, 亦或者对象具有抽象性且可能会被继承, 那么 ``class`` 适. 如果拿不准, 就用 ``class``.
为了和 STL 保持一致, 对于仿函数等特性可以不用 ``class`` 而是使用 ``struct``.
为了和 STL 保持一致, 对于traits技术、 :ref:`模板元编程 <template-metaprogramming>`仿函数等特性可以不用 ``class`` 而是使用 ``struct``.
注意: 类和结构体的成员变量使用不同的 :ref:`命名规则 <variable-names>`.
注意到类和结构体的成员变量使用不同的 :ref:`命名规则 <variable-names>`.
.. _structs-vs-pairs-and-tuples:
3.5. 结构体 VS. pair 和 tuple
**总述**
当变量的命名有意义时, 倾向于使用 ``struct`` 而不是 pair 或 tuple.
**说明**
使用 pair 或 tuple 可以省略为变量命名, 潜在地减少了写代码的开销, 但一个清晰的变量命名会让你的代码更加清晰, 相比于 ``.first``, ``.second``, ``std::get<X>`` 来说. 虽然 C++14 通过引入 ``std::get<Type>`` 来获取 tuple 中的变量 (而不是下标索引) 会让代码更清晰, 但一个带名字的变量总是会比上述方法更加显而易见.
pair 或 tuple 可能适用于通用代码, 其中的变量没有特定含义. 为了与现有的代码和 API 互操作时, 可能也需要使用它们.
.. _inheritance:
3.5. 继承
3.6. 继承
~~~~~~~~~~~~~~~~~~~~
**总述**
@ -188,98 +228,38 @@
**定义**
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. C++ 实践中, 继承主要用于两种场合: 实现继承, 子类继承父类的实现代码; :ref:`接口继承 <interface>`, 子类仅继承父类的方法名称.
当子类继承基类时, 子类包含了父基类所有数据及操作的定义. “接口继承”指从纯抽象基类 (不包含状态或定义的方法) 继承; 所有其他继承都是“实现继承”.
**优点**
实现继承通过原封不动的复用基类代码减少了代码量. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 从编程角度而言, 接口继承是用来强制类输出特定的 API. 在类没有实现 API 中某个必须的方法时, 编译器同样会发现并报告错误.
实现继承通过复用基类代码来减少代码大小. 由于继承是在编译时声明, 程序员和编译器都可以理解相应操作并发现错误. 接口继承是用来强制规定必须实现的 API. 在继承类中没有实现某个必须的 API 时, 编译器可以检测到错误.
**缺点**
对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 当然也就不能修改其实现. 基类也可能定义了一些数据成员, 因此还必须区分基类的实际布局.
对于实现继承, 由于子类的实现代码散布在父类和子类间之间, 要理解其实现变得更加困难. 子类不能重写父类的非虚函数, 因此无法修改其实现.
多重继承可能引入很多问题, 因为它通常会带来更高的性能开销 (事实上, 从单继承到多重继承导致的性能下降会高于从普通方法到虚方法的性能下降), 并且它有导致“菱形继承”的风险, 容易造成歧义、混乱和彻底的错误.
**结论**
所有继承必须是 ``public`` 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.
所有继承必须是 ``public`` 的. 如果您想使用私有继承, 您应该把该对象的实例保存在成员变量中. 当你不希望你的类被继承时, 应该使用 ``final`` 关键字.
不要过度使用实现继承. 组合常常更合适一些. 尽量做到只在 "是一个" ("is-a", YuleFox 注: 其他 "has-a" 情况下请使用组合) 的情况下使用继承: 如果 ``Bar`` 的确 "是一种" ``Foo``, ``Bar`` 才能继承 ``Foo``.
必要的话, 析构函数声明为 ``virtual``. 如果你的类有虚函数, 则析构函数也应该为虚函数.
``protected`` 的变量和函数限制为那些可能需要从子类访问的成员函数. 注意到 :ref:`数据成员应该是私有的 <access-control>`.
对于可能被子类访问的成员函数, 不要过度使用 ``protected`` 关键字. 注意, 数据成员都必须是 :ref:`私有的 <access-control>`.
使用 ``override````final`` (较少使用) 关键字来标识重载的虚函数或者虚的析构函数. 重载时不要使用 ``virtual`` . 基本原理: 如果一个函数带有 ``override````final`` 关键字, 但却不是基类虚函数的重载, 会出现编译报错, 有助于定位问题. 这些声明符也是一种标识, 如果不存在这些说明符, 代码阅读者需要查看基类来确定这些函数或者析构函数是否为虚的.
对于重载的虚函数或虚析构函数, 使用 ``override``, 或 (较不常用的) ``final`` 关键字显式地进行标记. 较早 (早于 C++11) 的代码可能会使用 ``virtual`` 关键字作为不得已的选项. 因此, 在声明重载时, 请使用 ``override``, ``final````virtual`` 的其中之一进行标记. 标记为 ``override````final`` 的析构函数如果不是对基类虚函数的重载的话, 编译会报错, 这有助于捕获常见的错误. 这些标记起到了文档的作用, 因为如果省略这些关键字, 代码阅读者不得不检查所有父类, 以判断该函数是否是虚函数.
多重继承是允许的, 但多重实现继承是强烈不推荐的.
.. _multiple-inheritance:
.. _operator_overloading:
3.6. 多重继承
3.7. 运算符重载
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**总述**
真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 ``Interface`` 为后缀的 :ref:`纯接口类 <interface>`.
**定义**
多重继承允许子类拥有多个基类. 要将作为 *纯接口* 的基类和具有 *实现* 的基类区别开来.
**优点**
相比单继承 (见 :ref:`继承 <inheritance>`), 多重实现继承可以复用更多的代码.
**缺点**
真正需要用到多重 *实现* 继承的情况少之又少. 有时多重实现继承看上去是不错的解决方案, 但这时你通常也可以找到一个更明确, 更清晰的不同解决方案.
**结论**
只有当所有父类除第一个外都是 :ref:`纯接口类 <interface>` 时, 才允许使用多重继承. 为确保它们是纯接口, 这些类必须以 ``Interface`` 为后缀.
**注意**
关于该规则, Windows 下有个 :ref:`特例 <windows-code>`.
.. _interface:
3.7. 接口
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**总述**
接口是指满足特定条件的类, 这些类以 ``Interface`` 为后缀 (不强制).
**定义**
当一个类满足以下要求时, 称之为纯接口:
- 只有纯虚函数 ("``=0``") 和静态函数 (除了下文提到的析构函数).
- 没有非静态数据成员.
- 没有定义任何构造函数. 如果有, 也不能带有参数, 并且必须为 ``protected``.
- 如果它是一个子类, 也只能从满足上述条件并以 ``Interface`` 为后缀的类继承.
接口类不能被直接实例化, 因为它声明了纯虚函数. 为确保接口类的所有实现可被正确销毁, 必须为之声明虚析构函数 (作为上述第 1 条规则的特例, 析构函数不能是纯虚函数). 具体细节可参考 Stroustrup 的 *The C++ Programming Language, 3rd edition* 第 12.4 节.
**优点**
``Interface`` 为后缀可以提醒其他人不要为该接口类增加函数实现或非静态数据成员. 这一点对于 :ref:`多重继承 <multiple-inheritance>` 尤其重要. 另外, 对于 Java 程序员来说, 接口的概念已是深入人心.
**缺点**
``Interface`` 后缀增加了类名长度, 为阅读和理解带来不便. 同时, 接口属性作为实现细节不应暴露给用户.
**结论**
只有在满足上述条件时, 类才以 ``Interface`` 结尾, 但反过来, 满足上述需要的类未必一定以 ``Interface`` 结尾.
3.8. 运算符重载
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**总述**
除少数特定环境外, 不要重载运算符. 也不要创建用户定义字面量.
明智地使用运算符重载. 不要使用用户自定义的字面值.
**定义**
@ -287,9 +267,9 @@ C++ 允许用户通过使用 ``operator`` 关键字 `对内建运算符进行重
**优点**
重载运算符可以让代码更简洁易懂, 也使得用户定义的类型和内建类型拥有相似的行为. 重载运算符对于某些运算来说是符合语言习惯的名称 (例如 ``==``, ``<``, ``=``, ``<<``), 遵循这些语言约定可以让用户定义的类型更易读, 也能更好地和需要这些重载运算符的函数库进行交互操作.
重载运算符可以让代码更简洁直观, 也使得用户定义的类型和内置类型拥有相似的行为. 重载运算符对于某些运算来说是符合语言习惯的名称 (例如 ``==``, ``<``, ``=``, ``<<``), 遵循这些语言约定可以让用户定义的类型更易读, 也能更好地和需要这些重载运算符的函数库进行交互操作.
对于创建用户定义的类型的对象来说, 用户定义字面是一种非常简洁的标记.
对于创建用户定义的类型的对象来说, 用户定义字面是一种非常简洁的标记.
**缺点**
@ -309,34 +289,40 @@ C++ 允许用户通过使用 ``operator`` 关键字 `对内建运算符进行重
- 运算符从通常定义在类的外部, 所以对于同一运算, 可能出现不同的文件引入了不同的定义的风险. 如果两种定义都链接到同一二进制文件, 就会导致未定义的行为, 有可能表现为难以发现的运行时错误.
- 用户定义字面量所创建的语义形式对于某些有经验的 C++ 程序员来说都是很陌生的.
- 用户定义字面量 (UDLs) 所创建的语义形式对于某些有经验的 C++ 程序员来说都是很陌生的, 例如将 ``"Hello World"sv`` 作为 ``std::string_view("Hello World")`` 的简写. 现有的符号更清晰, 但不够简洁.
- 用户定义字面量 (UDLs) 不能被命名空间限定, 所以对于它们的使用需要我们 :ref:`约定禁止 <namespaces>` 使用或者在头文件中禁止使用 (除非字面量必须出现在某个有问题的头文件的接口里). 鉴于头文件必须避免字面量后缀, 我们更希望能避免头文件的字面量和源文件的字面量不同的情况.
**结论**
只有在意义明显, 不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符. 例如, ``|`` 要作为位或逻辑或来使用, 而不是作为 shell 中的管道.
只有在意义明显, 不会出现奇怪的行为并且与对应的内建运算符的行为一致时才定义重载运算符. 例如, ``|`` 要作为位或/逻辑或来使用, 而不是作为 shell 中的管道.
只有对用户自己定义的类型重载运算符. 更准确地说, 将它们和它们所操作的类型定义在同一个头文件中, ``.cc`` 中和命名空间中. 这样做无论类型在哪里都能够使用定义的运算符, 并且最大程度上避免了多重定义的风险. 如果可能的话, 请避免将运算符定义为模板, 因为此时它们必须对任何模板参数都能够作用. 如果你定义了一个运算符, 请将其相关且有意义的运算符都进行定义, 并且保证这些定义的语义是一致的. 例如, 如果你重载了 ``<``, 那么请将所有的比较运算符都进行重载, 并且保证对于同一组参数, ``<````>`` 不会同时返回 ``true``.
只有对用户自己定义的类型重载运算符. 更准确地说, 将它们和它们所操作的类型定义在同一个头文件中, ``.cc`` 中和命名空间中. 这样做无论类型在哪里都能够使用定义的运算符, 并且最大程度上避免了多重定义的风险. 如果可能的话, 请避免将运算符定义为模板, 因为此时它们必须对任何可能得参数都起作用. 如果你定义了一个运算符, 请将其相关且有意义的运算符都进行定义, 并且保证这些定义的语义是一致的.
建议不要将不进行修改的二元运算符定义为成员函数. 如果一个二元运算符被定义为类成员, 这时隐式转换会作用于右侧的参数却不会作用于左侧. 这时会出现 ``a < b`` 能够通过编译而 ``b < a`` 不能的情况, 这是很让人迷惑的.
建议不要将不进行修改的二元运算符定义为成员函数. 如果一个二元运算符被定义为类成员, 这时隐式转换会作用于右侧的参数却不会作用于左侧. 这时会出现 ``a + b`` 能够通过编译而 ``b + a`` 不能的情况, 这是很让人迷惑的.
对于可以比较其值是否相等的类型 ``T``, 请定义非成员运算符 ``operator==`` 并且提供文档说明何时将类型为 ``T`` 的两个值视为相等. 如果一个类型为 ``T`` 的值 ``t1`` 小于 ``t2`` 是显而易见的, 那么你还应该定义 ``operator<==>``, 它的定义应该与 ``operator==`` 保持一致. 最好不要重载其他比较和排序运算符.
不要为了避免重载操作符而走极端. 比如说, 应当定义 ``==``, ``=``, 和 ``<<`` 而不是 ``Equals()``, ``CopyFrom()````PrintTo()``. 反过来说, 不要只是为了满足函数库需要而去定义运算符重载. 比如说, 如果你的类型没有自然顺序, 而你要将它们存入 ``std::set`` 中, 最好还是定义一个自定义的比较运算符而不是重载 ``<``.
不要重载 ``&&``, ``||``, ``,`` 或一元运算符 ``&``. 不要重载 ``operator""``, 也就是说, 不要引入用户定义字面量.
不要重载 ``&&``, ``||``, ``,`` 或一元运算符 ``&``. 不要重载 ``operator""``, 也就是说, 不要引入用户定义字面量. 不要使用任何其他人提供的字面量 (包括标准库).
类型转换运算符在 :ref:`隐式类型转换 <implicit-conversions>` 一节有提及. ``=`` 运算符在 :ref:`可拷贝类型和可移动类型 <copyable-and-movable-types>` 一节有提及. 运算符 ``<<``:ref:`流 <streams>` 一节有提及. 同时请参见 :ref:`函数重载 <function-overloading>` 一节, 其中提到的的规则对运算符重载同样适用.
.. _access-control:
3.9. 存取控制
3.8. 存取控制
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**总述**
*所有* 数据成员声明为 ``private``, 除非是 ``static const`` 类型成员 (遵循 :ref:`常量命名规则 <constant-names>`). 出于技术上的原因, 在使用 `Google Test <https://github.com/google/googletest>`_ 时我们允许测试固件类中的数据成员为 ``protected``.
*所有* 数据成员声明为 ``private``, 除非是常量 (遵循 :ref:`常量命名规则 <constant-names>`). 这会简化关于常量的获取, 但需要形成一些带有 ``const`` 值获取的访问器.
出于技术上的原因, 在使用 `Google Test <https://github.com/google/googletest>`_ 时我们允许测试夹具类中的数据成员在 ``.cc`` 文件中定义为 ``protected``. 如果测试夹具类是在使用它的 ``.cc`` 文件之外定义的, 例如在 ``.h`` 文件中, 将数据成员变量声明为私有的.
.. _declaration-order:
3.10. 声明顺序
3.9. 声明顺序
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
**总述**
@ -347,9 +333,25 @@ C++ 允许用户通过使用 ``operator`` 关键字 `对内建运算符进行重
类定义一般应以 ``public:`` 开始, 后跟 ``protected:``, 最后是 ``private:``. 省略空部分.
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序: 类型 (包括 ``typedef``, ``using`` 和嵌套的结构体与类), 常量, 工厂函数, 构造函数, 赋值运算符, 析构函数, 其它函数, 数据成员.
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序:
不要将大段的函数定义内联在类定义中. 通常,只有那些普通的, 或性能关键且短小的函数可以内联在类定义中. 参见 :ref:`内联函数 <inline-functions>` 一节.
- 1. 类型及其别名 (包括 ``typedef``, ``using``, ``enum``, 嵌套的结构体与类以及友元类型)
- 2. (可选, 仅用于 ``struct``) 非静态数据成员
- 3. 静态常量
- 4. 工厂函数
- 5. 构造函数和赋值运算符
- 6. 析构函数
- 7. 所有的剩余函数 (静态与非静态成员函数, 以及友元函数)
- 8. 所有的剩余数据成员 (静态的和非静态的)
不要将大段的函数定义内联在类定义中. 通常, 只有那些普通的, 或性能关键且短小的函数可以内联在类定义中. 参见 :ref:`内联函数 <inline-functions>` 一节.
译者 (YuleFox) 笔记
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~