lil_tea c++ 2026 style guide
总结一句话: 坚持 c like 特色指针主义道路, 以安全性换自由; 姓 bjarne 还是姓 graydon 的问题上我们要坚持 bjarne 领导, 坚持 k&r 的文化自信, 坚决杜绝 graydon 的糖衣炮弹.
think twice, code once. enjoy~
调试
最重要的当然是打印函数:
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 <bits/stdc++.h>, 或者直接用 import std.
不是说这个代码是竞赛专用, 只要 g++ 提供了就说明这个头文件是有意义的, 开发中使用也有很多好处, 增加的编译时间可以忽略不计.
有人说这个会引入一些符号, 这个要分两方面说:
- 函数名容易冲突. 你不
using namespace std哪来的函数名冲突? - 宏名容易冲突. 首先你应该少定义宏, 其次我不知道你为什么非要定义一个冲突的宏名, 再说了这里面定义的什么宏是你需要再定义一遍的?
而且我用这个头文件有个次要目的是为了避免我的代码被 msvc 编译, 因为我只能确定我的代码在类 unix 系统上不出错, windows 上出任何错误都有可能.
命名空间
禁止 using namespace std;.
推荐的有 using namespace std::literals;.
宏
一般不要在代码里用宏.
这方面我和 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 |
变量
尽量缩小变量的作用域, 比如 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; |
全局变量
如果可能会被多个线程修改就加锁, 如果确定只会被单线程修改就不用加锁.
用 namespace 管理全局变量和对应的锁.
namespace total { // 实际变量名 |
long val; // 占位变量名 |
std::mutex lock; |
} |
全局变量名不加 g_ 前缀, 查询或修改全局变量的函数名需要加 g_ 前缀.
函数
大多数函数的返回值应该只跟参数有关, 一个函数只做好一件事, 尽量不动全局变量, 动全局变量的函数的函数名需要加 g_ 前缀.
如果一个参数不变, 一定要加 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 0, 否则 return 1.
类
无论是单纯存数据还是带有函数, 都用 class.
类变量
变量名后加下划线, 比如 ld_ tot_.
根据需要可以放 private 或 public, 不必全放在 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]; |
类函数
根据需要可以放 private 或 public, 不必全放在 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 自动释放, 如果有裸指针则在析构函数里以合理方式杀死.
重载运算符
非常推荐, 比如矩阵乘法, 重载运算符后可以方便的实现矩阵快速幂.
shared_from_this
千万不要用 shared_from_this, 使用传入的 std::shared_ptr & 或 const std::shared_ptr & 代替.
参数名建议叫 ref_this, 先判断:
if (ref_this.get() != this) |
std::terminate(); |
当然这里现在用 pre 会更方便了.
std::weak_ptr 的重载版本
比如说有 classx::work(std::shared_ptr<classx> &ref_this), 这时候我还会写一个重载的 classx::work(std::weak_ptr<classx> &ref_this).
内容很简单, 直接调用:
更多推荐
所有评论(0)