C++ 编码风格

本文档试图解释 Mozilla 代码库中使用的基本风格和模式。新代码应尽量遵循这些标准,使其与现有代码一样易于维护。当然也存在例外情况,但了解规则仍然很重要!

本文档尤其针对刚接触 Mozilla 代码库并正在进行代码审查的新人。在请求审查之前,请仔细阅读本文档,确保您的代码符合建议。

Firefox 代码库采用了部分 Google C++ 代码编码风格,但并非所有规则都适用。一些规则在整个代码库中都遵循,另一些则旨在应用于新的或经过大幅修改的代码。当我们进一步评估 Google C++ 代码编码风格和/或更新我们的编码实践时,我们将来可能会扩展此列表。但是,我们的计划并非要采用 Google C++ 代码编码风格的所有规则。有些规则在任何时候都不太可能被采用。

在整个代码库中遵循的规则

  • 格式,除了此处另有说明的小节外

  • 隐式转换,由自定义 clang 插件检查强制执行,除非使用 MOZ_IMPLICIT 显式覆盖

在新/大幅修改的代码中遵循的规则

不太可能被采用的规则

  • 前向声明

  • 格式/条件语句 关于内嵌语句的花括号,我们要求在 Google 样式允许省略单行条件语句花括号的所有情况下都使用花括号

此列表反映了截至 2020-07-17 Google C++ 代码编码风格的状态。当 Google 修改其编码风格时,它可能会失效。

代码格式

格式化是通过 clang-format 自动完成的,并通过树内配置文件控制。有关更多信息,请参阅 使用 clang-format 格式化 C++ 代码

Unix 风格的换行符 (\n),而不是 Windows 风格 (\r\n)。您可以使用 dos2unix 实用程序或您喜欢的文本编辑器将带有 DOS 换行符的补丁转换为 Unix 风格。

静态分析

Google C++ 编码风格中的一些规则以及下面提到的补充规则可以通过 clang-tidy 进行检查(有些规则来自上游 clang-tidy,有些规则通过 Mozilla 特定的插件提供)。其中一些检查还允许自动应用修复。

mach static-analysis 提供了一种方便的方法来运行这些检查。例如,对于名为 google-readability-braces-around-statements 的检查,您可以运行

./mach static-analysis check --checks="-*,google-readability-braces-around-statements" --fix <file>

在自动应用修复后,可能需要重新格式化文件,请参阅 使用 clang-format 格式化 C++ 代码

其他规则

本节中的规范应在新代码中遵循。对于现有代码,请使用文件或模块中的主要风格,如果您在其他团队的代码库中或不清楚使用哪种风格,请咨询所有者。

控制结构

始终使用花括号括起受控语句,即使是 if else else 的单行结果。这通常是多余的,但它避免了悬空 else 错误,因此在大规模情况下比微调更安全。

示例

if (...) {
} else if (...) {
} else {
}

while (...) {
}

do {
} while (...);

for (...; ...; ...) {
}

switch (...) {
  case 1: {
    // When you need to declare a variable in a switch, put the block in braces.
    int var;
    break;
  }
  case 2:
    ...
    break;
  default:
    break;
}

