☰
Skia C++ 编码风格规范详解:命名约定、类设计模式与 clang-format 自动化落地
2026/9/25 7:57:45 网站建设 项目流程
  • 图形学
  • 图像处理

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images.

项目地址:https://gitcode.com/gh_mirrors/skia1/skia
点击查看免费下载

本文基于 Skia 官方贡献文档 Coding Style Guidelines,系统梳理 Skia 代码库的 C++ 编码风格约定:从文件组织、命名规则、宏与大括号风格,到类声明、onMethodName虚函数模式与参数传递惯例,并逐条对照仓库根目录 .clang-format 中的机器可执行配置,说明这些约定如何被格式化工具自动强制,帮助贡献者在提交补丁前一次性对齐项目风格。

约定沿革与适用范围

原文档开头说明:这些约定是随项目演进而逐渐成型的,早期代码并不严格遵循全部规则,但随代码演进,期望存量代码逐步向规范靠拢。因此阅读代码时遇到风格不一致的旧代码属于正常现象,新增与修改的代码应以现行规范为准。

这一规范适用于 C++ 源码与头文件,Python 工具脚本则单独遵循 Google Python Style Guide(见文末说明)。对于补丁提交流程本身(CLA、AUTHORS 文件登记、提交前自查等前置要求),可参考 Contributing to Skia 一文。

文件组织与头文件约定

扩展名与目录布局

  • C++ 源文件与头文件统一使用.cpp和.h扩展名;
  • 不对外暴露的私有头文件应放在src目录下,使其不进入客户端(client)的头文件搜索路径;
  • 若私有头文件需要被公开头文件包含,则应放入 include/private 目录。

最小化 include 与字母序排列

规范强调“尽可能减少 include”:如果头文件中只需前向声明(forward declare)某个名称就足够,那么前向声明优先于完整 include。前向声明与文件 include 均应按字母顺序排列。

仓库根目录的 .clang-format 通过以下配置从机器层面落实了字母序要求(见第 100 行附近):

SortIncludes: true IncludeCategories: - Regex: '^<.*\.h>' Priority: 1 - Regex: '^<.*' Priority: 2 - Regex: '.*' Priority: 3 IncludeIsMainRegex: '(-_)?$'

即系统头<...>中的 C 头文件、其余系统头、项目内"..."头文件被分为三个优先级类别排序,且以test/unittest结尾的文件会优先匹配对应的同名头文件。

禁止在 SkTypes.h 之前使用条件编译

不要在包含"SkTypes.h"(直接或间接)之前使用#if/#ifdef。原因是:大多数你想做条件编译判断的宏,往往要等到SkTypes.h被处理后才真正确定。这是 Skia 特有的构建顺序约束——SkTypes.h是配置宏(平台、后端能力等)的最终裁决点。

空白与换行

  • 缩进使用 4 个空格,禁止 Tab;
  • 使用 Unix 风格换行符(LF);
  • 尽量不留行尾空白,但执行上不严格;
  • 行宽控制在 100 列以内,除非换行会“丑得离谱”(可自行判断)。

以上两条硬规则对应 .clang-format 的IndentWidth: 4、UseTab: Never、TabWidth: 4与ColumnLimit: 100。

命名规范

这是整份文档篇幅最大的部分,规则可以归纳为一张“前缀表”:

对象命名规则示例
对外可见类型/函数Sk前缀(GPU 后端 Ganesh 代码用Gr前缀)SkCanvas、GrContext
嵌套类型无需前缀HelperClass
含方法的类/结构体/联合体数据成员小写f开头 + 驼峰fMilesDriven
纯数据访问类型可不加f前缀milesDriven
全局变量小写g开头 + 驼峰gLoggingEnabled
局部变量与参数小写开头驼峰numCats
constexpr/const且值程序期内固定k开头 + 驼峰kPictureSize
枚举值k前缀;无作用域枚举追加_枚举名后缀kGlazed_DonutType
宏全大写下划线分隔;跨文件作用域的宏加SK或GR前缀SK_...、GR_GL_...
实现文件内 static 非类函数小写下划线分隔(snake_case)tastes_like_chicken
extern 函数 / static 类函数大写开头驼峰SkIsOdd

类前缀与嵌套类型

对外可见的类型和函数使用Sk前缀表明其属于 Skia;Ganesh(Skia 的 GPU 后端,源码主要位于src/gpu目录,Gr前缀符号集中于此)中的代码使用Gr前缀。嵌套类型无需前缀:

class SkClass { public: class HelperClass { ... }; };

数据成员:f 前缀

含方法的 struct/class/union 中的数据字段以小写f开头再接驼峰命名,用以和其他变量区分;而主要面向“直接字段访问”的类型则不需要f装饰:

