lil_tea c++ 2026 style guide
本文使用 macbook pro m4 + vscode 编写.
聚了散散了又聚
这一路我绝对没想到能走到现在这一步
那些本不该来的已经到站下车
你我注定这一趟车赶不上一拨
这是我在开发和算竞都使用的代码风格. 2023 版本.
部分借鉴自 the cherno, 部分借鉴自 google c++ style guide, 部分借鉴自 linux kernel coding style, 部分借鉴自 算法竞赛进阶指南.
因为我年纪大了所以已经忘记哪一条是从哪借鉴的了, 朋友们看到哪条觉得合理就用吧, 不合理的不用就好了.
总结一句话: 坚持 c like 特色指针主义道路, 以安全性换自由; 姓 bjarne 还是姓 graydon 的问题上我们要坚持 bjarne 领导, 坚持 k&r 的文化自信, 杜绝 graydon 的糖衣炮弹.
调试
最重要的当然是打印函数:- void log(long line_num) {
- #ifdef lil_tea
- std::println(std::cerr, "line: {} | hey siri, play <hit`em up> please", line_num);
- #endif
- }
复制代码 但是向量加法函数可以有新的办法了:- std::vector<long> add(const std::vector<long> &a, const std::vector<long> &b)
- pre(a.size() == b.size()) { // c++ 2026 新特性, 见 [contracts](https://en.cppreference.com/w/cpp/language/contracts.html)
- std::vector<long> c(a.size());
- for (long x : std::views::iota(0, c.size()))
- c[x] = a[x] + b[x];
- return c;
- }
复制代码 用 contracts 配合 g++-16 -fcontract-evaluation-semantic=quick_enforce 就又可以让代码 fail fast 了.
代码框架
标识符
全部使用 snake_case, 和 STL 保持统一, STL 用 snake_case 那我也用.
这方面我和 bjarne 的意见相同, 可以在 bjarne stroustrup q&a2 的 how do you name variables? do you recommend "hungarian"? 条目见到 bjarne 观点的详细陈述.
头文件
用 #include .
不是说这个代码是竞赛专用, 只要 g++ 提供了就说明这个头文件是有意义的, 开发中使用也有很多好处, 增加的编译时间可以忽略不计.
有人说这个会引入一些符号, 这个要分两方面说:
- 函数名容易冲突. 你不 using namespace std 哪来的函数名冲突?
- 宏名容易冲突. 首先你应该少定义宏, 其次我不知道你为什么非要定义一个冲突的宏名, 再说了这里面定义的什么宏是你需要再定义一遍的?
而且我用这个头文件有个次要目的是为了避免我的代码被 msvc 编译, 因为我只能确定我的代码在类 unix 系统上不出错, windows 上出任何错误都有可能.
命名空间
禁止 using namespace std;.
推荐的有 using std::complex_literals: perator""i; 和 using std::string_literals: perator""s;.
宏
一般不要在代码里用宏.
这方面我和 bjarne 的意见再次相同, 可以在 bjarne stroustrup f&q2 的 so, what's wrong with using macros? 条目见到 bjarne 观点的详细陈述.
例外情况是, 假如你叫李华, 你可以定义 -Dli_hua 表示你在 debug, 然后写:- void log(long line_num) {
- #ifdef li_hua
- std::println(std::cerr, "line: {} | i`m not good at English", line_num);
- #endif
- }
复制代码 常量
用 k_ 前缀来代表这是个常量, 能用 constexpr 尽量用, 否则用 const.
比如说:- constexpr long k_inf = 0x3f3f3f3f3f3f3f3fl; // 用于最大值, 最小值直接用 -k_inf
- constexpr long k_mod = 998244353; // 用于取模
- constexpr long k_max_vtx = 1l << 17; // 用于顶点数量, 2^17 <=> 10^5
复制代码 如果要修改 (比如标记一条边) 就用引用:- auto tuple = std::make_tuple(1, 2, 3);
- auto x = std::move(y);
复制代码 函数
如果一个参数不变, 一定要加 const. 比如刚才那个向量加法函数.
如果两个参数指向的内容不会重叠, 一定要加 restrict. 优化的作用对我来说并不重要因我我信任 -O2, 但这样可以提醒我多次检查不要传入重叠的东西.
合理情况下可以用运算符重载.
引用符号和指针符号紧贴变量, 如果有 restrict 则写类似 long *restrict a, long *restrict b.
匿名函数
只能用于回调函数, 比如:- for (std::size_t x : std::views::iota(0uz, v.size()))
- std::println("{}", v[x]);
复制代码很明显这个例子并不好, 完全可以用 std::sort(a.rbegin(), a.rend()) 一行搞定的事非要用匿名函数, 但这是为了演示匿名函数所以情有可原, 实际应用中最好是使用 std::sort(a.rbegin(), a.rend()).
main 函数
用 signed main, 可以是 signed main(int argc, char **argv) 也可以是 signed main(void), 根据需求来.
没有出错则 return EXIT_SUCCESS, 否则 return EXIT_FAILURE.
类
无论是单纯存数据还是带有函数, 都用 class.
类变量
变量名后加下划线, 比如 ld_ tot_.
根据需要可以放 private 或 public, 不必全放在 private. 最好的例子是我用于处理图的类:- for (auto [y, z] : x->to_)
- y->dfs();
复制代码 构造函数里为类变量区分 复制, 引用, 抢劫
复制是说你要给传入的 object 复制一份, 也就是 ld_ = new tree(*y->ld_).
引用是说你要引用传入的 object, 也就是 ld_ = y->ld_.
抢劫则很明显就是你要让传入的 object 失效, 也就是 ld_ = y->ld_, 注意一定要额外写一行 y->ld_ = nullptr, 或者直接写 ld_ = std::move(y->ld_).
析构函数
非必要不写, 让智能指针和 STL 自动释放, 如果有裸指针则在析构函数里以合理方式杀死.
重载运算符
非常推荐, 比如矩阵乘法, 重载运算符后可以方便的实现矩阵快速幂.
指针
到了最有意思的部分了.
我哥 3f 的指针哲学大多来源于 mycall, 而我的指针哲学部分来源于 一扶苏一女士 另外的来源于 the cherno.
裸指针
用于管理图论的顶点 (树论也属于图论).
尽量尽量尽量不要用裸指针存储 new.
因为图论是复杂的, 但又是不变的, 这恰好也是裸指针的优势, 所以我们这样写:- for (auto &[y, z, delta] : x->to_)
- if (y->dfs())
- delta = 1;
复制代码 杂项
缩进
使用 tab 可以让你的代码在不同的 ide 里可以按照不同人的喜好来缩进, 而使用空格会导致所有人看到的都是按照你的喜好进行的缩进.
我平时用的 8 字长 tab, 到了 mycall 的电脑上显示的是 2 字长, 这样我们两人看着都方便.
行长限制
我个人喜欢把行长限制为 1mol 个字长, 我觉得过分限制行长 (比如 80 字长) 是不好的 don quixote 风格.
在 1mol 字长限制的情况下, 一行的长度经常超出我的显示器, 那么我就会使用这个设置:- // std::vector<long> a
- std::sort(a.begin(), a.end(), [](long x, long y) {
- return y < x;
- });
复制代码 运算符
二元运算符和三元运算符两边加空格, 比较美观, 没什么特别的用处. 一元运算符可加可不加.
如果一个地方你想要的优先级符合 iso 的优先级 那就不要加括号, 比如取中点你想要 \(\cfrac{left\_+right\_}{2}\) 就可以直接写 left_ + right_ >> 1.
额外的, 我还喜欢把 x > y 写成 y < x, 因为我不太喜欢大于号...
管道运算符
不要用, 不要用, 不要用.
如果你需要遍历一个集合, 你应该做的是先构造这个集合, 无论是 std::vector euler_sieve(long max) 还是 std::views::iota(0uz, v.size()) 都是构造的方式.
但管道是个奇怪的东西, 管道的含义是先构造出一个全集, 然后用淘汰赛的方式选出你需要的集合. 我觉得这非常不明确, 不如直接构造出想要的集合.
goto 运算符
这是运算符吗? 反正我喜欢当成运算符.
可以适当使用, 当且仅当你是往下跳. 不要用来跳到函数最后统一释放资源因为资源应该让智能指针自动释放, 而是用于状态机的设计.
比如分块的区间求和:
[code]long query(std::size_t p, std::size_t q) const { if (q < p) std::println(std::cerr, "range error: [{}, {}]", q, p), std::abort(); if (p < 0 || a_.size() |