首页
/ Ladybird 浏览器 C++ 编码风格详解:命名、注释、类型转换与 clang-format 实战指南

Ladybird 浏览器 C++ 编码风格详解:命名、注释、类型转换与 clang-format 实战指南

2026-09-04 11:15:16作者:仰钰奇

Ladybird 浏览器(一个独立开发的浏览器项目)对 C++ 代码有一套完整且强制执行的编码风格规范,本文以官方风格文档 Documentation/CodingStyle.md 为主体,完整覆盖其命名规则、成员前缀、访问器命名、构造初始化、单例模式、注释约定(含 Web 规范的 NOTE/NB 前缀机制)、const 摆放、类型转换、花括号省略等全部核心条款,并结合仓库根目录的 .clang-format 配置文件、Meta/Linters/lint_clang_format.py 校验脚本以及实际源码(如 AK/Vector.hLibraries/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-formatBasedOnStyle: 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" 一节给出两个方案(按推荐顺序):

  1. Debian/apt 系发行版:使用 LLVM 官方 apt 仓库安装最新发布的 clang-format;
  2. 从源码编译 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 中的 FDFiledescriptor);
  • 变量与函数: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: BeforeCommaBreakBeforeInheritanceComma: 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 关键字;子类重写时必须同时virtualoverride(或 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-formatQualifierAlignment: Right 的配置互为表里:clang-format 会把限定符统一推到右侧,手写时遵循该约定可避免格式化产生 diff。

十一、类型转换(Casts)

先思考能否不用转换。文档给出了几条替代思路:

  • 整型常量可用 ulul 等后缀指定大小;单精度浮点常量用 f 后缀;
  • 小尺寸整型参与算术表达式时因隐式提升(integer promotion)容易出错,通常可以在局部变量中直接使用 int 等大类型,最后再转换;
  • 准备 const_cast 时,认真审视 API 的 const 正确性:正在写的成员函数是否真的应该是 const
  • 基类/派生类之间的转换,审视被调函数是否真的需要更一般的类型,还是更专化的类型即可。

确实需要转换时,禁止 C 风格转换——其行为复杂且在许多场景下不符合预期。应明确代码想要完成的转换类型,使用 static_castreinterpret_castbit_castdynamic_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 体不是单条语句

十二(附)、规范符合性与落地检查清单

把以上条款收敛成一份可操作的自检清单,提交代码前可逐项核对:

  1. 格式:使用 clang-format 21 并带仓库根目录的 .clang-format 运行(clang-format -style=file -i <file>),或直接使用仓库脚本 Meta/Linters/lint_clang_format.py
  2. 命名:类/命名空间 CamelCase、函数/变量 snake_case、常量 SCREAMING_CASE;成员 m_/s_/g_ 前缀且 private;
  3. 访问器:setter 用 set_x,getter 用裸词 x(),出参 getter 用 get_x,惰性初始化 getter 用 ensure_x 且返回引用;
  4. 类型unsigned 不带 intint 而非 signed
  5. 注释//、完整句子、120 列换行、FIXME:/TODO: 不带署名、spec 笔记 NOTE: 原文保留、自有笔记 NB:、spec 疑问 [[ ... ]]
  6. OOP:虚方法重写 virtual + override/final 双关键字;单例 the()
  7. const 与转换:east const;禁用 C 风格转换,const_cast 前重新审视 API;
  8. 控制流:仅单行语句可省花括号,且 if/else 链统一。

这套规范的工程价值在于"文档 + 配置 + 脚本"三者对齐:Documentation/CodingStyle.md 规定语义层面的命名与表达约定,.clang-format 把排版层面的约定固化为可机器执行的格式(且版本由 lint_clang_format.py 锁定为 21),.clangd 则保证 IDE 端的头文件包含风格与编译数据库行为一致。新贡献者在本地按上述流程完成格式化与核对后,代码即可与整个 Ladybird 代码库保持风格统一。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341