lil_tea c++ style guide
部分借鉴自 the cherno, 部分借鉴自 google c++ style guide, 部分借鉴自 linux kernel coding style, 部分借鉴自 算法竞赛进阶指南.
因为我年纪大了所以已经忘记哪一条是从哪借鉴的了, 朋友们看到哪条觉得合理就用吧, 不合理的不用就好了.
总结一句话: 坚持 c like 特色指针主义道路, 以安全性换自由; 姓 bjarne 还是姓 graydon 的问题上我们要坚持 bjarne 领导, 坚持 k&r 的文化自信, 杜绝 graydon 的糖衣炮弹.
调试
我觉得调试是最重要的, 所以放在最开头.
调试, 最最最重要的, sudo apt remove gdb (这只是个玩笑, 不要真的执行).
深入学习贯彻 fail fast 原则, 在出现错误时直接退出程序, 而不是使用 try throw catch. 编写程序的时候假设所有东西不会出错, 然后每当出现程序异常退出就可以知道程序出错了.
检测出错的方式非常简单, 在你认为可能出错的一行语句后输出调试, 输出的可以是任何你喜欢的东西. 可以在分号后直接写, 也可以换行后写, 但一定是在后面写.
这里是个例子, 你可以调用 log(__LINE__) 的方式直接输出调试, 而在没有 -Dyoung_tea 时调试语句不会输出.
注意
""s需要using std::string_literals::operator""s;.
cpp
void log(long line_number) {
#ifdef lil_tea
std::cerr << "line: "s << line_number << " | hey siri, play <hit'em up> please\n"s;
#endif
}
比如我有一个向量加法的函数:
cpp
std::vector<long>* add(const std::vector<long> &a, const std::vector<long> &b) { // 这行不算注释正好 80 字符
if (a.size() != b.size()) {
log(__LINE__); // 很显然如果运行到这里, 那应该是死了
std::abort(); // 我炸
// *(long*)nullptr = 998244353; // 很显然我以前用的这个方式更粗暴, 不建议使用
}
auto c = new std::vector<long>(a.size());
for (long x : std::views::iota(0, a.size()))
(*c)[x] = a[x] + b[x];
return c;
}
代码框架
标识符
全部使用 snake_case, 和 STL 保持统一, STL 用 snake_case 那我也用.
头文件
用 #include <bits/stdc++.h>.
不是说这个代码是竞赛专用, 只要 g++ 提供了就说明这个头文件是有意义的, 开发中使用也有很多好处, 增加的编译时间可以忽略不计.
有人说这个会引入一些符号, 这个要分两方面说:
- 函数名容易冲突. 你不
using namespace std哪来的函数名冲突? - 宏名容易冲突. 首先你应该少定义宏, 其次我不知道你为什么非要定义一个冲突的宏名, 再说了这里面定义的什么宏是你需要再定义一遍的?
而且我用这个头文件有个次要目的是为了避免我的代码被 msvc 编译, 因为我只能确定我的代码在类 unix 系统上不出错, windows 上出任何错误都有可能.
命名空间
禁止 using namespace std;.
推荐的有 using std::complex_literals::operator""i; 和 using std::string_literals::operator""s;.
宏
一般不要在代码里用宏.
例外情况是, 加入你叫李华, 你可以定义 -Dli_hua 表示你在 debug, 然后写:
cpp
void log(long line_number) {
#ifdef li_hua
std::cerr << "line: "s << line_number << " | hey siri, play <hit'em up> please\n"s;
#endif
}
常量
用 k_ 前缀来代表这是个常量, 能用 constexpr 尽量用, 否则用 const.
比如说:
cpp
constexpr long k_inf = 0x3f3f3f3f3f3f3f3fl; // 用于最大值, 最小值直接用 -k_inf
constexpr long k_mod = 998244353; // 用于取模
constexpr long k_max_ver = 1l << 17; // 用于顶点数量, 2^17 <=> 10^5
变量
尽量缩小变量的作用域, 比如 for 用的变量就尽量不要让作用域到 for 外面.
在类型明确的时候可以用 auto, 需要明确类型的时候用类型名.
这个 明确 包括函数返回值的类型, 认为是明确的.
cpp
auto tuple = std::make_tuple(1, 2, 3);
auto x = std::move(y);
cpp
for (std::size_t x = 0; x < v.size(); ++x)
std::cout << v[x] << "\n"s;
当然带权图遍历连边应该用结构化绑定:
cpp
for (auto [y, z] : x->to_)
y->dfs();
如果要修改 (比如标记一条边) 就用引用:
cpp
for (auto &[y, z, delta] : x->to_)
if (y->dfs())
delta = 1;
函数
如果一个参数不变, 一定要加 const. 比如刚才那个向量加法函数.
合理情况下可以用运算符重载.
匿名函数
只能用于回调函数, 比如:
cpp
// std::vector<long> a
std::sort(a.begin(), a.end(), [](long x, long y) {
return y < x;
});
很明显这个例子并不好, 完全可以用
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. 最好的例子是我用于处理图的类:
cpp
class vtx {
public:
std::vector<vtx*> to_;
long dfn_, low_; // for tarjan
vtx *top, *dear_mama, *kid; // for 树链剖分
void add_edge(vtx*);
void dfs_tarjan(/*anything*/);
void dfs1_hld(vtx*), dfs2_hld(vtx*);
};
vtx v[k_max_ver];
void vtx::add_edge(vtx *y) {
if (this < v || v + k_max_ver <= this)
std::abort();
if (y < v || v + k_max_ver <= y)
std::abort();
to_.emplace_back(y);
}
类函数
根据需要可以放 private 或 public, 不必全放在 public, 最好的例子是线段树:
cpp
class tree {
std::unique_ptr<tree> ld_, rd_;
long left_, right_;
long val_, tag_;
void push_up(void); // 私有
void push_down(void); // 私有
public:
tree(long, long, const std::vector<long>&); // 公有
void update(long, long, long); // 公有
long query(long, long, long); // 公有
};
构造函数
非常推荐, 一定要用初始化列表. STL 容器可以初始化或不初始化. 智能指针见后文指针部分.
cpp
tree::tree(long left, long right, const std::vector<long> &a)
: left_(left), right_(right),
val_(0), tag_(0) {
if (left_ == right_) {
val_ = a[left_];
return;
}
ld_ = std::make_unique<tree>(left_, left_ + right_ >> 1, a);
rd_ = std::make_unique<tree>((left_ + right_ >> 1) + 1, right_, a);
push_up();
}
构造函数里为类变量区分 复制 , 引用 , 抢劫
复制是说你要给传入的 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.
因为图论是复杂的, 但又是不变的, 这恰好也是裸指针的优势, 所以我们这样写:
cpp
class vtx{
public:
std::vector<vtx*> to_;
void add_edge(vtx*);
};
vtx v[k_max_ver]; // 全局数组, 可以放在 graph:: 命名空间或者直接全局命名空间
void vtx::add_edge(vtx *y) {
if (this < v || v + k_max_ver <= this)
std::abort(); // fail fast
if (y < v || v + k_max_ver <= y)
std::abort();
to_.emplace_back(y);
}
带权图类似, 这样写:
cpp
class vtx{
public:
std::vector<std::tuple<vtx*, long>> to_;
void add_edge(vtx*, long);
};
vtx v[k_max_ver];
void vtx::add_edge(vtx *y, long z) {
if (this < v || v + k_max_ver <= this)
std::abort();
if (y < v || v + k_max_ver <= y)
std::abort();
to_.emplace_back(y, z);
}
关于 char **argv
我的习惯是这样:
cpp
signed solve(const std::vector<std::string>&);
signed main(int argc, char **argv) {
std::vector<std::string> args(argv, argv + argc);
return solve(args);
}
很显然这是借鉴了 java 的
String[] args, 但 java 作为友军语言也是可以借鉴的优秀设计.
智能指针
独占指针
独占指针的语义是, 这个内容是你自己的.
很好的例子是线段树, 你的孩子顶点肯定是你独占的, 所以我们用独占指针.
cpp
class tree {
std::unique_ptr<tree> ld_, rd_;
// other
};
共享指针
共享指针的语义是, 这个内容是公有的.
很好的例子是持久化线段树, 你的孩子是继承的 / 修改的, 所以我们用共享指针.
cpp
class tree {
std::shared_ptr<tree> ld_, rd_;
// other
};
弱指针
弱指针的语义是, 我还是个共享指针, 但是我不参与引用计数, 用于避免循环引用.
很好的例子是双链表.
cpp
class list_node {
public:
std::weak_ptr<list_node> pre;
std::shared_ptr<list_node> nxt;
// other
};
iso 和 cherno 认为的弱指针语义是, 这个对象可能活着也可能死了, 所以使用之前需要先检查. 但我不认同这个语义.
很显然刚才的双链表就是避免循环引用才用的弱指针, 你无需使用 if (pre.lock()) 就能确定 pre.lock() 是有效的, 如果无效则说明你的双链表是错误的 (当然了, 头顶点的 pre 就应该是空的, 而后面每个顶点的 pre 都应该是有效的), 而如果错了就会因为 pre.lock() 为空而及时 fail fast.
杂项
缩进
使用 tab 可以让你的代码在不同的 ide 里可以按照不同人的喜好来缩进, 而使用空格会导致所有人看到的都是按照你的喜好进行的缩进.
我平时用的 8 字长 tab, 到了 mycall 的电脑上显示的是 2 字长, 这样我们两人看着都方便.
运算符
二元运算符和三元运算符两边加空格, 比较美观, 没什么特别的用处. 一元运算符可加可不加.
额外的, 我还喜欢把 x > y 写成 y < x, 因为我不太喜欢大于号...
代码块
if 后空格再写条件, for 后空格再写循环描述, while 后空格再写条件, 禁用 switch.
因为 switch 没写 break 害我在基本算法单元测试里丢了 \(160\) 分.
注释
行内注释使用 /**/ (不允许跨行), 行末注释使用 //.
枚举
推荐使用 enum class, 比如说我设计 game engine 的时候就有:
cpp
enum class color {
red = 0xff0000,
green = 0x00ff00,
blue = 0x0000ff
};
这样就可以确保使用 color x = color::red 而不是 int x = color::red.