struct GrCar { float milesDriven; Color color; }; class GrMotorcyle { public: float getMilesDriven() const { return fMilesDriven; } void setMilesDriven(float milesDriven) { fMilesDriven = milesDriven; } Color getColor() const { return fColor; } private: float fMilesDriven; Color fColor; };

注意对照:GrCar是纯数据载体,字段不加f;GrMotorcyle有访问器方法,字段加f。

全局变量与局部变量

全局变量同样遵循驼峰约定,仅前缀改为g:

bool gLoggingEnabled;

仓库中可见真实用例:tools/flags/CommonFlagsConfig.cpp 中的文件作用域静态数组即gPredefinedConfigs,与上述g前缀约定一致。

局部变量与函数参数为小写开头驼峰:

int herdCats(const Array& cats) { int numCats = cats.count(); }

常量:k 前缀

声明为constexpr或const、且取值在程序运行期间固定的变量,以k开头再接驼峰:

int drawPicture() { constexpr SkISize kPictureSize = {100, 100}; constexpr float kZoom = 1.0f; }

枚举命名

枚举值同样以k为前缀,具体形态分四种情况:

1.enum class(作用域枚举)——值无需后缀:

// Enum class does not need suffixes. enum class SkPancakeType { kBlueberry, kPlain, kChocolateChip, };

2. 无作用域枚举(独占值)——k值_枚举名后缀,枚举名用单数:

// Enum should have a suffix after the enum name. enum SkDonutType { kGlazed_DonutType, kSprinkles_DonutType, kChocolate_DonutType, kMaple_DonutType, kLast_DonutType = kMaple_DonutType }; static const SkDonutType kDonutTypeCount = kLast_DonutType + 1;

规则细节:枚举本体对“独占值”用单数名、对“位域”用复数名;枚举数量若需要,命名为k<单数枚举名>Count且不作为枚举成员(如上例的kDonutTypeCount),或者在枚举内保留一个kLast成员也可以。

3. 无作用域位域枚举——值以Bit结尾:

enum SkSausageIngredientBits { kFennel_SausageIngredientBit = 0x1, kBeef_SausageIngredientBit = 0x2 };

4. 标志位枚举(Flags)——值以Flag结尾:

enum SkMatrixFlags { kTranslate_MatrixFlag = 0x1, kRotate_MatrixFlag = 0x2 };

函数命名

实现文件内的 static 非类函数用小写下划线分隔:

static inline bool tastes_like_chicken(Food food) { return kIceCream_Food != food; }

extern 函数与 static 类函数用大写开头驼峰:

bool SkIsOdd(int n); class SkFoo { public: static int FooInstanceCount(); // Not static. int barBaz(); };

宏命名

宏一律全大写下划线分隔;作用域大于文件的宏应以SK或GR开头:

#define GR_GL_TEXTURE0 0xdeadbeef

Ganesh 中专门与 GL 相关的宏前缀为GR_GL。另外 Ganesh 倾向让宏始终有定义,用#if MACRO而非#ifdef MACRO:

#define GR_GO_SLOWER 0 ... #if GR_GO_SLOWER Sleep(1000); #endif

Skia 其余部分对布尔标志则惯用#ifdef SK_MACRO风格。两种风格的区别在于:#if风格允许把开关值集中定义在一处并可取非,而#ifdef风格下宏“存在与否”本身就是开关。

大括号与流程控制

大括号位置

开括号不换行;else/else if与前后大括号同行,除非有预处理条件编译介入;if、else、while、for、do后必须使用大括号,即使只有一行:

if (...) { oneOrManyLines; } if (...) { oneOrManyLines; } else if (...) { oneOrManyLines; } else { oneOrManyLines; } for (...) { oneOrManyLines; } void function(...) { oneOrManyLines; } // 预处理条件编译打断 else 时的写法 if (!error) { proceed_as_usual(); } #if HANDLE_ERROR else { freak_out(); } #endif

这与 .clang-format 中BreakBeforeBraces: Custom且所有BraceWrapping.*: false的设置完全一致(Allman/K&R 之外的定制风格:类、函数、控制语句之后均不换行开括号)。

控制关键字留白

流程控制关键字与左括号之间、括号与大括号之间都要留空格:

while (...) { } do { } while (...); switch (...) { ... }

对应配置为SpaceBeforeParens: ControlStatements。

switch/case:缩进、fallthrough 与 case 内块

case与default相对switch缩进一级:

switch (color) { case kBlue: ... break; case kGreen: ... break; ... default: ... break; }

跨 case 的隐式贯穿必须用[[fallthrough]]标注;但连续多个空 case 标签共享同一实现时不需要标注:

switch (recipe) { ... case kSmallCheesePizza_Recipe: case kLargeCheesePizza_Recipe: ingredients |= kCheese_Ingredient | kDough_Ingredient | kSauce_Ingredient; break; case kCheeseOmelette_Recipe: ingredients |= kCheese_Ingredient; [[fallthrough]] case kPlainOmelette_Recipe: ingredients |= (kEgg_Ingredient | kMilk_Ingredient); break; ... }

这一约定在核心代码中真实存在,例如 src/core/SkBlurEngine.cpp:

case 2: [[fallthrough]]; case 3: [[fallthrough]];

以及 src/core/SkBitmapProcState_matrixProcs.cpp 中的多 case 连续贯穿写法。

当某个 case 需要声明局部变量时,用花括号开块、} break;收尾:

