lil_tea c++ 2023 style guide

lil_tea c++ 2023 style guide

本文使用 macbook pro m4 + vscode 编写.

我喝 mundo novo 不抽 marlboro
懂 c++ 的 coders 只能算少数
同质化的驼峰真的烂到吐
有廿年开发经验但我虚岁 thirty-two

这是我在开发和算竞都使用的代码风格.

部分借鉴自 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;.

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) {	// 这行不算注释是 79 字符	// 注释中使用讽刺的手法批评了 linus 的理想主义. 但实际上 lil_tea 非常喜欢 linus, 只是不喜欢 80 字符的行长限制而已
	if (a.size() != b.size()) {
		log(__LINE__);	// 很显然如果运行到这里, 那应该是死了
		std::abort();	// 我炸
		// *(long*)nullptr = 998244353;	// 很显然我以前用的这个方式更粗暴, 不建议使用
	}

	std::vector<long> c(a.size());

	for (long x : std::views::iota(0, c.size()))
		c[x] = a[x] + b[x];

	return c;
}

代码框架

标识符

全部使用 snake_case, 和 STL 保持统一, STL 用 snake_case 那我也用.

这方面我和 bjarne 的意见相同, 可以在 bjarne stroustrup q&a2how do you name variables? do you recommend "hungarian"? 条目见到 bjarne 观点的详细陈述.

头文件

#include <bits/stdc++.h>.

不是说这个代码是竞赛专用, 只要 g++ 提供了就说明这个头文件是有意义的, 开发中使用也有很多好处, 增加的编译时间可以忽略不计.

有人说这个会引入一些符号, 这个要分两方面说:

  1. 函数名容易冲突. 你不 using namespace std 哪来的函数名冲突?
  2. 宏名容易冲突. 首先你应该少定义宏, 其次我不知道你为什么非要定义一个冲突的宏名, 再说了这里面定义的什么宏是你需要再定义一遍的?

而且我用这个头文件有个次要目的是为了避免我的代码被 msvc 编译, 因为我只能确定我的代码在类 unix 系统上不出错, windows 上出任何错误都有可能.

命名空间

禁止 using namespace std;.

推荐的有 using namespace std::literals;.

一般不要在代码里用宏.

这方面我和 bjarne 的意见再次相同, 可以在 bjarne stroustrup f&q2so, 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

变量

尽量缩小变量的作用域, 比如 for 用的变量就尽量不要让作用域到 for 外面.

引用符号和指针符号紧贴变量, 如 tree *ld_const std::vector<long> &a.

在类型明确的时候可以用 auto, 需要明确类型的时候用类型名.

这个 明确 包括函数返回值的类型, 认为是明确的.

auto tuple = std::make_tuple(1, 2, 3);
auto x = std::move(y);
for (std::size_t x : std::views::iota(0uz, v.size()))
	std::println("{}", v[x]);

当然带权图遍历连边应该用结构化绑定:

for (auto [y, z] : x->to_)
	y->dfs();

如果要修改 (比如标记一条边) 就用引用:

for (auto &[y, z, delta] : x->to_)
	if (y->dfs())
		delta = 1;

函数

如果一个参数不变, 一定要加 const. 比如刚才那个向量加法函数.

如果两个参数指向的内容不会重叠, 一定要加 restrict. 优化的作用对我来说并不重要因我我信任 -O2, 但这样可以提醒我多次检查不要传入重叠的东西.

合理情况下可以用运算符重载.

引用符号和指针符号紧贴变量, 如果有 restrict 则写类似 long *restrict a, long *restrict b.

匿名函数

只能用于回调函数, 比如:

// std::vector<long> a
std::sort(a.begin(), a.end(), [](long x, long y) {
	return y < 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_.

根据需要可以放 privatepublic, 不必全放在 private. 最好的例子是我用于处理图的类:

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_vtx];

void	vtx::add_edge(vtx *y) {
	if (this < v || v + k_max_vtx <= this)
		std::abort();
	if (y < v || v + k_max_vtx <= y)
		std::abort();

	to_.emplace_back(y);
}

类函数

根据需要可以放 privatepublic, 不必全放在 public, 最好的例子是线段树:

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 容器可以初始化或不初始化. 智能指针见后文指针部分.

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.

因为图论是复杂的, 但又是不变的, 这恰好也是裸指针的优势, 所以我们这样写:

class	vtx{
public:
	std::vector<vtx*> to_;

	void add_edge(vtx*);
};

vtx v[k_max_vtx];	// 全局数组, 可以放在 graph:: 命名空间或者直接全局命名空间

void	vtx::add_edge(vtx *y) {
	if (this < v || v + k_max_vtx <= this)
		std::abort();	// fail fast
	if (y < v || v + k_max_vtx <= y)
		std::abort();

	to_.emplace_back(y);
}

带权图类似, 这样写:

class	vtx{
public:
	std::vector<std::tuple<vtx*, long>> to_;

	void add_edge(vtx*, long);
};

vtx v[k_max_vtx];

void	vtx::add_edge(vtx *y, long z) {
	if (this < v || v + k_max_vtx <= this)
		std::abort();
	if (y < v || v + k_max_vtx <= y)
		std::abort();

	to_.emplace_back(y, z);
}

关于 char **argv

我的习惯是这样:

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 作为友军语言也是可以借鉴的优秀设计.

智能指针

独占指针

独占指针的语义是, 这个内容是你自己的.

很好的例子是线段树, 你的孩子顶点肯定是你独占的, 所以我们用独占指针.

class	tree {
	std::unique_ptr<tree> ld_, rd_;
	// other
};

共享指针

共享指针的语义是, 这个内容是公有的.

很好的例子是持久化线段树, 你的孩子是继承的 / 修改的, 所以我们用共享指针.

class	tree {
	std::shared_ptr<tree> ld_, rd_;
	// other
};

弱指针

弱指针的语义是, 我还是个共享指针, 但是我不参与引用计数, 用于避免循环引用.

很好的例子是双链表.

class	list_vtx {
public:
	std::weak_ptr<list_vtx> pre_;
	std::shared_ptr<list_vtx> nxt_;
	// other
};

iso 和 cherno 认为的弱指针语义是, 这个对象可能活着也可能死了, 所以使用之前需要先检查. 但我不认同这个语义.

很显然刚才的双链表就是避免循环引用才用的弱指针, 你无需使用 if (pre_.lock()) 就能确定 pre_.lock() 是有效的, 如果无效则说明你的双链表是错误的 (当然了, 头顶点的 pre_ 就应该是空的, 而后面每个顶点的 pre_ 都应该是有效的), 而如果错了就会因为 pre_.lock() 为空而及时 fail fast.

删除顶点 x 直接 x->pop():

void	pop(void) {
	if (nxt_)	// 如果先修改 pre_->nxt_ 则会导致引用计数为 0, 所以先修改 nxt_->pre_
		nxt_->pre_ = pre_;
	pre_.lock()->nxt_ = nxt_;	// 头顶点不存数据, 所以存数据的顶点一定有 pre_
	// 引用计数为 0 自动删除, 会调用析构函数
}

杂项

缩进

使用 tab 可以让你的代码在不同的 ide 里可以按照不同人的喜好来缩进, 而使用空格会导致所有人看到的都是按照你的喜好进行的缩进.

我平时用的 8 字长 tab, 到了 mycall 的电脑上显示的是 2 字长, 这样我们两人看着都方便.

行长限制

我个人喜欢把行长限制为 1mol 个字长, 我觉得过分限制行长 (比如 80 字长) 是不好的 don quixote 风格.

在 1mol 字长限制的情况下, 一行的长度经常超出我的显示器, 那么我就会使用这个设置:

"editor.wordWrap": "bounded",	// 在屏幕宽度和代码行长限制的较小值处自动折行, 实际还在同一行
"editor.wordWrapColumn": 1073741824,	// vscode 最大行长限制
"editor.wrappingIndent": "same",	// 自动折行后维持相同缩进
"editor.wrappingStrategy": "advanced",	// 让 vscode 计算行长, 因为 tab 的字长不一定是 1 所以需要

运算符

二元运算符和三元运算符两边加空格, 比较美观, 没什么特别的用处. 一元运算符可加可不加.

如果一个地方你想要的优先级符合 iso 的优先级 那就不要加括号, 比如取中点你想要 \(\cfrac{left\_+right\_}{2}\) 就可以直接写 left_ + right_ >> 1.

额外的, 我还喜欢把 x > y 写成 y < x, 因为我不太喜欢大于号...

管道运算符

不要用, 不要用, 不要用.

如果你需要遍历一个集合, 你应该做的是先构造这个集合, 无论是 std::vector<long> euler_sieve(long max) 还是 std::views::iota(0uz, v.size()) 都是构造的方式.

但管道是个奇怪的东西, 管道的含义是先构造出一个全集, 然后用淘汰赛的方式选出你需要的集合. 我觉得这非常不明确, 不如直接构造出想要的集合.

goto 运算符

这是运算符吗? 反正我喜欢当成运算符.

可以适当使用, 当且仅当你是往下跳. 不要用来跳到函数最后统一释放资源因为资源应该让智能指针自动释放, 而是用于状态机的设计.

比如分块的区间求和:

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() <= q)
		std::println(std::cerr, "range beyond [0, size())"),
		std::abort();

	std::size_t from = pos_[p], term = pos_[q];
	long yt = 0;

	if (from != term)
		goto qry_from;

	for (std::size_t x = p; x <= q; ++x)
		yt += a_[x] + tag_[from];
	
	return yt;

