Table of contents
Open Table of contents
TL;DR
C++ 没有统一的命名规范,主流风格(STL/Google/LLVM/Qt/Unreal)各自为政——这是 C++ 40 年多时代叠加的历史遗留问题。但”没有统一标准”不等于”怎么写都行”:C++ 标准对保留标识符有硬性规定,违反就是 UB;宏必须全大写也是跨风格的唯一共识。实操铁律是:进入已有 codebase 严格 follow,新建项目选一个成熟 style guide 一以贯之,并用 clang-format 强制执行——风格选择的质量远不如风格一致性的质量重要。
1. 为什么 C++ 不像 Go/Rust 那样统一?
这是理解整个 C++ 命名生态的前提。
1.1 其他现代语言怎么做到统一的
- Go:
gofmt随语言一起发布,Pike/Thompson 直接把CamelCase(大写开头=export,小写开头=private)写进语言语义,命名风格和可见性绑定,没有选择空间 - Rust:
rustfmt随cargo发布,rustc编译时对命名风格发 warning(non_snake_case、non_camel_case_types),不改 warning 不让过 CI - Python:PEP 8 是社区半官方标准,
black/ruff强制执行,虽然不如 Go/Rust 刚性但社区共识强 - Java:Sun 从 1995 年开始就有官方 code conventions,JDK 源码就是示范
1.2 C++ 的历史包袱
- 40 年跨越 4 个时代:C 血统(1979)→ 经典 OOP(1990s)→ STL + Boost(2000s)→ Modern C++(2011 至今),每个时代在 codebase 里留下不同的印记,老代码不能推倒重写
- ISO 标准委员会只管语言不管风格:标准里从不提”命名应该用什么 case”,因为委员会成员来自不同流派,任何规定都会得罪一半人
- 没有官方 formatter:
clang-format2013 年才出现,此时各大组织的风格已经固化了 10+ 年 - 模板和运算符重载让自动化风格检查更难:Go/Rust 的静态 AST 工具可以精确检查命名,C++ 的模板实例化让很多检查要到编译期才能做
结果就是:每个大组织/大项目搞自己的一套,还都能找到”合理”的理由——这不是一个可以靠投票解决的问题,而是一个可以靠 clang-format 逐项目解决的问题。
2. 主流风格全景对比
| 风格 | 类型 | 函数/方法 | 变量 | 成员变量 | 常量 | 命名空间 | 代表项目 |
|---|---|---|---|---|---|---|---|
| STL / Boost | snake_case | snake_case | snake_case | m_foo 或 foo_ | snake_case | snake_case | std::、boost:: |
PascalCase | PascalCase(方法) / snake_case(访问器) | snake_case | foo_ | kMaxSize | snake_case | Chromium、TensorFlow、gRPC、Protobuf | |
| LLVM | PascalCase | camelCase | PascalCase | 无前缀 | PascalCase | snake_case/lowercase | LLVM、Clang、Swift 编译器 |
| Qt | QPascalCase | camelCase | camelCase | m_member | — | — | Qt、KDE |
| Unreal | FFoo(struct)/AFoo(actor)/UFoo(UObject)/IFoo(interface) | PascalCase | PascalCase | PascalCase | — | — | UE 游戏代码 |
| Microsoft (现代) | PascalCase | PascalCase | camelCase | m_foo | PascalCase | PascalCase | Windows SDK、.NET/C++ |
读法:横向对比同一项(比如”变量”)在不同风格下的写法,纵向对比同一风格的整体协调性。没有哪一种是”对的”,但某些组合在视觉上更协调。
3. 各风格的历史源流(理解”为什么”)
3.1 STL / Boost:snake_case 的思想源自 C 和 Unix
Stepanov 1994 年为 HP 设计 STL 时刻意选择 snake_case:
- 与 C 标准库一致(
strcpy、malloc、memcmp),让 C 程序员无缝迁移 - Stepanov 本人的数学背景:他把 STL 视为”通用数据结构和算法的数学化描述”,
push_back这种动词短语读起来像数学公式 - 1994 年 ISO 标准化时被全盘接受,从此
std::的所有东西都是 snake_case
Boost 1998 年成立时明确声明”模仿 STL 风格”,为后来被标准吸收做准备(std::shared_ptr、std::thread、std::filesystem 全部来自 Boost)。
推论:如果你写的库打算进入 std:: 或跟标准库配合使用,应该用 snake_case——这是”礼貌”。
3.2 Google:PascalCase 来自 Java 背景
Google 早期 C++ style guide(2008 年前后公开)的作者很多来自 Java 背景,自然带入了 Java 的 PascalCase 方法名:
GetUserName()比get_user_name()符合 Java 程序员肌肉记忆- 但变量保留 snake_case——因为 Google 内部代码大量与 C 库交互
kConstantName来自 Google 的早期教学材料,据说是 Rob Pike 在 Google 时引入的(Go 里也能看到类似痕迹)
getter/setter 走 snake_case 的特例是 Google 后来(~2010 年)为了”让 property 访问看起来像字段访问”而引入的——争议很大,很多新人反复踩坑。
推论:Google 风格适合业务代码密集、团队规模大、需要明确视觉区分类型/函数/变量的场景。
3.3 LLVM:极简主义的 PascalCase 变量
Chris Lattner 2003 年启动 LLVM 时的选择:
- 函数用
camelCase(getName()、emitCode()),来自 Objective-C/Cocoa 传统(Lattner 是苹果人) - 变量用
PascalCase(Workers、Size)——这是 LLVM 的独特选择,理由是”一个字面 token 就能区分变量和关键字” - 极端节省视觉噪声:没有
m_、没有尾部_、没有k前缀,靠大小写承担一切信息
代价:Foo bar = Foo(); 这种语句视觉上类型和变量都是首字母大写,初读容易混淆——但 LLVM 社区觉得这是”熟练度问题,不是风格问题”。
推论:LLVM 风格适合代码量大、人均熟练度高、愿意靠工具(clang-tidy)辅助阅读的场景。
3.4 Qt:受 Java/Smalltalk 影响的 camelCase
Qt 1995 年诞生时,Trolltech 团队受 Smalltalk 和 Java 影响:
QString、QWidget的Q前缀是 namespace 替代品——1995 年 C++ 还没标准化namespace关键字- 所有方法
camelCase,setName()、mapToParent() - signals/slots 机制 让方法命名带有”事件驱动”色彩,
textChanged、clicked
历史遗产:即使今天 Qt 有 QtCore namespace,Q 前缀仍然保留——因为改名会破坏整个 KDE 生态。
3.5 Unreal Engine:匈牙利命名法的现代变种
Epic 的 UE 代码(Unreal Script 时代继承下来):
FVector(F = struct)、AActor(A = actor)、UObject(U = UObject 基类)、IInterface(I = interface)、EEnum(E = enum)、TArray<T>(T = template)- 前缀承担类型分类,一眼看出这是值类型/引用类型/UE 托管对象
- 变量也是
PascalCase——和 LLVM 类似但不加前缀
推论:这是游戏引擎的特殊需求——UE 的反射系统、GC、蓝图都依赖前缀做编译期/运行期类型判别。不要在普通 C++ 项目学这个。
3.6 Microsoft:匈牙利命名法的退场
1990 年代 Microsoft 文档里充斥着 szName(sz = string, zero-terminated)、lpszBuffer(lp = long pointer)、hWnd(h = handle)这种系统匈牙利命名法。
2000 年后 Microsoft 逐渐抛弃这套,现代 Windows/.NET C++ 代码用 PascalCase 类型 + camelCase 变量,只有在维护老 Win32 API 时还能看到匈牙利命名。
推论:看到 szName、lpBuffer 就知道是老代码,不要在新代码写这种。
4. 典型风格对比同一段代码
4.1 STL 风格(snake_case 一路到底)
namespace my_lib {
class thread_pool {
public:
using task_type = std::function<void()>;
explicit thread_pool(size_t num_workers);
~thread_pool();
void submit_task(task_type task);
size_t active_workers() const noexcept;
bool is_stopped() const noexcept { return stopped_; }
private:
std::vector<std::thread> workers_;
std::queue<task_type> pending_tasks_;
std::atomic<bool> stopped_;
mutable std::mutex queue_mutex_;
};
} // namespace my_lib
特点:
- 与
std::vector、std::thread、std::function视觉完全一致 using task_type也走 snake_case(和 STL 的value_type、size_type对齐)- 成员变量用尾部下划线(Boost 广泛使用这种形式)
Why:阅读这段代码的人正在 include <vector> 和 <thread>,视觉连贯性降低认知负载。
4.2 Google 风格
namespace my_lib {
class ThreadPool {
public:
using TaskType = std::function<void()>; // 类型别名:PascalCase
explicit ThreadPool(size_t num_workers);
~ThreadPool();
void SubmitTask(TaskType task); // 方法:PascalCase
size_t active_workers() const { return active_workers_; } // 访问器例外:snake_case
bool is_stopped() const { return stopped_; } // 布尔访问器也用 snake_case
static constexpr int kMaxWorkers = 1024; // 常量:k 前缀
private:
std::vector<std::thread> workers_; // 成员:尾部下划线
std::atomic<int> active_workers_;
std::atomic<bool> stopped_;
};
} // namespace my_lib
特点:
- 方法
SubmitTask和访问器active_workers()两种风格混用 - 类型别名
TaskType走 PascalCase(和类一致) kMaxWorkers的k前缀是 Google 特色
Why:类型/方法都是 PascalCase 形成”主动操作”视觉,访问器用 snake_case 形成”被动读取字段”视觉——理论上让读者一眼区分”这是操作”vs”这是取值”。
4.3 LLVM 风格
namespace my_lib {
class ThreadPool {
public:
using TaskType = std::function<void()>;
explicit ThreadPool(size_t NumWorkers); // 参数也是 PascalCase
~ThreadPool();
void submitTask(TaskType Task); // 方法:camelCase
size_t getActiveWorkers() const;
bool isStopped() const { return Stopped; }
static constexpr int MaxWorkers = 1024; // 常量也是 PascalCase,无前缀
private:
std::vector<std::thread> Workers; // 变量:PascalCase,无前缀
std::atomic<int> ActiveWorkers;
std::atomic<bool> Stopped;
};
} // namespace my_lib
特点:
- 变量和类型都是 PascalCase,靠上下文区分
- 没有任何前后缀装饰
submitTask(Task)这种”方法 camelCase + 参数 PascalCase”的组合是 LLVM 独有的视觉节奏
Why:极端节省视觉噪声,相信程序员的熟练度——“看代码不需要靠前缀辅助”。
4.4 Qt 风格
namespace my_lib {
class ThreadPool : public QObject {
Q_OBJECT
public:
explicit ThreadPool(int numWorkers, QObject *parent = nullptr);
~ThreadPool() override;
void submitTask(std::function<void()> task);
int activeWorkers() const;
bool isStopped() const { return m_stopped; }
signals:
void taskCompleted(int taskId); // signal:动词过去式
void workerStarted(int workerId);
private slots:
void onWorkerFinished(); // slot:on + 事件
private:
QVector<QThread*> m_workers; // 成员:m_ 前缀
QAtomicInt m_activeWorkers;
bool m_stopped;
};
} // namespace my_lib
特点:
m_前缀的成员变量(对比 Google 的尾部_)- signal 用过去式(
taskCompleted表示”事件已发生”) - slot 用
on前缀表示”响应”
5. 深入专题:C++ 特有的命名维度
5.1 命名空间
// STL 风格:全小写 + snake_case(标准库做法)
namespace std::chrono { }
namespace std::filesystem { }
// Google/大部分项目:全小写 + snake_case
namespace my_company::backend::rpc { }
// LLVM:全小写(但有时嵌套深时用单词缩写)
namespace llvm::cl { } // cl = command line
namespace llvm::sys { } // sys = system
// 反模式:namespace 用 PascalCase
namespace MyCompany::Backend { } // 极少见,不推荐
共识:命名空间几乎所有风格都推荐 snake_case,因为它们经常嵌套成 a::b::c,大小写混用会视觉混乱。
5.2 文件命名
文件名有三个维度:大小写、分隔符、扩展名。
| 风格 | 头文件 | 源文件 | 示例 |
|---|---|---|---|
| STL / Boost | .hpp 或无扩展 | .cpp | shared_ptr.hpp |
.h | .cc | thread_pool.h、thread_pool.cc | |
| LLVM | .h | .cpp | ThreadPool.h、ThreadPool.cpp |
| Microsoft / Qt | .h | .cpp | 类名一致 |
几个反模式:
- 大小写敏感陷阱:
FooBar.h在 Linux(区分大小写)和 Windows(不区分)行为不一致,跨平台项目容易踩雷——推荐全小写文件名 .hxx/.cxx扩展名是老 IBM/DEC 编译器遗留,现代项目别用- 只用
.h不区分 C 和 C++:Google 坚持用.h而不是.hpp,理由是”C 和 C++ 头文件混用很常见”,但这会让 IDE 语言检测出错
Google 风格的文件映射规则:ThreadPool 类 → thread_pool.h + thread_pool.cc,一一对应,方便工具查找。
5.3 enum 和 enum class
C++11 之前的 enum 污染外部作用域,所以老代码有大量前缀防撞:
// C++03:前缀是刚需
enum Color {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
};
// 必须 COLOR_RED 否则和全局的 RED 撞
// Windows API 的典型反模式(为了防撞而巨丑)
enum WINDOW_STYLE {
WS_BORDER = 0x00800000,
WS_CAPTION = 0x00C00000,
};
C++11 引入 enum class 后,枚举值有自己的作用域,前缀变成多余:
// C++11+:enum class 无需前缀
enum class Color {
Red, // 访问时 Color::Red,天然防撞
Green,
Blue,
};
// Google 风格坚持 k 前缀
enum class Color {
kRed,
kGreen,
kBlue,
};
// STL 风格用 snake_case
enum class memory_order {
relaxed,
consume,
acquire,
release,
acq_rel,
seq_cst,
};
建议:
- 新代码全部用
enum class,不要用 C++03 styleenum - 枚举值大小写跟随整体风格,不要因为”老 enum 习惯”而强行全大写
5.4 模板参数
STL 约定俗成的单字母大写:
template <typename T> // 单类型:T
template <typename T, typename U> // 两个:T, U
template <typename K, typename V> // K-V 对用 K, V
template <typename Iter> // 迭代器用 Iter 或 It
template <typename InputIt, typename OutputIt> // 有语义区分时写全
template <typename... Args> // 变参包用 Args(或 Ts)
template <std::size_t N> // 非类型模板参数用单大写字母
template <template <typename> class Alloc> // 模板模板参数
Google 风格更倾向描述性名字:
template <typename ValueType, typename HashFunction>
class HashMap { };
争议点:
- 单字母
T可读性低但节省视觉空间——STL 生态的绝对主流 - 描述性名字
ValueType可读性高但在嵌套模板里会爆炸 - 折中方案:简单模板用
T,有多个相关参数时用Key/Value/Hash/Allocator等 STL 约定俗成的命名
5.5 Concept(C++20)
C++20 引入 concept 后出现新的命名争议:
// STL 标准库选择:snake_case
template <typename T>
concept integral = std::is_integral_v<T>;
template <typename I>
concept input_iterator = requires(I i) { *i; ++i; };
// Google/LLVM 倾向 PascalCase
template <typename T>
concept Integral = std::is_integral_v<T>;
template <typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
标准库选择 snake_case 的理由:concept 在类型位置使用(Integral auto x),它”约束了类型”但本身”像一个类型谓词”,跟 is_integral_v 这种 type trait 性质接近。
实操建议:跟随你项目的类型命名风格——用 PascalCase 命名类型就用 PascalCase 命名 concept,用 snake_case 就用 snake_case。标准库的 snake_case 选择在 Google 风格项目里会显得突兀。
5.6 类型别名(using / typedef)
// STL:snake_case,常见后缀 _type、_t
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using iterator = /* ... */;
// 自定义后缀 _t 模仿 C 标准
using buffer_t = std::vector<uint8_t>;
// Google:PascalCase
using UserId = int64_t;
using CallbackFunc = std::function<void(int)>;
// LLVM:PascalCase
using SymbolMap = DenseMap<StringRef, Symbol*>;
注意:不要同时使用 _type 和 _t 两种后缀在同一个类里,这是常见的”风格漂移”问题。
5.7 布尔变量和函数
所有风格都推荐以动词/系动词开头,让读者读成一句话:
bool is_ready; // "是否就绪"
bool has_error; // "是否有错"
bool should_retry; // "是否应重试"
bool can_modify; // "能否修改"
bool was_cancelled; // "是否已取消"
// 反模式:名词形式的布尔
bool ready; // 不清楚是"准备好了"还是"准备好"这个动作
bool error; // 看起来像错误对象,不像布尔
bool cancel; // 动词,看起来像命令不像状态
Google 特别规定:不要用否定式布尔命名。
// 反模式:否定式
bool is_not_ready;
if (!is_not_ready) { } // 双重否定,读起来头疼
// 正确:用肯定式
bool is_ready;
if (!is_ready) { }
5.8 Getter / Setter 的三种流派
// ---- Java 风格(Google 方法层用这个)----
class User {
public:
const std::string& GetName() const { return name_; }
void SetName(const std::string& name) { name_ = name; }
private:
std::string name_;
};
// ---- 无前缀风格(Google 访问器 + STL 的做法)----
class User {
public:
const std::string& name() const { return name_; }
void set_name(const std::string& name) { name_ = name; }
// 注意:getter 无前缀,setter 有 set_ 前缀——不对称但是主流
private:
std::string name_;
};
// ---- LLVM / Modern C++ 风格 ----
class User {
public:
StringRef getName() const { return Name; }
void setName(StringRef N) { Name = N; }
private:
std::string Name;
};
现代共识:对小类型(int、bool、StringRef)用”无前缀 getter + set_ setter”最简洁。大型成员(返回 const T&)也用同样模式。只有强 Java 背景的团队保留 Get/Set 全套前缀。
5.9 缩写和全大写单词的处理
经典难题:HTTPServer 还是 HttpServer?IO 还是 Io?
// ---- 保留原始大小写("HTTP is an acronym so it's all caps")----
class HTTPServer { };
class XMLParser { };
int HTTPStatusCode();
class URLBuilder { };
// ---- 全部作为"一个单词"处理(Google 推荐)----
class HttpServer { };
class XmlParser { };
int HttpStatusCode();
class UrlBuilder { };
Google 的理由:HTTPServer 和 HTTPSServer 紧邻时视觉很难区分(HTTPSServer 中 HTTP、S、Server 的边界不明显),而 HttpsServer vs HttpServer 一眼可辨。
LLVM 不作强制要求,但 LLVM 源码里 JIT、IR、AST 都保留大写,只有更长的缩写(HttpClient)才小写。
推论:选一条严格执行——HTTPServer + JITCompiler 和 HttpServer + JitCompiler 都 OK,但不要混用 HTTPServer + JitCompiler。
5.10 接口/抽象类是否加 I 前缀
// ---- Microsoft/Unreal/Java 风格 ----
class IRenderer {
public:
virtual ~IRenderer() = default;
virtual void render() = 0;
};
class OpenGLRenderer : public IRenderer { };
// ---- 无前缀风格(Google、STL、Modern C++ 主流)----
class Renderer {
public:
virtual ~Renderer() = default;
virtual void Render() = 0;
};
class OpenGLRenderer : public Renderer { };
反对 I 前缀的理由(Google/C++ Core Guidelines 主张):
- 实现和接口应该可以互相替换(Liskov 原则),调用方不应该知道自己拿到的是接口还是具体类
I前缀把”是否是抽象类”暴露在 API 层面,违反封装- 匈牙利命名法已经被证明是反模式,
I前缀是它的残余
支持 I 前缀的理由(Microsoft/Unreal 主张):
- C++ 没有
interface关键字,靠纯虚类实现,I前缀是”这是接口不是类”的视觉提示 - 大型 codebase 里接口和实现常在不同文件,前缀让查找更快
推论:Modern C++ 主流去前缀;只有当你的项目是 COM/UE 生态的一部分时才加 I。
5.11 异常类命名
// STL 风格:错误类型后缀 _error(继承自 std::exception 体系)
class runtime_error;
class logic_error;
class out_of_range;
// 自定义遵循同样模式
class connection_error : public std::runtime_error { };
// Google/LLVM 风格:Exception 或 Error 后缀
class ParseError : public std::exception { };
class ConnectionException : public std::runtime_error { };
建议:跟随基类风格——如果继承 std::runtime_error(snake_case),自定义也用 snake_case + _error 后缀,让整个错误体系视觉一致。
5.12 物理单位的命名
C++ 没有 Rust/Haskell 的强类型 unit 系统,命名里写清单位是防止 bug 的重要手段:
// 反模式:单位不明
int timeout; // 毫秒?秒?微秒?
double distance; // 米?英尺?
int buffer_size; // 字节?KB?
// 推荐:单位作为后缀
int timeout_ms; // 明确毫秒
int timeout_us; // 微秒
double distance_m; // 米
int buffer_size_bytes;
int64_t file_size_kb;
C++20 <chrono> 的解决方案:用类型承载单位,彻底消除命名里写单位的需要:
using namespace std::chrono_literals;
std::chrono::milliseconds timeout = 500ms;
std::chrono::microseconds precise_delay = 100us;
// 类型系统强制你不能把 ms 当 us 用
6. 常见误区(Pitfalls)
6.1 保留标识符是硬性规则,不是风格问题「确定,来源:C++ 标准 [lex.name]/3」
C++ 标准规定以下名字保留给实现,用户代码定义它们是 UB(大多数编译器不会报错,但标准库头文件可能用到这些名字,触发诡异冲突或平台相关 bug):
| 模式 | 作用域 | 是否 UB | 例子 |
|---|---|---|---|
_Name(下划线 + 大写字母) | 任何作用域 | UB | _Foo、_X、_MyVar |
__name(任何位置含双下划线) | 任何作用域 | UB | my__name、__init、foo__bar |
_name(下划线 + 小写) | 全局命名空间 | UB | global 作用域的 _foo |
_name | class/函数作用域 | OK 但危险 | class 成员 _member 合法 |
// UB - 保留名字
int _MyVar = 0; // _Uppercase 永远保留
int __counter = 0; // 双下划线永远保留
namespace { int _foo; } // 在 global namespace 严格说 UB
#define __DEBUG 1 // 双下划线宏更危险,必然撞库
// OK
class Foo {
int _m; // class 作用域,_ + 小写,合法但有歧义风险
int m_; // 尾部下划线,安全
int m_m; // m_ 前缀,安全
};
int my_var_ = 0; // namespace 作用域,尾部下划线 OK
为什么这条规则如此重要:标准库实现者(libstdc++、libc++、MSVC STL)大量使用 _Name 和 __name 作为内部名字——你写 _Size 某天编译器升级就会撞名。这是定时炸弹级别的 bug,很难排查。
6.2 成员变量前缀:m_ vs 前导 _ vs 尾部 _
| 前缀方式 | 安全性 | 视觉强度 | 典型使用者 |
|---|---|---|---|
m_foo | 绝对安全 | 强 | Qt、Unreal、老派 C++ |
foo_(尾部) | 安全 | 中 | Google、Boost |
_foo(前导) | 有风险 | 中 | 新手常用,不推荐 |
无前缀(foo) | 安全 | 零 | LLVM |
前导下划线的陷阱:
class Foo {
int _size; // class 作用域,技术上合法
};
// 某天代码重构把 _size 提取成 helper 函数:
int _size() { /* ... */ } // 全局作用域,UB!
// 编译通过,行为未定义
建议:用 m_ 或尾部 _ 二选一,永远不要前导下划线。即使在 class 作用域技术上合法,跨文件重构时容易漂移到 UB 区域。
6.3 宏必须 UPPER_SNAKE_CASE——这是唯一近乎普世的规则
#define MAX_BUFFER_SIZE 4096 // 正确:视觉警告"这是宏"
#define maxBufferSize 4096 // 错误:看起来像变量
#define max_buffer_size 4096 // 错误:看起来像函数
// 灾难场景:宏名和标识符冲突
#define max 1024
#include <algorithm>
int x = std::max(a, b); // 展开成 std::1024(a, b),编译失败或更糟
Why:宏绕过名字查找、作用域、命名空间,全大写是唯一的视觉警告。违反这条就是给未来的自己埋雷。
附加规则:宏名应该带项目前缀防撞,尤其是 header-only 库:
// 反模式:通用名字
#define CHECK(x) if (!(x)) abort()
#define LOG(msg) std::cerr << msg
// 正确:带项目前缀
#define MYLIB_CHECK(x) if (!(x)) abort()
#define MYLIB_LOG(msg) std::cerr << msg
// header guard 也一样
#ifndef MYLIB_FOO_H_
#define MYLIB_FOO_H_
// ...
#endif // MYLIB_FOO_H_
6.4 常量的 k 前缀有争议
// Google 坚持
static constexpr int kMaxSize = 1024;
static constexpr double kPi = 3.14159;
// LLVM / Modern C++
static constexpr int MaxSize = 1024;
static constexpr double Pi = 3.14159;
// STL
inline constexpr int max_size = 1024;
反对 k 前缀的理由:
constexpr的类型系统已经标注”这是常量”,k前缀冗余- Google 内部文档承认这是”历史遗留,新项目可以不用”
- 和数学常量
kPi这种看起来很怪(Pi 本来就是常量,不需要提醒)
支持 k 前缀的理由:
- 在 codebase 里 grep
k[A-Z]能快速找到所有常量引用 - 局部变量和常量视觉区分强
推论:这是一条可以跳过的 Google 式教条,除非你的项目其他部分都是 Google 风格(保持一致性 > 个人偏好)。
6.5 匈牙利命名法已经死了(别学)
// 系统匈牙利命名(Windows API 遗产)
int iCount;
char* szName;
DWORD dwFlags;
LPVOID lpBuffer;
HWND hWnd;
// Apps 匈牙利命名(原始意图,较少见)
int cx; // count of x
int dx; // delta x
char* pszBuffer; // pointer to string, zero-terminated
为什么淘汰:
- 类型已经由类型系统承担,
iCount的i提供零新信息 - 重构时类型变了名字就不准(
int iCount改成size_t要改所有iCount→nCount) - 现代 IDE 鼠标悬停就能看到类型
- Windows API 的
DWORD/LPVOID本身就是 typedef 混乱的产物
唯一的例外:维护老 Win32 代码时跟随原有风格,不要在新代码学这个。
6.6 snake_case 和 PascalCase 在同一 class 里混用是最糟的选择
// 反模式:风格漂移
class ThreadPool {
public:
void submit_task(Task t); // STL 风格
void SubmitTask(Task t); // Google 风格
void submitTask(Task t); // LLVM 风格
// 三种风格同时存在,通常是不同人分别添加的
};
典型出现场景:
- 老代码是 STL 风格,新人按 Google 风格加方法
- 从 Qt 项目迁移到纯 C++,方法名保留 camelCase 但新代码用 snake_case
- 复制粘贴第三方库的示例代码,没统一风格
补救:在 CI 里加 clang-tidy 的 readability-identifier-naming 检查,强制全项目一致。
6.7 auto 让变量命名更重要
auto 隐藏类型,所以变量名必须承担更多语义:
// 反模式:auto + 无信息变量名
auto x = getData();
auto y = process(x);
auto z = format(y);
// 读者根本不知道 x/y/z 是什么类型、什么含义
// 正确:auto + 描述性名字
auto user_records = fetch_user_records(db);
auto filtered_records = filter_active(user_records);
auto json_output = to_json(filtered_records);
// 即使看不到类型也能推断用途
C++ Core Guidelines ES.11:使用 auto 时,命名必须补足类型隐藏带来的信息损失。
6.8 lambda 捕获命名
// 反模式:捕获名和外层同名,引发阴影
int count = 0;
auto lambda = [count]() {
int count = 10; // 编译警告:shadow outer capture
return count;
};
// 正确:需要修改时用不同名字
int count = 0;
auto lambda = [captured_count = count]() {
return captured_count * 2;
};
C++14 初始化捕获让捕获变量有了独立命名的空间,可以显式区分”外层变量”和”lambda 内变量”。
7. clang-format 实战
风格选择的 80% 问题可以靠 .clang-format 文件解决。以下是三种风格的起手模板。
7.1 Google 风格(最常用,起手推荐)
# .clang-format
BasedOnStyle: Google
IndentWidth: 2
ColumnLimit: 100
AccessModifierOffset: -1
DerivePointerAlignment: false
PointerAlignment: Left
AllowShortFunctionsOnASingleLine: Inline
IncludeBlocks: Regroup
7.2 LLVM 风格
BasedOnStyle: LLVM
IndentWidth: 2
ColumnLimit: 80
AlignConsecutiveDeclarations: false
PointerAlignment: Left
7.3 STL / Boost 风格(custom)
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 120
BreakBeforeBraces: Allman
AlignConsecutiveDeclarations: true
# 注意:clang-format 不能强制 snake_case 命名
# 命名检查需要配合 clang-tidy
7.4 clang-tidy 命名检查
clang-format 只管缩进和空白,命名风格要靠 clang-tidy:
# .clang-tidy
Checks: "readability-identifier-naming"
CheckOptions:
- key: readability-identifier-naming.ClassCase
value: CamelCase
- key: readability-identifier-naming.FunctionCase
value: CamelCase
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.PrivateMemberSuffix
value: "_"
- key: readability-identifier-naming.ConstantCase
value: CamelCase
- key: readability-identifier-naming.ConstantPrefix
value: "k"
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
- key: readability-identifier-naming.EnumConstantCase
value: CamelCase
- key: readability-identifier-naming.EnumConstantPrefix
value: "k"
集成到 CI:
# pre-commit hook
clang-format -i --style=file $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cc|cpp|h|hpp)$')
clang-tidy --config-file=.clang-tidy $(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(cc|cpp)$')
关键洞察:一旦 .clang-format + .clang-tidy 就位,风格选择就不再是持续消耗认知的事情——工具保证一致性,人类只在最初选一次。
8. 决策框架:新项目怎么选?
是否有大量 STL/Boost 集成或打算贡献给标准?
├── 是 → STL 风格(snake_case),和 std:: 无缝
└── 否
├── 是否是编译器/语言基础设施(LLVM 生态)?
│ ├── 是 → LLVM 风格
│ └── 否
│ ├── 是否是游戏引擎(UE 生态)?
│ │ ├── 是 → Unreal 风格(F/A/U 前缀)
│ │ └── 否
│ │ ├── 是否是 Qt/KDE 应用?
│ │ │ ├── 是 → Qt 风格
│ │ │ └── 否 → Google 风格(默认推荐)
为什么 Google 风格是”默认”:
- 文档最全面、最细致
clang-format/clang-tidy开箱即用(BasedOnStyle: Google)- 生态最大(Chromium、TensorFlow、gRPC、Abseil 都是这个风格)
- 覆盖场景最全,从嵌入式到服务器应用都有参考
- 新人学习资料多
Google 风格的缺点:
kConstantName的k前缀有争议- getter/setter 的 snake_case 特例让人困惑
- 方法名 PascalCase 和 STL 视觉不一致
如果预算允许只关心一件事:在项目第一天就写好 .clang-format,并在 CI 里强制执行——风格本身的选择远不如”有没有强制一致性”重要。
9. C++ Core Guidelines 的底线
Stroustrup & Sutter 的 C++ Core Guidelines NL 章节(Naming and Layout)是”最低共识”,任何风格都应该满足:
- NL.1:用注释说明意图,命名承担不了的部分
- NL.4:保持命名空间使用一致
- NL.5:避免在名字里编码类型信息(反匈牙利)
- NL.7:名字长度应该大致正比于作用域(局部用短名、全局用长名)
- NL.8:保持一致的命名风格
- NL.9:宏用
ALL_CAPS - NL.10:避免保留标识符
- NL.11:使字面量可读(
1'000'000而非1000000) - NL.16:按”public-protected-private”顺序声明成员
NL.7 的”作用域比例”原则特别值得一提:
for (int i = 0; i < n; ++i) { } // 局部循环变量 i 合理
int i = globalInterfaceCounter; // 全局作用域用 i 不合理
// 作用域越大,名字越描述性
class Foo {
int total_user_count_; // 类成员作用域
void update(int n) { // 函数参数作用域小,短名可以
for (int i = 0; i < n; ++i) { }
}
};
10. 实操总结
- 进入已有项目:严格跟随项目风格,不要混搭。混搭是最糟的选择——比任何一种风格都难读
- 新项目选型:按第 8 节决策框架走,默认选 Google
- 必装
.clang-format+.clang-tidy:写进仓库根目录,pre-commit hook 自动 format + 检查命名 - 硬规则不能违反:保留标识符(6.1)、宏全大写(6.3)、不要匈牙利命名(6.5)——这三条跨风格通用
- 命名承担语义:
auto时代变量名要承担类型隐藏带来的信息损失 - 风格一致性 > 风格选择:选哪种不重要,全项目一致才重要
一句话总结:C++ 命名的真正最佳实践不是”选对风格”,而是”选一种并坚持到底,用工具替代人类的意志力”。
信息来源
- C++ 标准
[lex.name]/3——保留标识符规则(确定) - C++ Core Guidelines NL 章节:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-naming (需自行验证最新版)
- Google C++ Style Guide:https://google.github.io/styleguide/cppguide.html
- LLVM Coding Standards:https://llvm.org/docs/CodingStandards.html
- Boost Library Requirements:https://www.boost.org/development/requirements.html
- Qt Coding Style:https://wiki.qt.io/Qt_Coding_Style
- Unreal Engine Coding Standard:https://docs.unrealengine.com/en-US/epic-cplusplus-coding-standard-for-unreal-engine/
关联概念
- 匈牙利命名法历史:Charles Simonyi 1972 年在 Xerox PARC 提出,原意是”Apps Hungarian”(编码含义,如
rwPosition= “row position”),被 Microsoft 误读成”Systems Hungarian”(编码类型)后发扬光大并最终被淘汰 - Stepanov 和 STL 的数学背景:
push_back、begin、end等命名的数学化美感 clang-format/clang-tidy:工具替代共识的现代范式- ADL (Argument-Dependent Lookup) 和命名空间:命名空间选择会影响 ADL 行为,不只是命名问题