switch (filter) { ... case kGaussian_Filter: { Bitmap srcCopy = src->makeCopy(); ... } break; ... };

case缩进由 .clang-format 的IndentCaseLabels: true强制(注意:Google 基础风格默认不缩进 case,这里做了覆盖)。

类声明规范

可见性排序与成员分组

除非有前向声明需要,类声明中的可见性区应按public、protected、private顺序排列,每个可见性标签前留一个空行;同一可见性区内,数据字段与方法不要交叉混排,建议把所有数据字段集中放在区段末尾:

class SkFoo { public: ... protected: ... private: void barHelper(...); ... SkBar fBar; ... };

对应配置AccessModifierOffset: -4:public:等标签向左缩进 4 列(顶格)。

override 与父类方法限定

被派生类重写的虚函数应使用override关键字并省略virtual:

void myVirtual() override { }

当调用父类版本的同名方法需要明确体现时,用Parent::method();使用了作用域限定符时,不再需要this->:

class GrDillPickle : public GrPickle { ... bool onTasty() const override { return GrPickle::onTasty() && fFreshDill; } ... private: bool fFreshDill; };

构造函数初始化列表格式

初始化器若能在同一行放下,就放在构造函数同行;否则每个初始化器独占一行、缩进对齐,逗号放在下一行行首:

GrDillPickle::GrDillPickle() : GrPickle(), fSize(kDefaultPickleSize) {} GrDillPickle::GrDillPickle(float size, float crunchiness, const PickleOptions* options) : GrPickle(options) , fSize(size) , fCrunchiness(crunchiness) {}

这是 .clang-format 中三行配置组合出的效果:ConstructorInitializerAllOnOneLineOrOnePerLine: true(要么全在一行,要么一行一个)、ConstructorInitializerIndentWidth: 8(初始化器缩进 8 列)、BreakConstructorInitializersBeforeComma: true(逗号前置)。

explicit 单参构造函数

接受单一实参的构造函数几乎总是应声明为explicit,例外仅限极少数“自动兼容”类:

class Foo { explicit Foo(int x); // Good. Foo(float y); // Spooky implicit conversion from float to Foo. No no no! ... };

this-> 前缀

在方法内部调用本对象的方法时,应显式加this->:

this->method();

核心模式:公开非虚入口 + 私有 onMethodName 虚函数

Skia 中虚方法的一个标志性模式是:提供一个公开的非虚(或 final)入口方法,与之配对的是一个私有虚方法onMethodName。其设计目的是保证基类逻辑(前后置约束)一定被执行,由基类掌控虚方法的使用方式,而不是依赖每个子类自觉调用Parent::onMethodName():

class SkSandwich { public: void assemble() { // All sandwiches must have bread on the top and bottom. this->addIngredient(kBread_Ingredient); this->onAssemble(); this->addIngredient(kBread_Ingredient); } bool cook() { return this->onCook(); } private: // All sandwiches must implement onAssemble. virtual void onAssemble() = 0; // Sandwiches can remain uncooked by default. virtual bool onCook() { return true; } }; class SkGrilledCheese : public SkSandwich { private: void onAssemble() override { this->addIngredient(kCheese_Ingredient); } bool onCook() override { return this->toastOnGriddle(); } }; class SkPeanutButterAndJelly : public SkSandwich { private: void onAssemble() override { this->addIngredient(kPeanutButter_Ingredient); this->addIngredient(kGrapeJelly_Ingredient); } };

从源码结构看,这种“入口 + on 前缀钩子”的组合在 Skia 各类可扩展组件(绘制、后端接口)中是普遍的组织方式,读者在src/gpu下检索virtual.*on[A-Z]即可看到大量实例。

整数类型与函数参数

整数类型

Skia 对整数类型的取舍遵循 Google C++ Style Guide 中 “Integer Types” 一节(旧代码正在逐步改造对齐)。要点:

  • 默认使用int;只有当确需保证位宽时,才使用stdint.h中的定宽类型(int32_t等);
  • 对“计数不为负”等语义,用断言(SK_ASSERT等)而不是unsigned类型来表达;
  • 位域一律用uint32_t,除非出于打包或性能原因必须更短。

参数传递惯例

