Ladybird 浏览器 C++ 编码风格详解:命名、注释、类型转换与 clang-format 实战指南
Ladybird 浏览器(一个独立开发的浏览器项目)对 C++ 代码有一套完整且强制执行的编码风格规范,本文以官方风格文档 Documentation/CodingStyle.md 为主体,完整覆盖其命名规则、成员前缀、访问器命名、构造初始化、单例模式、注释约定(含 Web 规范的 NOTE/NB 前缀机制)、const 摆放、类型转换、花括号省略等全部核心条款,并结合仓库根目录的 .clang-format 配置文件、Meta/Linters/lint_clang_format.py 校验脚本以及实际源码(如 AK/Vector.h、Libraries/LibWeb/CSS/Parser/ErrorReporter.h)逐条印证。读完本文,你将掌握 Ladybird 代码库的完整编码约定,能按其规范写出可直接通过 CI lint 检查的 C++ 代码。
一、总体约定:.clang-format 是第一权威
风格文档开篇明确:对于缩进、括号位置、大括号摆放等底层排版问题,所有代码都应遵循仓库根目录 .clang-format 文件指定的格式。文档特别强调必须使用正确版本的 clang-format——CI 中强制的版本由 Meta/Linters/lint_clang_format.py 定义,即:
CLANG_FORMAT_MAJOR_VERSION = 21
也就是说,Ladybird 项目要求 clang-format 21。如果发行版自带版本不匹配,脚本会明确警告:
You are using clang-format version X, which appears to not be clang-format 21.
It is very likely that the resulting changes are not what you wanted.
.clang-format 实际配置解读
根目录 .clang-format 以 BasedOnStyle: WebKit 为基础,并叠加了以下关键定制:
| 配置项 | 值 | 含义 |
|---|---|---|
BasedOnStyle |
WebKit |
继承 WebKit 基础风格(4 空格缩进、K&R 变体等) |
BraceWrapping.AfterFunction |
true |
函数定义的大括号换行(Allman 风格函数体) |
BreakBeforeBraces |
Custom |
配合 BraceWrapping 使用 |
BreakBeforeInheritanceComma |
true |
继承列表的逗号放在行首(:\n BaseA\n , BaseB) |
BreakConstructorInitializers |
BeforeComma |
构造函数初始化列表逗号在行首,与文档"逗号在前"的要求一致 |
QualifierAlignment: Right |
— | 限定符右对齐,对应"east const"风格(Salt const&) |
NamespaceIndentation |
None |
命名空间内部不额外缩进 |
WrapNamespaceBodyWithEmptyLines |
Always |
命名空间首尾强制空行(} // namespace AK 模式) |
RemoveSemicolon |
true |
移除空的多余分号 |
AlignTrailingComments.Kind: Always |
— | 尾随注释对齐 |
LineEnding |
LF |
统一 LF 换行 |
该文件还有一个独立的 Language: ObjC 作用域(块大小 4 空格缩进、ObjCBinPackProtocolList: Never 等),用于 macOS 的 AppKit UI 层(UI/AppKit)的 Objective-C++ 代码。
获取正确版本的 clang-format
Documentation/AdvancedBuildInstructions.md 的 "Clang-format updates" 一节给出两个方案(按推荐顺序):
- Debian/apt 系发行版:使用 LLVM 官方 apt 仓库安装最新发布的 clang-format;
- 从源码编译 LLVM(参见 LLVM 官方 Getting Started 文档)。
CI 校验脚本的工作方式
Meta/Linters/lint_clang_format.py 的完整流程:
- 文件发现:不带参数时执行
git ls-files -- '*.cpp' '*.h' '*.mm' ':!:Base',即对 Git 跟踪的所有 C/C++/ObjC 源文件(排除 Base 目录)做格式化; - 可执行文件查找:优先查找版本化可执行文件
clang-format-21,其次查找 Homebrew 的llvm@21前缀下的bin/clang-format,最后退回 PATH 中的clang-format并校验大版本号; - 执行:调用
clang-format -style=file -i <files>(-style=file表示读取仓库内的 .clang-format);Windows 上因命令行长度限制会按每批 10 个文件分块执行; - 参数:
--overwrite-inplace为必需参数,用于就地覆盖文件格式。
配套的 .clangd 文件(仓库根目录)则负责 IDE 支持:它声明编译数据库位于 Build/release(可按构建配置手动修改),并规定 AK/.*、Lib.* 等库头文件使用尖括号包含方式(AngledHeaders),这与后文 "using" 规则中 #include <AK/Vector.h> 的写法相呼应。
二、命名规则:CamelCase、snake_case 与 SCREAMING_CASE 的混合体系
基础命名法
- 类、结构体、命名空间:CamelCase,且缩写词全字母大写(如
FileDescriptor而非FileDescriptor中的FD或Filedescriptor); - 变量与函数:snake_case(全小写,下划线分词);
- 常量(全局与静态成员变量):SCREAMING_CASE。
正确示例:
struct Entry;
size_t buffer_size;
class FileDescriptor;
String absolute_path();
错误示例:
struct data;
size_t bufferSize;
class Filedescriptor;
String MIME_Type();
与 Web 规范(spec)命名保持一致
实现规范算法及规范明确命名的构造时,应尽量贴近规范原文命名。文档举的例子来自 HTML 规范中的 "Suffering from being missing" 概念:
bool HTMLInputElement::suffering_from_being_missing(); // 与 spec 命名完全一致
bool HTMLInputElement::has_missing_constraint(); // 错误:擅自改写 spec 命名
使用完整单词
使用完整单词,除非缩写是更标准、更易理解的写法:
size_t character_size;
size_t length;
short tab_index; // More canonical.
size_t char_size;
size_t len;
short tabulation_index; // Goofy.
成员变量前缀
类内数据成员应为 private;静态数据成员前缀 s_,普通数据成员前缀 m_,全局变量前缀 g_:
class String {
public:
...
private:
int m_length { 0 };
};
错误示例是把 int length { 0 }; 直接暴露到类的非 private 区且无前缀。这一约定在仓库中无处不在,例如 AK/Singleton.h 中:
private:
mutable Atomic<T*> m_obj { nullptr };
访问器命名
setter 以 set 开头,getter 用裸词,且与变量名对应:
void set_count(int); // Sets m_count.
int count() const; // Returns m_count.
void set_count(int); // Sets m_the_count. —— 错误:setter/getter 与变量名不匹配
int get_count() const; // Returns m_the_count. —— 错误:getter 不该加 get_ 前缀
通过出参(out argument)返回值的"getter"必须以 get 开头:
void get_filename_and_inode_id(String&, InodeIdentifier&) const;
使用描述性动词
bool convert_to_ascii(short*, size_t); // 正确
bool to_ascii(short*, size_t); // 错误
ensure_ 前缀
当同一变量有两个 getter,其中一个会自动确保对象被实例化时,该 getter 以 ensure_ 前缀命名,并且必须返回引用而非指针(因为它保证对象一定被创建):
Inode* inode();
Inode& ensure_inode(); // 正确
Inode& inode();
Inode* ensure_inode(); // 错误
这一"确保创建"语义与 AK/Singleton.h 中的 ensure_instance() 方法思想一致——先触发惰性初始化再使用。
声明中省略无意义参数名
参数类型名包含参数名(去掉尾部数字或复数形式)时,参数名应省略。bool、字符串和数值类型通常应保留参数名:
void set_count(int);
void do_something(Context*); // 正确
void set_count(int count);
void do_something(Context* context); // 错误
用枚举代替 bool 参数
当调用方可能传入常量时,函数参数优先使用枚举而非 bool,因为命名常量在调用点可读性更好。例外是 setter:函数名本身已说明该 bool 的含义。
do_something(something, AllowFooBar::Yes);
paint_text_with_shadows(context, ..., text_stroke_width > 0, is_horizontal());
set_resizable(false); // 正确
do_something(something, false); // 错误:false 指代不明
set_resizable(NotResizable); // 错误:setter 不应改用枚举
枚举成员使用首字母大写的 InterCaps 风格。
常量与头文件守卫
- 优先
const而非#define;优先内联函数而非宏; #define常量使用全大写下划线分隔命名;- 头文件守卫使用
#pragma once,而不是#define/#ifdef:
// MyClass.h
#pragma once
// MyClass.h
#ifndef MyClass_h
#define MyClass_h
仓库中如 AK/Singleton.h 均使用 #pragma once,与该条款一致。
三、构造、迭代与指针/引用
构造函数初始化
构造函数应使用 C++ 初始化列表语法初始化成员。每个成员(和超类)单独占一行,冒号或逗号放在该行的行首(与 .clang-format 的 BreakConstructorInitializers: BeforeComma、BreakBeforeInheritanceComma: true 精确对应)。同时优先在成员定义处就地初始化:
class MyClass {
...
Document* m_document { nullptr };
int m_my_member { 0 };
};
MyClass::MyClass(Document* document)
: MySuperClass()
, m_document(document)
{
}
MyOtherClass::MyOtherClass()
: MySuperClass()
{
}
错误写法包括:在函数体内赋值(m_myMember = 0;)、把本应在头文件就地初始化的成员放到构造初始化列表里、以及把整条初始化列表压缩到声明行(MyOtherClass::MyOtherClass() : MySuperClass() {})。
迭代:优先 range-for
遍历 Vector 时优先使用 range-for,其次是索引循环,避免手写迭代器:
for (auto& child : children)
child->do_child_thing();
for (int i = 0; i < children.size(); ++i) // OK
children[i]->do_child_thing();
for (auto it = children.begin(); it != children.end(); ++it) // Wrong
(*it)->do_child_thing();
指针与引用的写法、出参传递
指针/引用类型与 *、& 之间不留空格(类型名与符号紧邻)。函数出参应传引用,仅在出参可选的罕见情况下传指针:
void MyClass::get_some_value(OutArgumentType& out_argument) const
{
out_argument = m_value;
}
void MyClass::do_something(OutArgumentType* out_argument) const // 可选项
{
do_the_thing();
if (out_argument)
*out_argument = m_value;
}
void MyClass::get_some_value(OutArgumentType* outArgument) const // Wrong
{
*out_argument = m_value;
}
四、"using" 语句的边界
AK 子库头文件的例外
一般不允许在头文件里引入 using 声明,但 AK 子库的例外:可以在文件末尾用 using 声明把 AK 命名空间中的某个具体名字引入全局作用域:
// AK/Vector.h
namespace AK {
} // namespace AK
using AK::Vector; // 正确
using namespace AK; // 错误:using namespace
并且此例外只适用于 AK 子库自身——在其他库(例如 runtime/Object.h)的头文件中写 using AK::SomethingOrOther; 同样错误。仓库中的 AK/Singleton.h 正是这一条款的活样本:
#if USING_AK_GLOBALLY
using AK::Singleton;
#endif
实现文件中禁用 STL using 声明
在 .cpp 实现文件中,不要用任何形式的 using 声明引入标准模板库的名字,应在使用点直接限定:
// File.cpp
std::swap(a, b);
c = std::numeric_limits<int>::max() // 正确
using std::swap; swap(a, b); // 错误
using namespace std; // 错误
五、类型写法
- 使用
unsigned修饰符时省略 "int"; - 不要使用 "signed" 修饰符,用
int代替:
unsigned a;
int b; // 正确
unsigned int a; // Wrong:没省略 int
signed b; // Wrong:用了 signed
signed int c; // Wrong:既带 signed 又带 int
六、类与结构体
有方法的类型优先用 class,纯数据聚合用 struct:
- class:公共 getter/setter,成员
private且带m_前缀; - struct:全部字段 public,不需要
m_前缀。
struct Thingy {
String name;
int frob_count { 0 };
};
class Doohickey {
public:
String const& name() const { return m_name; }
int frob_count() const { return m_frob_count; }
void jam();
private:
String m_name;
int m_frob_count { 0 };
}
错误示例:struct 里混用 public/private 和 m_ 前缀、class 里把 String name; 直接暴露给 public 并用 this->name 访问。
单参数构造:隐式转换的判定
当参数在语义上可视为类型转换、且转换是快速操作时,用单参数构造函数做隐式转换;否则用 explicit 关键字或返回该类型的函数。此规则只针对单参数构造函数:
class LargeInt {
public:
LargeInt(int); // 快速类型转换:隐式
...
class Vector {
public:
explicit Vector(int size); // 不是类型转换
Vector create(Array); // 昂贵转换应走函数
...
class Task {
public:
Task(ExecutionContext&); // Wrong:不是类型转换,应 explicit
explicit Task(); // Wrong:无参构造不应 explicit
explicit Task(ExecutionContext&, Other); // Wrong:多参构造不应 explicit
...
七、单例模式:the() 静态成员函数
单例的访问入口必须是一个名为 the() 的静态成员函数,不得用自由函数或其他名称:
class UniqueObject {
public:
static UniqueObject& the(); // 正确
...
static UniqueObject& shared(); // Wrong:命名不符
UniqueObject& my_unique_object(); // Wrong:不应是自由函数
仓库中 Libraries/LibWeb/CSS/Parser/ErrorReporter.h 就是标准实现:
static ErrorReporter& the();
八、注释规范://、FIXME、以及 spec 笔记的 NOTE/NB 前缀机制
- 注释用
//而非/* */(版权头除外); - 注释写成完整句子:首字母大写、以句号等标点结尾。两个例外:行尾短注释(
if (x == y) // false for NaN),以及从规范中逐字摘录的注释——应原样引用,除换行或补充排版所需的符号(如数字的幂用**,因为规范 HTML 里的<sup>标签不会出现在复制文本中)外不得修改; - 长注释应换行,行宽以 120 字符为宜;
- 用
FIXME:标注需要未来处理的事项,TODO:也可接受,两者都不带署名。
draw_jpg(); // FIXME: Make this code handle jpg in addition to the png support. // 正确
draw_jpg(); // TODO: ... // OK
draw_jpg(); // FIXME(joe): ... // Wrong:不应署名
- 注释应解释为什么,而不是复述代码在做什么:
i++; // Go to the next page. // 正确:解释意图
page_index++; // 更佳:让名字自己说话
i++; // Increment i. // Wrong:无信息量
规范笔记前缀:NOTE 保留给 spec,NB: 留给自己
许多 Web 规范的笔记以 NOTE: ... 开头。为了让规范笔记能逐字复制进代码,Ladybird 保留 NOTE: 前缀给规范原文,开发者自己的笔记统一使用 NB:(nota bene)前缀。此约定仅适用于直接实现规范算法/行为的代码中的注释,其他位置的注释不需要前缀。
规范草稿中的疑问用双方括号包含:[[ ... ]]。
// 2. If property is in already serialized, continue with the steps labeled declaration loop.
// NOTE: The prefabulated aluminite will not be suitable for use here. If the listed spec note is so long that we reach
// column 120, we wrap around and indent the lines to match up with the first line.
// NB: We _can_ actually use the aluminite since we unprefabulated it in step 1 for performance reasons.
// 3. For each property in window [[ in what order? ]]:
// LB-NOTE: The aluminite might come pre-prefabulated at this point. // Wrong:自造前缀
// Spec-note: Another example of a custom note prefix that we shouldn't use.
// There is no prefix whatsoever here... // Wrong:无前缀,难以区分是规范步骤、规范笔记还是开发者笔记
// 3. For each property in window (in what order?): // Wrong:疑问应使用 [[ ]]
九、虚方法重写:virtual + override/final 必须同时出现
类内虚方法的声明必须写 virtual 关键字;子类重写时必须同时写 virtual 和 override(或 final):
class Person {
public:
virtual String description() { ... };
}
class Student : public Person {
public:
virtual String description() override { ... }; // 正确:virtual + override
}
virtual String description() final { ... }; // 正确:virtual + final,且禁止再被重写
错误示例:只用 override(缺 virtual)、只用 final(缺 virtual)、或只写 virtual 而不加 override/final。
十、const 摆放:east const
采用 "east const" 风格,const 写在所限定类型的右侧:
Salt const& m_salt; // 正确
const Salt& m_salt; // Wrong
这与 .clang-format 中 QualifierAlignment: Right 的配置互为表里:clang-format 会把限定符统一推到右侧,手写时遵循该约定可避免格式化产生 diff。
十一、类型转换(Casts)
先思考能否不用转换。文档给出了几条替代思路:
- 整型常量可用
u、l、ul等后缀指定大小;单精度浮点常量用f后缀; - 小尺寸整型参与算术表达式时因隐式提升(integer promotion)容易出错,通常可以在局部变量中直接使用
int等大类型,最后再转换; - 准备
const_cast时,认真审视 API 的 const 正确性:正在写的成员函数是否真的应该是const; - 基类/派生类之间的转换,审视被调函数是否真的需要更一般的类型,还是更专化的类型即可。
确实需要转换时,禁止 C 风格转换——其行为复杂且在许多场景下不符合预期。应明确代码想要完成的转换类型,使用 static_cast、reinterpret_cast、bit_cast、dynamic_cast 等对应运算符。唯一例外:用 (void)parameter; 标记未使用的参数。
MyParentClass& object = get_object();
// Verify the type...
MyChildClass& casted = static_cast<MyChildClass&>(object);
// AK::Atomic::exchange()
alignas(T) u8 buffer[sizeof(T)];
T* ret = reinterpret_cast<T*>(buffer);
// SeekableStream::tell()
// Seek with 0 and SEEK_CUR does not modify anything despite the const_cast,
// so it's safe to do this.
return const_cast<SeekableStream*>(this)->seek(0, SeekMode::FromCurrentPosition);
size_t mask_length = (size_t)((u8)-1) + 1; // Wrong:应为 static_cast
return (u8 const*)string.characters_without_null_termination(); // Wrong:应为 reinterpret_cast
十二、花括号省略规则
if/else/for/while 等语句块只有在语句体只有一行时才可省略花括号;且同一 if/else 链中只要有一个分支按此规则需要花括号,则所有分支都需要:
if (condition)
foo();
if (condition) {
foo();
bar();
}
if (condition) {
foo();
} else if (condition) {
bar();
baz();
} else {
qux();
}
for (size_t i = i; condition; ++i) {
if (other_condition)
foo();
}
if (condition) {
foo();
} // OK:单行语句也允许保留花括号
错误示例:
if (condition)
// There is a comment here.
foo(); // Wrong:中间有注释,需要花括号
if (condition)
foo();
else {
bar();
baz();
} else
qux(); // Wrong:else 分支需要花括号时,if 分支也要
for (size_t i = i; condition; ++i)
if (other_condition)
foo(); // Wrong:for 体不是单条语句
十二(附)、规范符合性与落地检查清单
把以上条款收敛成一份可操作的自检清单,提交代码前可逐项核对:
- 格式:使用 clang-format 21 并带仓库根目录的 .clang-format 运行(
clang-format -style=file -i <file>),或直接使用仓库脚本 Meta/Linters/lint_clang_format.py; - 命名:类/命名空间 CamelCase、函数/变量 snake_case、常量 SCREAMING_CASE;成员
m_/s_/g_前缀且 private; - 访问器:setter 用
set_x,getter 用裸词x(),出参 getter 用get_x,惰性初始化 getter 用ensure_x且返回引用; - 类型:
unsigned不带int、int而非signed; - 注释:
//、完整句子、120 列换行、FIXME:/TODO:不带署名、spec 笔记NOTE:原文保留、自有笔记NB:、spec 疑问[[ ... ]]; - OOP:虚方法重写
virtual+override/final双关键字;单例the(); - const 与转换:east const;禁用 C 风格转换,
const_cast前重新审视 API; - 控制流:仅单行语句可省花括号,且 if/else 链统一。
这套规范的工程价值在于"文档 + 配置 + 脚本"三者对齐:Documentation/CodingStyle.md 规定语义层面的命名与表达约定,.clang-format 把排版层面的约定固化为可机器执行的格式(且版本由 lint_clang_format.py 锁定为 21),.clangd 则保证 IDE 端的头文件包含风格与编译数据库行为一致。新贡献者在本地按上述流程完成格式化与核对后,代码即可与整个 Ladybird 代码库保持风格统一。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00