qry_from:
	if (from_[from] == p)	// 整块则当成整块
		goto qry_term;

	for (std::size_t x = p; x <= term_[from]; ++x)
		yt += a_[x] + tag_[from];

	from++;

qry_term:
	if (q == term_[term])	// 整块则当成整块
		goto qry_ssr;

	for (std::size_t x = from_[term]; x <= q; ++x)
		yt += a_[x] + tag_[term];

	term--;

qry_ssr:
	for (std::size_t x = from; x <= term; ++x)
		yt += sum_[x];

	return yt;
}

代码块

if 后空格再写条件, for 后空格再写循环描述, while 后空格再写条件, 禁用 switch.

因为 switch 没写 break 害我在基本算法单元测试里丢了 160 分.

if 里赋值是很常见的情况, 这时建议使用双层括号来明确语义, 比如说树剖的 dfs:

std::vector<vtx*> list;	// 全局 std::vector

void	vtx::dfs_top(vtx *_) {
	if ((top_ = _))	// if 的括号内赋值, 使用双层括号
		rank_ = list.size(),
		list.emplace_back(this);
	else
		std::println(std::cerr, "{}'s top has no prestige", this - v),
		std::abort();

	if (kid_ == v)	// if 的括号内判断相等, 使用单层括号
		return;

	kid_->dfs_top(top_);

	for (vtx *y : to_) {
		if (y->top_)
			continue;

		y->dfs_top(y);
	}
}

注释

行内注释使用 /**/ (不允许跨行), 行末注释使用 //.

枚举

推荐使用 enum class, 比如说我设计 game engine 的时候就有:

enum class color : long {
	red = 0xff0000,
	green = 0x00ff00,
	blue = 0x0000ff
};

这样就可以确保使用 color x = color::red 而不是 int x = color::red.

结语

coder 是铁, style 是钢. 有那么多种码风, 找你喜欢的用就行了, 有不舒服的地方就改成让你舒服的.

posted @ 2026-03-19 21:44  lil_tea  阅读(204)  评论(0)    收藏  举报