  • 必须存在的常量对象参数:const 引用传递;
  • 可选的常量对象参数:const 指针传递;
  • 会被修改的对象参数:非 const 指针传递;
  • 非常引用传参极少使用。
// src and paint are optional void SkCanvas::drawBitmapRect(const SkBitmap& bitmap, const SkIRect* src, const SkRect& dst, const SkPaint* paint = nullptr); // metrics is mutable (it is changed by the method) SkScalar SkPaint::getFontMetrics(FontMetric* metrics, SkScalar scale) const;

可选参数用“指针 + 默认 nullptr”表达,可变参数用裸指针表达——这一惯例与 Google C++ 风格“optional 参数优先用指针”的建议一致。

长参数列表的换行

参数一行放不下时允许两种排版方式:

方式一——溢出参数与首参对齐:

void drawBitmapRect(const SkBitmap& bitmap, const SkRect& dst, const SkPaint* paint = nullptr) { this->drawBitmapRectToRect(bitmap, nullptr, dst, paint, kNone_DrawBitmapRectFlag); }

方式二——全部参数换行、缩进 8 个空格:

void drawBitmapRect( const SkBitmap& bitmap, const SkRect& dst, const SkPaint* paint = nullptr) { this->drawBitmapRectToRect( bitmap, nullptr, dst, paint, kNone_DrawBitmapRectFlag); }

8 空格缩进对应配置ContinuationIndentWidth: 8与AllowAllParametersOfDeclarationOnNextLine: true;两种排版都合规,体现了规范“100 列以内、自行判断”的弹性。

用 clang-format 自动落实风格

上述大部分排版规则已由仓库根目录的 .clang-format 机器化。该文件同时包含 Cpp 与 ObjC 两个配置段(文件头注释特别提醒:修改 Cpp 段时必须同步修改 ObjC 段)。关键配置与文档规则的映射如下:

文档规则.clang-format 配置
行宽 100 列ColumnLimit: 100
4 空格缩进、禁用 TabIndentWidth: 4、TabWidth: 4、UseTab: Never
开括号不换行、else同行BreakBeforeBraces: Custom+ 全部BraceWrapping.*: false
case 相对 switch 缩进IndentCaseLabels: true
初始化列表“一行一个且逗号前置、缩进 8 列”ConstructorInitializerAllOnOneLineOrOnePerLine: true、ConstructorInitializerIndentWidth: 8、BreakConstructorInitializersBeforeComma: true
控制关键字后留空格SpaceBeforeParens: ControlStatements
include 字母序SortIncludes: true+IncludeCategories
短函数/if/循环可单行AllowShortFunctionsOnASingleLine: All、AllowShortIfStatementsOnASingleLine: true、AllowShortLoopsOnASingleLine: true
指针左对齐(SkBitmap *b风格)PointerAlignment: Left

配置文件头部注释给出了标准工作流:

# Typical usage is to apply this to the lines you've modified in a local # change. Make sure to install git-clang-format [1] by adding it to your # path and make it executable. # # Stage your changes with "git add" and then run: # $ git clang-format # You can optionally use the "--" file filter to restrict formatting to certain # files or directories. The tool will display the list of files that were # modified. These have been modified without being staged. You can review the # modifications using "git diff".

即:git add暂存改动后运行git clang-format,工具只格式化你改动过的行(未暂存回文件),再用git diff复核。注意适用前提:由于部分客户端仍在用较老版本的 clang-format,配置中刻意只保留了 clang-format 10 及更早版本支持的选项(注释中记录了 Xcode 自带 clang-format 10、bin/clang-format为 11、Homebrew 安装为 14 的对应关系),本地格式化工具版本过高反而可能产生与 CI 不一致的噪音。

Python 脚本

Skia 构建与工具链中包含大量 Python 脚本(如 gn 工具目录 下的构建辅助脚本)。规范对它们的约定简洁明确:Python 代码遵循 Google Python Style Guide。

小结

Skia 的编码风格是“人读文档 + 机读配置”双层保障:Coding Style Guidelines 给出语义层面的判断标准(f/k/g前缀体系、onMethodName模式、参数传递策略),.clang-format 则把缩进、括号、换行、include 排序等确定性规则交给git clang-format强制执行。对贡献者而言,掌握前者的判断规则、依赖后者处理机械排版,是提交补丁前对齐项目风格的最短路径。

  • 图形学
  • 图像处理

【免费下载链接】skia

Skia is a complete 2D graphic library for drawing Text, Geometries, and Images.

项目地址:https://gitcode.com/gh_mirrors/skia1/skia
点击查看免费下载

相关推荐

上一篇:Payload JWT 认证:如何签发带 role 的 JWT 并校验请求身份
下一篇:Android Sunflower深度链接终极指南:如何实现URL路由导航与Jetpack Compose集成

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询