else 之后只能跟随 {if;即,不允许使用其他控制关键字,并且应将其放在花括号内。

注意

对于此规则,clang-tidy 提供了 google-readability-braces-around-statements 检查,并带有自动修复功能。

C++ 命名空间

Mozilla 项目 C++ 声明应位于 mozilla 命名空间中。模块应避免在 mozilla 下添加嵌套命名空间。此规则有几个例外情况:

  • 名称与代码库中的其他名称发生冲突的可能性很高。例如,PointPath 等。此类符号可以放在模块特定的命名空间下,在 mozilla 下,使用简短的全小写名称。

  • 实现 WebIDL 绑定的类倾向于位于 mozilla::dom 中,尽管这不是严格要求的,并且可以通过 Bindings.conf 进行自定义。有关更多信息,请参阅 Web IDL 绑定

不允许使用除了 mozilla 之外的其他全局命名空间。

在头文件中不允许使用 using 指令,除非在类定义或函数内部。(我们不希望污染使用头文件的编译单元的全局范围。)

注意

对于此规则的一部分,clang-tidy 提供了 google-global-names-in-headers 检查。它仅检测全局命名空间中的 using namespace 指令。

using namespace ...; 仅允许在 .cpp 文件中,在所有 #include 之后。如果可能,最好用 namespace ... { ... }; 包裹代码。using namespace ...; 应始终指定完全限定的命名空间。也就是说,要使用 Foo::Bar,不要写 using namespace Foo; using namespace Bar;,而要写 using namespace Foo::Bar;

使用嵌套命名空间(例如:namespace mozilla::widget {

注意

clang-tidy 提供了 modernize-concat-nested-namespaces 检查,并带有自动修复功能。

匿名命名空间

我们更倾向于使用 static 而不是匿名 C++ 命名空间。一旦调试器更好地支持(尤其是在 Windows 上)在匿名命名空间中的代码上设置断点等,这可能会改变。您仍然可以使用匿名命名空间来处理无法使用 static 隐藏的内容,例如类型或需要传递给模板函数的某些对象。

C++ 类

namespace mozilla {

class MyClass : public A
{
  ...
};

class MyClass
  : public X
  , public Y
{
public:
  MyClass(int aVar, int aVar2)
    : mVar(aVar)
    , mVar2(aVar2)
  {
     ...
  }

  // Special member functions, like constructors, that have default bodies
  // should use '= default' annotation instead.
  MyClass() = default;

  // Unless it's a copy or move constructor or you have a specific reason to allow
  // implicit conversions, mark all single-argument constructors explicit.
  explicit MyClass(OtherClass aArg)
  {
    ...
  }

  // This constructor can also take a single argument, so it also needs to be marked
  // explicit.
  explicit MyClass(OtherClass aArg, AnotherClass aArg2 = AnotherClass())
  {
    ...
  }

  int LargerFunction()
  {
    ...
    ...
  }

private:
  int mVar;
};

} // namespace mozilla

使用上面给出的样式定义类。

注意

对于 = default 的规则,clang-tidy 提供了 modernize-use-default 检查,并带有自动修复功能。

对于显式构造函数和转换运算符的规则,clang-tidy 提供了 mozilla-implicit-constructor 检查。

全局命名空间中的现有类使用简短的前缀(例如,ns)作为伪命名空间。

方法和函数

C/C++

在 C/C++ 中,方法名称应使用 UpperCamelCase

从不失败且从不返回 null 的 Getter 被命名为 Foo(),而所有其他 Getter 使用 GetFoo()。Getter 可以通过 Foo** aResult 输出参数(通常用于 XPCOM Getter)返回对象值,或者作为 already_AddRefed<Foo>(通常用于 WebIDL Getter,可能带 ErrorResult& rv 参数),或者偶尔作为 Foo*(通常用于具有已知生命周期的对象的内部 Getter)。有关更多信息,请参阅 错误 223255

XPCOM Getter 始终通过输出参数返回基本值,而其他 Getter 通常使用返回值。

方法声明最多只能使用以下关键字之一:virtualoverridefinal。使用 virtual 声明虚方法,这些方法不重写具有相同签名的基类方法。使用 override 声明重写基类方法的虚方法,这些方法具有相同的签名,但可以在派生类中进一步重写。使用 final 声明重写基类方法的虚方法,这些方法具有相同的签名,但不能在派生类中进一步重写。这有助于阅读代码的人员完全理解声明的作用,而无需进一步检查基类。

注意

对于 virtual/override/final 规则,clang-tidy 提供了 modernize-use-override 检查以及自动修复功能。

操作符

一元关键字操作符 sizeof 的操作数应使用括号括起来,即使它是一个表达式;例如 int8_t arr[64]; memset(arr, 42, sizeof(arr));

字面量

对于非 ASCII 字符,使用 \uXXXX Unicode 转义序列。XUL、脚本和属性文件的字符集是 UTF-8,不易阅读。

前缀

遵循以下命名前缀约定

变量前缀

  • k=常量(例如 kNC_child)。并非所有代码都使用此样式;一些代码使用 ALL_CAPS 表示常量。

  • g=全局变量(例如 gPrefService

  • a=参数(例如 aCount

  • C++ 特定前缀

    • s=静态成员(例如 sPrefChecked

    • m=成员(例如 mLength

    • e=枚举变量(例如 enum Foo { eBar, eBaz })。枚举类应使用 CamelCase 而不是(例如 enum class Foo { Bar, Baz })。

全局函数/宏等

  • 宏以 MOZ_ 开头,并且全部大写(例如 MOZ_WOW_GOODNESS)。请注意,旧代码使用 NS_ 前缀;虽然这些不会更改,但对于新的宏,您应该只使用 MOZ_。唯一的例外是,如果您正在创建新的宏,并且它是仍然使用旧 NS_ 前缀的一组相关宏的一部分。然后,您应该与现有宏保持一致。

错误变量

  • 分配 nsresult 结果代码的局部变量应命名为 rv(即,例如,不是 res,不是 result,不是 foo)。rv 不应用于布尔值或其他结果类型。

  • 分配 bool 结果代码的局部变量应命名为 ok

C/C++ 实践

  • **您是否检查了编译器警告?** 警告通常会指出真正的错误。许多警告 在构建系统中默认启用。

  • 在 C++ 代码中,对指针使用 nullptr。在 C 代码中,允许使用 NULL0

注意

对于 C++ 规则,clang-tidy 提供了 modernize-use-nullptr 检查以及自动修复功能。

  • 不要在 C++ 中使用 PRBoolPRPackedBool,而是使用 bool

  • 要检查 std 容器是否没有项目,不要使用 size(),而是使用 empty()

  • 在测试指针时,使用 (!myPtr)(myPtr);不要使用 myPtr != nullptrmyPtr == nullptr

  • 不要比较 x == truex == false。而是使用 (x)(!x)if (x == true) 的语义可能与 if (x) 不同!

注意

clang-tidy 提供了 readability-simplify-boolean-expr 检查以及自动修复功能,该功能会检查这些以及其他一些可以简化的布尔表达式。

  • 通常,使用 nsFoo aFoo = bFoo, 初始化变量,而不是 nsFoo aFoo(bFoo)

    • 对于构造函数,使用:nsFoo aFoo(bFoo) 语法初始化成员变量。

  • 为了避免仅在调试版本中使用的变量创建警告,在声明它们时使用 DebugOnly<T> 帮助程序。

  • 您应该 使用静态首选项 API 来处理首选项。

  • 不属于复制或移动构造函数的单参数构造函数通常应标记为 explicit。异常应使用 MOZ_IMPLICIT 进行注释。

  • 应避免使用具有运行时初始化的全局变量。将它们标记为 constexprMOZ_CONSTINIT 是确保初始化在编译时发生的良好方法。如果无法避免运行时初始化,请使用属性 MOZ_RUNINIT 来识别这些变量并告诉 linter 忽略该变量。如果一个变量被标记为 MOZ_RUNINIT,而 linter 检测到它可以是 MOZ_CONSTINIT,则会产生错误。在全局变量的状态变化(例如,取决于模板参数)的情况下,只需将其标记为 MOZ_GLOBINIT

  • 使用 char32_t 作为返回类型或方法的参数类型,该方法返回或作为参数获取单个 Unicode 标量值。(不过,不要使用 UTF-32 字符串。)

  • 对于语义上非负的整数值,首选无符号类型。

  • 当对可能溢出的整数进行运算时,使用 CheckedInt

  • 避免使用 typedef,而是使用 using

注意

对于此规则的一部分,clang-tidy 提供了 modernize-use-using 检查以及自动修复功能。

头文件

由于 Firefox 代码库非常庞大并且使用单片构建,因此限制每个翻译单元中包含的文件数量到所需的最小值对于保持构建时间的合理性至关重要。导出头文件在这方面需要特别注意,因为它们包含的文件会传播,并且其中许多文件直接或间接包含在大量翻译单元中。

  • 包含保护根据 Google 编码风格命名(即大写蛇形命名法,后面带一个下划线)。它们不应包含前导 MOZ_MOZILLA_。例如,dom/media/foo.h 将使用保护 DOM_MEDIA_FOO_H_

  • 尽可能在头文件中前置声明类,而不是包含它们。例如,如果您有一个接口,其中包含 void DoSomething(nsIContent* aContent) 函数,则使用 class nsIContent; 进行前置声明,而不是 #include "nsIContent.h"。如果为某个类型提供了“转发头文件”,则包含该头文件,而不是在头文件中放置文字前置声明。例如,对于某些 JavaScript 类型,有 js/TypeDecls.h,对于字符串类型,有 StringFwd.h。这样做的原因之一是,这允许通过仅更改转发头文件来将类型更改为类型别名。以下类型的用法仅可以使用前置声明

    • 函数声明中的参数或返回类型

    • 成员/局部变量指针或引用类型

    • 用作成员/局部变量类型中的模板参数(并非在所有情况下)

    • 定义类型别名

    以下类型的用法需要完整定义

    • 基类

    • 成员/局部变量类型

    • 与 delete 或 new 一起使用

    • 用作模板参数(并非在所有情况下)

    • 任何非作用域枚举类型的用法

    • 作用域枚举类型的枚举值

    用作模板参数有些棘手。这取决于模板如何使用该类型。例如,mozilla::Maybe<T>AutoTArray<T> 始终需要 T 的完整定义,因为模板实例的大小取决于 T 的大小。RefPtr<T>UniquePtr<T> 不需要完整定义(因为它们的指针成员始终具有相同的大小),但它们的析构函数需要完整定义。如果您遇到一个无法仅用前置声明实例化的模板,但它似乎应该可以实例化,请提交错误(如果该错误尚不存在)。

    因此,还要考虑以下准则,以便尽可能广泛地使用前置声明。

  • 头文件中的内联函数体通常会引入许多额外的依赖项。在添加或扩展内联函数体时要谨慎,并考虑将函数体移动到 cpp 文件或不包含在任何地方的单独头文件中。错误 1677553 旨在为此提供更具体的指南。

  • 考虑使用 Pimpl 惯用法,即在单独的 Impl 类中隐藏实际实现,该类在实现文件中定义,并且在头文件中仅公开 class Impl; 前置声明和 UniquePtr<Impl> 成员。

  • 不要使用非作用域枚举类型。这些无法进行前置声明。而是使用作用域枚举类型,并在可能的情况下对其进行前置声明。

  • 避免需要从类外部引用的嵌套类型。这些无法进行前置声明。而是将它们放在命名空间中,也许是在额外的内部命名空间中,并在可能的情况下对其进行前置声明。

  • 避免在一个头文件中混合具有不同依赖项集的声明。这通常是明智的,但当这些声明中的一些仅由包含组合头文件的一部分翻译单元使用时,更是如此。考虑这样一个混合不佳的头文件,例如

    /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
    /* vim: set ts=8 sts=2 et sw=2 tw=80: */
    /* This Source Code Form is subject to the terms of the Mozilla Public
    * License, v. 2.0. If a copy of the MPL was not distributed with this file,
    * You can obtain one at http://mozilla.org/MPL/2.0/. */
    
    #ifndef BAD_MIXED_FILE_H_
    #define BAD_MIXED_FILE_H_
    
    // Only this include is needed for the function declaration below.
    #include "nsCOMPtr.h"
    
    // These includes are only needed for the class definition.
    #include "nsIFile.h"
    #include "mozilla/ComplexBaseClass.h"
    
    namespace mozilla {
    
    class WrappedFile : public nsIFile, ComplexBaseClass {
    // ... class definition left out for clarity
    };
    
    // Assuming that most translation units that include this file only call
    // the function, but don't need the class definition, this should be in a
    // header file on its own in order to avoid pulling in the other
    // dependencies everywhere.
    nsCOMPtr<nsIFile> CreateDefaultWrappedFile(nsCOMPtr<nsIFile>&& aFileToWrap);
    
    } // namespace mozilla
    
    #endif // BAD_MIXED_FILE_H_
    

基于这些规则的示例头文件(带有一些额外的注释)

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef DOM_BASE_FOO_H_
#define DOM_BASE_FOO_H_

// Include guards should come at the very beginning and always use exactly
// the style above. Otherwise, compiler optimizations that avoid rescanning
// repeatedly included headers might not hit and cause excessive compile
// times.

#include <cstdint>
#include "nsCOMPtr.h"  // This is needed because we have a nsCOMPtr<T> data member.

class nsIFile;  // Used as a template argument only.
enum class nsresult : uint32_t; // Used as a parameter type only.
template <class T>
class RefPtr;   // Used as a return type only.

namespace mozilla::dom {

class Document; // Used as a template argument only.

// Scoped enum, not as a nested type, so it can be
// forward-declared elsewhere.
enum class FooKind { Small, Big };

class Foo {
public:
  // Do not put the implementation in the header file, it would
  // require including nsIFile.h
  Foo(nsCOMPtr<nsIFile> aFile, FooKind aFooKind);

  RefPtr<Document> CreateDocument();

  void SetResult(nsresult aResult);

  // Even though we will default this destructor, do this in the
  // implementation file since we would otherwise need to include
  // nsIFile.h in the header.
  ~Foo();

private:
  nsCOMPtr<nsIFile> mFile;
};

} // namespace mozilla::dom

#endif // DOM_BASE_FOO_H_

相应的实现文件

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/dom/Foo.h"  // corresponding header

#include "mozilla/Assertions.h"  // Needed for MOZ_ASSERT.
#include "mozilla/dom/Document.h" // Needed because we construct a Document.
#include "nsError.h"  // Needed because we use NS_OK aka nsresult::NS_OK.
#include "nsIFile.h"  // This is needed because our destructor indirectly calls delete nsIFile in a template instance.

namespace mozilla::dom {

// Do not put the implementation in the header file, it would
// require including nsIFile.h
Foo::Foo(nsCOMPtr<nsIFile> aFile, FooKind aFooKind)
 : mFile{std::move(aFile)} {
}

RefPtr<Document> Foo::CreateDocument() {
  return MakeRefPtr<Document>();
}

void Foo::SetResult(nsresult aResult) {
   MOZ_ASSERT(aResult != NS_OK);

   // do something with aResult
}

// Even though we default this destructor, do this in the
// implementation file since we would otherwise need to include
// nsIFile.h in the header.
Foo::~Foo() = default;

} // namespace mozilla::dom

包含指令

  • 排序

    • 在实现文件(cpp 文件)中,第一个包含指令应包含相应的头文件,后面跟一个空行。

    • 任何条件包含(取决于某些 #ifdef 或类似内容)都应在非条件包含之后。不要将它们混合在一起。

    • 不要在非条件包含之间放置注释。

    错误 1679522 通过 clang-format 处理自动化排序,这将强制执行一些更严格的规则。预计包含文件将被重新排序。如果您包含的第三方头文件不是自包含的,因此需要以特定顺序包含,则将这些头文件(仅限这些头文件)括在 // clang-format off// clang-format on 之间。对于 Mozilla 头文件,不应这样做,如果 Mozilla 头文件不是自包含的,则应将其设为自包含的。

  • 方括号与引号:C/C++ 标准库头文件使用方括号包含(例如 #include <cstdint>),所有其他包含指令都使用(双)引号(例如 #include "mozilla/dom/Document.h)。

  • 导出头文件应始终从其导出路径包含,而不是从其在树中的源路径包含,即使在本地可用也是如此。例如,始终使用 #include "mozilla/Vector.h",而不是 #include "Vector.h",即使在 mfbt 内部也是如此。

  • 通常,您应该只包含所需的头文件,不多不少。不幸的是,这并不容易看出。也许 C++20 模块会对此有所改进,但它的采用需要很长时间。

  • 基本规则是,如果您在文件中字面使用了在头文件 A.h 中声明的符号,则包含该头文件。特别是在头文件中,检查前向声明或包含转发头文件是否足够,请参见第 Header files 节。

    在某些情况下,此基本规则是不够的。您需要包含其他头文件的一些情况是

    • 您引用了代码中未明确提及的类型的成员,例如,它是您正在调用的函数的返回值。

    也有一些情况,基本规则会导致冗余包含。请注意,这里的“冗余”并非指“意外冗余”的头文件,例如,在编写时 mozilla/dom/BodyUtil.h 包含 mozilla/dom/FormData.h,但它不需要(它只需要一个前向声明),因此在包含 mozilla/dom/BodyUtil.h 时包含 mozilla/dom/FormData.h 是“意外冗余”。mozilla/dom/BodyUtil.h 的包含可能会随时更改,因此,如果包含 mozilla/dom/BodyUtil.h 的文件需要 mozilla::dom::FormData 的完整定义,则它应该自己包含 mozilla/dom/FormData.h。事实上,这些“意外冗余”的头文件**必须**包含。依赖于意外冗余的包含使得对头文件的任何更改都变得极其困难,尤其是在考虑到意外冗余包含的集合在不同平台之间存在差异时。但有些情况实际上是非意外冗余的,这些情况可以并且通常不应该重复。

    • 头文件的包含不需要在其对应的实现文件中重复。理由:实现文件及其对应的头文件本身是紧密耦合的。

    宏是一个特殊情况。通常,字面规则也适用于此,即如果宏定义引用了某个符号,则包含宏定义的文件应包含定义该符号的头文件。例如,在 nsISupportsImpl.h 中定义的 NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE 利用了在 mozilla/Assertions.h 中定义的 MOZ_ASSERT,因此 nsISupportsImpl.h 包含 mozilla/Assertions.h。但是,这需要人们判断意图,因为从技术上讲,只有宏的调用引用了符号(这就是 include-what-you-use 处理此问题的方式)。它可能取决于上下文或参数实际上引用了哪个符号,有时这是故意的。在这些情况下,宏的用户需要包含所需的头文件。

COM 和指针

  • 使用 nsCOMPtr<> 如果您不知道如何使用它,请开始在代码中查找示例。一般规则是,键入 NS_RELEASE 的行为本身就应该提醒您质疑您的代码:“我应该在这里使用 nsCOMPtr 吗?” 通常,NS_RELEASE 的唯一有效用法是在将引用计数指针存储在长期存在的结构中时。

  • 使用 XPIDL 声明新的 XPCOM 接口,以便它们可以被脚本化。

  • 使用 nsCOMPtr 表示强引用,使用 nsWeakPtr 表示弱引用。

  • 不要直接使用 QueryInterface。请改用 CallQueryInterfacedo_QueryInterface

  • 使用 Contract IDs,而不是使用 do_CreateInstance/do_GetService 时的 CIDs。

  • 对于函数输出参数,使用指针而不是引用,即使对于基本类型也是如此。

IDL

使用小写开头或“interCaps”

在 IDL 中定义方法或属性时,第一个字母应小写,每个后续单词应大写。例如

long updateStatusBar();

尽可能使用属性

每当您在没有任何上下文的情况下检索或设置单个值时,都应使用属性。如果可以使用属性,则不要使用两种方法。使用属性可以逻辑地连接值的获取和设置,并使脚本代码看起来更简洁。

此示例的方法过多

interface nsIFoo : nsISupports
{
    long getLength();
    void setLength(in long length);
    long getColor();
};

下面的代码将生成完全相同的 C++ 签名,但更易于脚本化。

interface nsIFoo : nsISupports
{
    attribute long length;
    readonly attribute long color;
};

使用 Java 风格的常量

在 IDL 中定义可脚本化常量时,名称应全部大写,单词之间用下划线分隔。

const long ERROR_UNDEFINED_VARIABLE = 1;

另请参见

有关接口开发的详细信息以及更详细的样式指南,请参阅 接口开发指南

错误处理

尽早且经常检查错误

每次调用 XPCOM 函数时,都应检查错误条件。即使您知道该调用永远不会失败,也需要这样做。为什么?

  • 将来某人可能会更改被调用方以返回失败条件。

  • 相关对象可能位于另一个线程、另一个进程,甚至可能是另一台机器上。代理可能首先无法进行您的调用。

此外,当您创建一个可能失败的新函数(即它将返回一个 nsresult 或一个可能指示错误的 bool)时,您应该明确标记返回值,并且应始终检查返回值。例如

// for IDL.
[must_use] nsISupports
create();

// for C++, add this in *declaration*, do not add it again in implementation.
[[nodiscard]] nsresult
DoSomething();

有一些例外情况

  • 返回 boolnsresult 的谓词或 getter。

  • IPC 方法实现(例如,bool RecvSomeMessage())。

  • 大多数调用方将检查输出参数,请参见下文。

nsresult
SomeMap::GetValue(const nsString& key, nsString& value);

如果大多数调用方需要首先检查输出值,则添加 [[nodiscard]] 可能过于冗长。在这种情况下,将返回值更改为 void 可能是合理的选择。

还有一个静态分析属性 [[nodiscard]],可以将其添加到类声明中,以确保在返回这些声明时始终使用它们。

当错误意外发生时使用 NS_WARN_IF 宏

如果条件失败,可以在调试版本中使用 NS_WARN_IF 宏发出控制台警告。只有当故障出乎意料且不会由正常的网页内容引起时,才应使用此宏。

如果您正在编写希望在方法失败时发出警告的代码,请直接使用 NS_WARNING 或使用新的 NS_WARN_IF 宏。

if (NS_WARN_IF(somethingthatshouldbefalse)) {
  return NS_ERROR_INVALID_ARG;
}

if (NS_WARN_IF(NS_FAILED(rv))) {
  return rv;
}

以前,NS_ENSURE_* 宏用于此目的,但这些宏隐藏了 return 语句,不应在新代码中使用。(此编码风格规则通常未达成一致,因此 NS_ENSURE_* 的使用可能是有效的。)

立即从错误中返回

在大多数情况下,当发生错误条件时,您的本能反应应该是从当前函数返回。不要这样做

rv = foo->Call1();
if (NS_SUCCEEDED(rv)) {
  rv = foo->Call2();
  if (NS_SUCCEEDED(rv)) {
    rv = foo->Call3();
  }
}
return rv;

而是这样做

rv = foo->Call1();
if (NS_FAILED(rv)) {
  return rv;
}

rv = foo->Call2();
if (NS_FAILED(rv)) {
  return rv;
}

rv = foo->Call3();
if (NS_FAILED(rv)) {
  return rv;
}

为什么?错误处理不应模糊代码的逻辑。在第一个示例中,作者的意图是依次进行 3 次调用。将调用包装在嵌套的 if() 语句中反而模糊了代码最可能的行为。

考虑一个更复杂的示例来隐藏错误

bool val;
rv = foo->GetBooleanValue(&val);
if (NS_SUCCEEDED(rv) && val) {
  foo->Call1();
} else {
  foo->Call2();
}

作者的意图可能是,只有当 val 的值为 false 时才会发生 foo->Call2()。事实上,当 foo->GetBooleanValue(&val) 失败时,也会调用 foo->Call2()。这可能是也可能不是作者的意图。从这段代码中看不出来。这是一个更新的版本

bool val;
rv = foo->GetBooleanValue(&val);
if (NS_FAILED(rv)) {
  return rv;
}
if (val) {
  foo->Call1();
} else {
  foo->Call2();
}

在此示例中,作者的意图很明确,错误条件避免了对 foo->Call1()foo->Call2(); 的两次调用。

可能的例外情况:有时调用失败并非致命。例如,如果您通知一系列观察者某个事件已触发,则其中一个通知失败可能无关紧要。

for (size_t i = 0; i < length; ++i) {
  // we don't care if any individual observer fails
  observers[i]->Observe(foo, bar, baz);
}

另一种可能性是,您不确定某个组件是否存在或是否已安装,并且如果找不到该组件,您希望继续正常运行。

nsCOMPtr<nsIMyService> service = do_CreateInstance(NS_MYSERVICE_CID, &rv);
// if the service is installed, then we'll use it.
if (NS_SUCCEEDED(rv)) {
  // non-fatal if this fails too, ignore this error.
  service->DoSomething();

  // this is important, handle this error!
  rv = service->DoSomethingImportant();
  if (NS_FAILED(rv)) {
    return rv;
  }
}

// continue normally whether or not the service exists.

字符串

注意

本节与 String guide 中提供的更详细的建议重叠。这些建议最终应该合并。目前,请参阅该指南以获取更多建议。

  • 函数的字符串参数应声明为 [const] nsA[C]String&

  • 优先使用字符串字面量。特别是,使用空字符串字面量,即 u""_ns""_ns,而不是 Empty[C]String()const nsAuto[C]String empty;。仅当您特别需要 const ns[C]String& 时才使用 Empty[C]String(),例如使用三元运算符或当您需要返回/绑定到引用或获取空字符串的地址时。

  • 对于 16 位字面量字符串,使用 u"..."_ns 或,如果需要,使用 NS_LITERAL_STRING_FROM_CSTRING(...),而不是 nsAutoString() 或其他会进行运行时转换的方式。请参见下文 Avoid runtime conversion of string literals

  • 要将字符串与字面量进行比较,请使用 .EqualsLiteral("...")

  • 使用 str.IsEmpty() 而不是 str.Length() == 0

  • 使用 str.Truncate() 而不是 str.SetLength(0)str.Assign(""_ns)str.AssignLiteral("")

  • 不要使用 ctype.h 中的函数(isdigit()isalpha() 等)或 strings.h 中的函数(strcasecmp()strncasecmp())。这些函数对区域设置敏感,这使得它们不适合处理协议文本。同时,它们的功能过于有限,无法正确处理自然语言文本。请使用 mozilla/TextUtils.hnsUnicharUtils.h 中的替代方案替换 ctype.h。在 strings.h 的位置,优先使用 nsStringComparator 设施来比较字符串,或者如果必须使用以零结尾的字符串,请使用 nsCRT.h 进行 ASCII 不区分大小写的比较。

对于局部值,使用字符串的 Auto 形式

在声明局部、短暂的 nsString 类时,始终使用 nsAutoStringnsAutoCString。这些会在栈上预分配一个 64 字节的缓冲区,并避免堆碎片。不要这样做

nsresult
foo()
{
  nsCString bar;
  ..
}

而是

nsresult
foo()
{
  nsAutoCString bar;
  ..
}

注意返回 char* 或 PRUnichar* 的非 XPCOM 函数泄漏的值

从内部辅助函数返回已分配的字符串,然后在代码中内联使用该函数而不释放其值,这是一个容易出现的陷阱。请考虑以下代码

static char*
GetStringValue()
{
  ..
  return resultString.ToNewCString();
}

  ..
  WarnUser(GetStringValue());

在上述示例中,WarnUser 将获得从 resultString.ToNewCString() 分配的字符串并丢弃指针。最终结果值永远不会被释放。相反,要么使用字符串类,以确保字符串在超出作用域时自动释放,要么确保手动释放字符串。

自动清理

static void
GetStringValue(nsAWritableCString& aResult)
{
  ..
  aResult.Assign("resulting string");
}

  ..
  nsAutoCString warning;
  GetStringValue(warning);
  WarnUser(warning.get());

手动释放字符串

static char*
GetStringValue()
{
  ..
  return resultString.ToNewCString();
}

  ..
  char* warning = GetStringValue();
  WarnUser(warning);
  nsMemory::Free(warning);

避免字符串字面量的运行时转换

通常需要将字面量字符串的值(例如 "Some String")赋值给 Unicode 缓冲区。与其使用 nsStringAssignLiteralAppendLiteral,请改用用户定义的字面量,例如 u”foo”_ns。在大多数平台上,这将强制编译器编译原始 Unicode 字符串,并直接赋值。在通过用于 8 位和 16 位方式的宏定义字面量的情况下,可以使用 NS_LITERAL_STRING_FROM_CSTRING 在编译时进行转换。

错误的

nsAutoString warning;
warning.AssignLiteral("danger will robinson!");
...
foo->SetStringValue(warning);
...
bar->SetUnicodeValue(warning.get());

正确的

constexpr auto warning = u"danger will robinson!"_ns;
...
// if you'll be using the 'warning' string, you can still use it as before:
foo->SetStringValue(warning);
...
bar->SetUnicodeValue(warning.get());

// alternatively, use the wide string directly:
foo->SetStringValue(u"danger will robinson!"_ns);
...

// if a macro is the source of a 8-bit literal and you cannot change it, use
// NS_LITERAL_STRING_FROM_CSTRING, but only if necessary.
#define MY_MACRO_LITERAL "danger will robinson!"
foo->SetStringValue(NS_LITERAL_STRING_FROM_CSTRING(MY_MACRO_LITERAL));

// If you need to pass to a raw const char16_t *, there's no benefit to
// go through our string classes at all, just do...
bar->SetUnicodeValue(u"danger will robinson!");

// .. or, again, if a macro is the source of a 8-bit literal
bar->SetUnicodeValue(u"" MY_MACRO_LITERAL);

PR_(MAX|MIN|ABS|ROUNDUP) 宏调用的用法

使用标准库函数(std::max),而不是 PR_(MAX|MIN|ABS|ROUNDUP)

使用 mozilla::Abs 而不是 PR_ABS。在 C++ 代码中,所有 PR_ABS 调用都已替换为 mozilla::Abs 调用,如 bug 847480 中所述。Firefox/core/toolkit 中的所有新代码都需要 #include "nsAlgorithm.h" 并使用 NS_foo 变体而不是 PR_foo,或者 #include "mozilla/MathAlgorithms.h" 用于 mozilla::Abs

SpiderMonkey 根类型定义的用法

js/public/TypeDecls.h 中的根类型定义(例如 HandleObjectRootedObject)在 SpiderMonkey 内部和外部均已弃用。它们最终将被移除,不应在新代码中使用。