在 Mozilla 代码中使用 C++¶
C++ 语言特性¶
Mozilla 代码仅使用 C++ 的一个子集。运行时类型信息 (RTTI) 已禁用,因为它往往会导致代码大小大幅增加。这意味着 dynamic_cast
、typeid()
和 <typeinfo>
无法在 Mozilla 代码中使用。异常也被禁用;不要使用 try
/catch
或抛出任何异常。如果愿意将抛出视为中止,则可以使用抛出异常的库。
在扩展 C++ 的方面,我们使用 -fno-strict-aliasing
进行编译。这意味着,当将指针重新解释为不同类型的指针时,在取消引用重新解释的指针时,无需遵守标准中的“有效类型”(被指向对象)规则(也称为“严格别名规则”)。您仍然需要确保不会违反对齐要求,并且需要确保在取消引用指针以进行读取时,指向的内存位置处的数据根据指针的类型形成有效值。同样,如果您通过取消引用重新解释的指针进行写入,并且原始类型的指针可能仍被取消引用以进行读取,则需要确保您写入的值根据原始类型有效。对于例如所有位模式都是有效值的原始整数,此值有效性问题是无关紧要的。
从 Mozilla 59 开始,构建 Mozilla 需要 C++14 模式。
从 Mozilla 67 开始,不再可以使用 MSVC 构建 Mozilla。
从 Mozilla 73 开始,构建 Mozilla 需要 C++17 模式。
这意味着在所有平台上受支持的情况下都可以使用 C++17。可接受的功能列表如下所示
GCC |
Clang |
||
---|---|---|---|
当前最低要求 |
8.1 |
8.0 |
|
特性 |
GCC |
Clang |
可在代码中使用 |
|
4.3 |
2.9 |
是(见注释) |
方法上的引用限定符 |
4.8.1 |
2.9 |
是 |
默认成员初始化器(位字段除外) |
4.7 |
3.0 |
是 |
默认成员初始化器(用于位字段) |
8 |
6 |
否 |
可变参数模板 |
4.3 |
2.9 |
是 |
初始化列表 |
4.4 |
3.1 |
是 |
|
4.3 |
2.9 |
是 |
|
4.4 |
2.9 |
是 |
lambda 表达式 |
4.5 |
3.1 |
是 |
|
4.3 |
2.9 |
是 |
|
4.3 |
2.9 |
是 |
|
4.4 |
3.1 |
是 |
模板别名 |
4.7 |
3.0 |
是 |
|
4.6 |
3.0 |
是 |
|
4.4 |
2.9 |
是 |
|
4.4 |
2.9 |
是 |
|
4.6 |
3.1 |
是 |
|
4.8 |
3.3 |
否(见注释) |
|
4.6 |
3.1 |
是 |
|
4.8 |
3.3 |
是 |
|
4.8 |
3.3 |
是,但请参阅注释;仅 clang 3.6 声称 as_feature(cxx_alignof) |
委托构造函数 |
4.7 |
3.0 |
是 |
继承构造函数 |
4.8 |
3.3 |
是 |
|
4.5 |
3.0 |
是 |
|
4.4 |
3.0 |
是 |
|
4.5 |
3.0 |
是 |
|
4.7 |
3.1 |
是 |
|
4.4 |
2.9 |
是 |
|
4.4 |
3.0 |
是 |
无限制联合 |
4.6 |
3.1 |
是 |
|
4.6 |
3.0 |
是 |
|
4.7 |
3.0 |
是 |
|
4.8 |
3.3 |
否(见注释) |
函数模板默认参数 |
4.3 |
2.9 |
是 |
局部结构体作为模板参数 |
4.5 |
2.9 |
是 |
扩展的友元声明 |
4.7 |
2.9 |
是 |
|
4.9 |
2.9 |
是 |
“一些 C++ 上下文转换的调整”(C++14) |
4.9 |
3.4 |
是 |
返回类型推导(C++14) |
4.9 |
3.4 |
是(但仅在您原本会使用 |
泛型 lambda 表达式(C++14) |
4.9 |
3.4 |
是 |
初始化的 lambda 捕获(C++14) |
4.9 |
3.4 |
是 |
数字分隔符(C++14) |
4.9 |
3.4 |
是 |
变量模板(C++14) |
5.0 |
3.4 |
是 |
放宽的 constexpr(C++14) |
5.0 |
3.4 |
是 |
聚合成员初始化(C++14) |
5.0 |
3.3 |
是 |
阐明内存分配(C++14) |
5.0 |
3.4 |
是 |
[[deprecated]] 属性(C++14) |
4.9 |
3.4 |
否(见注释) |
大小已知的释放(C++14) |
5.0 |
3.4 |
否(见注释) |
概念(Concepts TS) |
6.0 |
— |
否 |
内联变量(C++17) |
7.0 |
3.9 |
是 |
constexpr_if(C++17) |
7.0 |
3.9 |
是 |
constexpr lambda 表达式(C++17) |
— |
— |
否 |
结构化绑定(C++17) |
7.0 |
4.0 |
是 |
在 |
7.0 |
3.9 |
是 |
折叠表达式(C++17) |
6.0 |
3.9 |
是 |
[[fallthrough]]、[[maybe_unused]]、[[nodiscard]](C++17) |
7.0 |
3.9 |
是 |
对齐分配/释放(C++17) |
7.0 |
4.0 |
否(见注释) |
指定初始化器(C++20) |
8.0 (4.7) |
10.0 (3.0) |
是 [sic](见注释) |
#pragma once |
3.4 |
是 |
直到我们 标准化头文件之前 |
8.0 |
— |
否 |
来源¶
注释¶
- 右值引用
无法使用隐式移动方法生成。
- 属性
一些常见属性在 mozilla/Attributes.h 或 nscore.h 中定义。
- 对齐
一些对齐实用程序在 mozilla/Alignment.h 中定义。
警告
MOZ_ALIGNOF
和alignof
的语义不同。注意您对它们的期望。[[deprecated]]
如果我们有已弃用的代码,我们应该删除它而不是将其标记为已弃用。将内容标记为
[[deprecated]]
也意味着如果使用已弃用的 API,编译器会发出警告,这会在我们的自动化构建中变成致命错误,这没有帮助。- 大小已知的释放
我们的编译器都支持此功能(GCC 和 Clang 需要自定义标志),但启用它会破坏某些类的
operator new
方法,并且需要进行 一些工作才能使其成为我们自定义内存分配器的效率提升。- 对齐分配/释放
我们的自定义内存分配器不支持这些函数。
- 线程局部变量
Android 上不支持
thread_local
。- 指定初始化器
尽管它们是 C++ 中较晚添加的功能(并且编译器直到最近才正式支持),但 C++20 的指定初始化器仅仅是 最初在 C99 中引入的功能的一个子集——并且自至少 GCC 4.7 和 Clang 3.0 以来,此子集已在 C++ 代码中被接受,且未加评论。
C++ 和 Mozilla 标准库¶
Mozilla 代码库包含多个子项目,它们遵循不同的规则来确定哪些库可以和不可以使用。此处列出的规则适用于正常的平台代码,并假设可以无限制地使用 MFBT 或 XPCOM API。
警告
本节的其余部分是出于说明和探索目的而编写的草稿。请勿相信此处列出的信息。
以下是 Mozilla 或 C++ 标准提供的标准库组件列表。如果此处未列出 API,则不允许在 Mozilla 代码中使用它。此处未列出已弃用的 API。一般来说,即使允许使用后者,也优先使用 Mozilla 数据结构的变体而不是标准 C++ 数据结构,因为 Mozilla 变体往往具有标准库中找不到的功能(例如,内存大小跟踪)或具有更可控的性能特征。
已批准的标准库头文件列表维护在 config/stl-headers.mozbuild 中。
数据结构¶
名称 |
头文件 |
STL 等价物 |
注释 |
---|---|---|---|
|
|
类似于 |
|
|
|
类似于 |
|
|
|
概率集合成员资格(参见 维基百科) |
|
|
|
nsTHashtable 的适配,参见 XPCOM 哈希表指南 |
|
|
|
类似于 |
|
|
|
|
nsTHashtable 的适配,参见 XPCOM 哈希表指南 |
|
|
|
nsTHashtable 的适配,参见 XPCOM 哈希表指南 |
|
|
|
|
|
|
类似于 |
|
|
|
通用哈希映射和哈希集。 |
|
|
|
|
nsTHashtable 的适配,参见 XPCOM 哈希表指南 |
|
|
|
双向链表 |
|
|
|
nsTHashtable 的适配,参见 XPCOM 哈希表指南 |
|
|
|
向量元素的双向链表 |
|
|
快速访问最近访问的元素(参见维基百科) |
|
|
|
|
|
|
|
|
参见XPCOM 哈希表指南,您可能需要一个子类 |
|
|
类似于 |
|
|
|
|
与STL类不同,它不是容器适配器 |
|
|
|
|
|
|
与 |
安全实用程序¶
名称 |
头文件 |
STL 等价物 |
注释 |
---|---|---|---|
|
|
安全的数组索引 |
|
|
|
强制类型转换 |
|
|
|
避免溢出 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
类似于 |
|
|
|
|
|
|
|
|
Rust 的切片概念在 C++ 中的实现(没有借用检查) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
字符串¶
有关nsAString
(我们用于替换std::u16string
的写时复制实现)和nsACString
(我们用于替换std::string
的写时复制实现)的用法,请参见Mozilla 内部字符串指南。
请确保不要引入更多std::wstring
的使用,它不可移植!(IPC 代码中存在一些用法。)
算法¶
|
|
|
|
|
(rotate、ctlz、popcount、gcd、abs、lcm) |
|
|
并发¶
名称 |
头文件 |
STL/boost 等价物 |
注释 |
---|---|---|---|
|
mfbt/Atomic.h |
|
|
|
xpcom/threads/CondVar.h |
|
|
|
xpcom/threads/DataMutex.h |
|
|
|
xpcom/threads/Monitor.h |
||
|
xpcom/threads/Mutex.h |
|
|
|
xpcom/threads/ReentrantMonitor.h |
||
|
xpcom/base/StaticMutex.h |
|
可以(实际上必须)用作全局/静态变量的互斥量。 |
其他¶
名称 |
头文件 |
STL/boost 等价物 |
注释 |
---|---|---|---|
|
mfbt/Alignment.h |
|
|
|
mfbt/MaybeOneOf.h |
|
~ |
|
mfbt/CompactPair.h |
|
最小空间! |
|
xpcom/ds/TimeStamp.h |
|
|
mozilla/PodOperations.h |
C++ 版本的 |
||
mozilla/ArrayUtils.h |
|||
mozilla/Compression.h |
|||
mozilla/Endian.h |
|||
mozilla/FloatingPoint.h |
|||
mozilla/HashFunctions.h |
|
||
mozilla/Move.h |
|
Mozilla 数据结构和标准 C++ 范围和迭代器¶
一些 Mozilla 定义的数据结构提供了 STL 风格的迭代器,并且可以在基于范围的 for 循环以及 STL算法中使用。
目前,这些包括
名称 |
头文件 |
错误 |
迭代器类别 |
注释 |
---|---|---|---|---|
|
|
随机访问 |
也可反向迭代。还支持通过 RemoveElementsAt 方法进行删除-擦除模式。还支持通过 |
|
|
|
前向 |
||
|
|
随机访问 |
也可反向迭代。 |
|
|
|
随机访问 |
也可反向迭代。 |
|
|
|
随机访问 |
也可反向迭代。 |
|
|
|
前向 |
||
|
|
丢失 |
也可反向迭代。 |
|
|
|
丢失 |
也可反向迭代。 |
|
|
|
随机访问 |
||
|
|
随机访问 |
也可反向迭代。 |
请注意,如果迭代器类别被声明为“丢失”,则该类型可能只能在基于范围的 for 循环中使用。这很可能是遗漏,可以很容易地修复。
在此上下文中,类模板IteratorRange
(可用于从任何一对迭代器构造范围)和函数模板Reversed
(可用于反转任何范围)也很有用,两者都在mfbt/ReverseIterator.h
中定义
更多 C++ 规则¶
不要使用静态构造函数¶
(您可能一开始就不应该使用全局变量。除了反对使用它们的重量级软件工程论点之外,全局变量还会影响启动时间!但有时我们必须做一些丑陋的事情。)
不可移植的示例
FooBarClass static_object(87, 92);
void
bar()
{
if (static_object.count > 15) {
...
}
}
曾经,编译器存在错误,可能导致全局对象的构造函数未被调用。这些错误现在可能早已不存在,但是即使该功能正常工作,正确排序 C++ 构造函数也存在很多问题,因此更容易使用 init 函数
static FooBarClass* static_object;
FooBarClass*
getStaticObject()
{
if (!static_object)
static_object =
new FooBarClass(87, 92);
return static_object;
}
void
bar()
{
if (getStaticObject()->count > 15) {
...
}
}
不要使用异常¶
请参阅本文档开头“C++ 语言特性”部分的介绍。
不要使用运行时类型信息¶
请参阅本文档开头“C++ 语言特性”部分的介绍。
如果您需要运行时类型,可以通过向层次结构的基类添加classOf()
虚成员函数并在每个子类中重写该成员函数来实现类似的结果。如果classOf()
为层次结构中的每个类返回唯一的值,则您将能够在运行时进行类型比较。
不要使用 C++ 标准库(包括 iostream 和 locale)¶
请参阅“C++ 和 Mozilla 标准库”部分。
使用 C++ lambda 表达式,但要谨慎¶
现在,所有编译器都支持 C++ lambda 表达式。欢呼吧!我们建议明确列出您在 lambda 表达式中捕获的变量,既是为了文档目的,也是为了仔细检查您是否只捕获了预期捕获的变量。
使用命名空间¶
可以根据C++ 编码风格中的样式指南使用命名空间。
不要混合使用可变参数和内联函数¶
什么?你为什么一开始就使用可变参数?!立即停止!
使头文件与 C 和 C++ 兼容¶
不可移植的示例
/*oldCheader.h*/
int existingCfunction(char*);
int anotherExistingCfunction(char*);
/* oldCfile.c */
#include "oldCheader.h"
...
// new file.cpp
extern "C" {
#include "oldCheader.h"
};
...
如果您使用公开的 C 接口创建新的头文件,请确保这些头文件在被 C 和 C++ 文件包含时都能正常工作。
(如果您需要在新的 C++ 文件中包含 C 头文件,那应该可以正常工作。如果不行,那是 C 头文件维护者的错,所以如果可以,请修复头文件,如果不行,您想出的任何 hack 可能都没问题。)
可移植的示例
/* oldCheader.h*/
PR_BEGIN_EXTERN_C
int existingCfunction(char*);
int anotherExistingCfunction(char*);
PR_END_EXTERN_C
/* oldCfile.c */
#include "oldCheader.h"
...
// new file.cpp
#include "oldCheader.h"
...
除了良好的风格之外,这样做还有很多原因。一方面,您正在为其他人简化生活,在一个公共位置(头文件)中完成工作,而不是所有包含它的 C++ 文件中。此外,通过使 C 头文件对 C++ 安全,您记录了“嘿,此文件现在已在 C++ 中包含”。这是一件好事。您还可以避免一个难以修复的重大可移植性噩梦……
在子类虚成员函数上使用 override¶
override
关键字在 C++11 和我们所有支持的编译器中都受支持,并且它可以捕获错误。
始终声明复制构造函数和赋值运算符¶
许多类不应该被复制或赋值。如果您正在编写其中一个类,那么执行策略的方法是将已删除的复制构造函数声明为私有,并且不提供定义。同时,对用于赋值相同类对象的赋值运算符执行相同的操作。示例
class Foo {
...
private:
Foo(const Foo& x) = delete;
Foo& operator=(const Foo& x) = delete;
};
隐式调用复制构造函数的任何代码都将遇到编译时错误。这样,在暗处就不会发生任何事情。当用户的代码无法编译时,他们会看到他们正在按值传递,而他们本意是按引用传递(哎呀)。
小心具有类似签名的重载方法¶
当方法的类型签名仅在一个“抽象”类型(例如PR_Int32
或int32
)方面不同时,最好避免重载方法。当您将该代码移动到不同的平台时,您会发现,在 Foo2000 编译器上,您的重载方法突然具有相同的类型签名。
对标量常量进行类型化以避免意外的歧义¶
不可移植的代码
class FooClass {
// having such similar signatures
// is a bad idea in the first place.
void doit(long);
void doit(short);
};
void
B::foo(FooClass* xyz)
{
xyz->doit(45);
}
请务必对标量常量进行类型化,例如uint32_t(10)
或10L
。否则,您可能会产生模棱两可的函数调用,这些调用可能会解析为多个方法,特别是如果您没有遵循上述 (2) 中的内容。并非所有编译器都会标记模棱两可的方法调用。
可移植的代码
class FooClass {
// having such similar signatures
// is a bad idea in the first place.
void doit(long);
void doit(short);
};
void
B::foo(FooClass* xyz)
{
xyz->doit(45L);
}
在 XPCOM 代码中使用 nsCOMPtr¶
有关用法详细信息,请参阅nsCOMPtr
用户手册。
不要使用以下划线开头的标识符¶
这条规则偶尔会让那些使用 C++ 数十年的程序员感到惊讶。但它直接来自 C++ 标准!
根据 C++ 标准 17.4.3.1.2 全局名称 [lib.global.names],第 1 段
某些名称集和函数签名始终保留给实现
每个包含双下划线 (__) 或以下划线后跟大写字母 (2.11) 开头的名称都保留给实现以供任何用途使用。
**每个以下划线开头的名称都保留给实现**以供用作全局命名空间中的名称。
对 C 或 C++ 有益的操作¶
尽可能避免条件 #include¶
如果可以将 #include
放到 #ifdef
外部,就不要将其放在 #ifdef
内部。无条件包含更好,因为它们使编译在所有平台和配置中更加相似,因此您不太可能在您从未使用过的其他人的首选平台上导致愚蠢的编译器错误。
错误代码示例
#ifdef MOZ_ENABLE_JPEG_FOUR_BILLION
#include <stdlib.h> // <--- don't do this
#include "jpeg4e9.h" // <--- only do this if the header really might not be there
#endif
当然,当您为不同的机器包含不同的系统文件时,您没有太多选择。那是不同的。
每个 .cpp 源文件都应该有一个唯一的名称¶
链接到 libxul 的每个目标文件都需要具有唯一的名称。避免使用 nsModule.cpp 等通用名称,而应使用 nsPlacesModule.cpp。
打开编译器的警告,然后编写无警告代码¶
在一个平台上生成警告的内容将在另一个平台上生成错误。打开警告。编写无警告代码。这对您有好处。通过将 ac_add_options --enable-warnings-as-errors
添加到您的 mozconfig 文件中,将警告视为错误。
在 struct
或 class
中对所有位字段使用相同的类型¶
某些编译器在为不同的位字段指定不同的类型时不会打包位。例如,以下结构的大小可能为 8 字节,即使它可以容纳 1 个字节。
struct {
char ch: 1;
int i: 1;
};
不要对位字段使用枚举类型¶
这方面的经典示例是对布尔位字段使用 PRBool
。不要那样做。 PRBool
是一种有符号整数类型,因此设置时的位字段值为 -1
而不是 +1
,这——我知道,很疯狂,对吧?C++ 黑客过去不得不忍受的事情……
无论如何,您都不应该使用 PRBool
。使用 bool
。类型为 bool
的位字段是可以的。
枚举在某些平台(在某些配置中)是有符号的,而在其他平台上是无符号的,因此不适合编写可移植代码,即使它们碰巧在您的系统上工作,即使每个位都很重要。