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++ 提供了就说明这个头文件是有意义的, 开发中使用也有很多好处, 增加的编译时间可以忽略不计.

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

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

而且我用这个头文件有个次要目的是为了避免我的代码被 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).

内容很简单, 直接调用:


auto work(std::shared_ptr<classx> &ref_this) {
return work(ref_this.lock());
}

这里为什么我敢在 lock() 为空的时候也传入呢, 因为 lock() 为空的时候是通不过 if (ref_this.get() != this) 的检测的, 就炸了.

指针

到了最有意思的部分了.

我哥 3f 的指针哲学大多来源于 mycall, 而我的指针哲学部分来源于 一扶苏一女士 另外的来源于 the cherno.

我的大概想法就是说, 裸指针指向 .bss (别跟我说全局变量在初始化后会跑到 .data, 我说是 .bss 就是 .bss), 智能指针指向 .heap, 引用指向 .stack, 不要越界.

裸指针

用于管理图论的顶点 (树论也属于图论).

尽量尽量尽量不要用裸指针存储 new.

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


constexpr long k_max_vtx = 1l << 17;
class vtx;
extern vtx v[k_max_vtx]; // 提前声明, 后面就能判断了
class vtx{
public:
std::vector<vtx*> to_;
void add_edge(vtx *y)
pre(v <= this && this < v + k_max_vtx && v <= y && y < v + k_max_vtx) {
to_.emplace_back(y);
}
};
vtx v[k_max_vtx];

关于 char **argv

我的习惯是这样:


signed solve(const std::span<std::string_view>&);
signed main(int argc, char **argv) {
std::vector<std::string_view> tmp(argv, argv + argc);
std::span<std::string_view> args(tmp);
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 的优先级 那就不要加括号, 比如取中点你想要 left_+right_22left_+right_​ 就可以直接写 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
pre(p <= q && 0 <= p && q < a.size()) {
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:


void dfs_kid(vtx *_) {
if ((dear_mama_ = _)) // if 里做赋值并判断
depth_ = dear_mama_->depth_ + 1;
else
depth_ = 0uz;
size_ = 1uz;
kid_ = v;
for (vtx *y : to_) {
if (y->kid_)
continue;
y->dfs_kid(this);
size_ += y->size_;
if (kid_->size_ < y->size_ || kid_->size_ < y->size_ && y < kid_)
kid_ = y;
}
}

注释

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

枚举

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


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

更多推荐