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

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

  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).

内容很简单, 直接调用:

更多推荐