[{"content":"Introduction 支配树在编译器中最主要的作用是用于计算支配边界，用于插入 $\\phi$ 函数，将 ir 提升到 SSA 形式。以及提供循环优化，DCE 等 pass 需要的结构信息。\nControl Flow Graph 有如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 int main () { int a = 1; a = fac(9); if (a == 1) { a = 2; } else { a = 4; } return a; } 我们可以构建出 ir：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func @main() : () -\u0026gt; i32 { ^entry_0: %0 = alloca : i32 -\u0026gt; i32* store 1, %0 : i32, i32* %1 = call @fac(9) : (i32) -\u0026gt; i32 store %1, %0 : i32, i32* %2 = eq %1, 1 : i32 -\u0026gt; i1 branch %2, ^then_1, ^else_2 ^then_1: store 2, %0 : i32, i32* jump ^merge_3 ^else_2: store 4, %0 : i32, i32* jump ^merge_3 ^merge_3: %3 = load %0 : i32* -\u0026gt; i32 ret %3 } 在程序执行过程，我们可以把这个 if 看成一个分叉点，分叉点之前 a = 1, 而分叉点之后 a = 2 或者 4,可以把这个过程看成有向图。ir 有四个基本块，表达如下：\n控制流在 if 出现时发生分叉，在 if 结束时汇合：\n我们对其建图，称之为 CFG (Control Flow Graph) 。有了 CFG，就可以将图论的知识应用到程序分析中。支配树就是基于 CFG 构建的。\nDomTree 本文涉及到的算法并不仅仅只作用于 DAG，但是为了简单起见，都以 DAG 说明。\n首先有存在 DAG 如下：\n可以观察到，从节点 1 出发，可以到达其余所有节点，我们给出定义：\n钦定入口结点 $s$，对于一个结点 $u$，若从 $s$ 到 $u$ 的每一条路径都经过某一个结点 $v$，那么我们称 $v$ 支配 $u$ ，也称 $v$ 是 $u$ 的一个支配点，记作 $v\\ dom\\ u$．\n对于函数来说，我们一定有一个进入函数执行流的基本块，一定有一个退出函数执行流的基本块，整个过程可以用「存在一个节点 $s$ 可达所有节点的 DAG」建模。如果存在不可达的节点，那么节点无效，可以删除。\n接下来我们深入介绍 domtree 相关概念。在图中，节点 1 是所有节点的支配点，而到达节点 6 的路径有两条 $1 \\to 2 \\to 3 \\to 6$ 和 $1 \\to 2 \\to 3 \\to 5 \\to 6$ 。则 2,3 是 6 的支配点，而 5 不是 6 的支配点。\n把节点 $u$ 所有支配点构成一个集合，称之为支配点集，上图所有节点的支配点集如下：\n一个直接的求解支配点思路是，如果一个节点 $u$ 从图中删除导致节点 $v$ 不可达，那么 $u$ 支配 $v$。\n显然这个思路可以进行优化，如果已知节点 $u$ 是 $v$ 的支配点，$v$ 是 $n$ 的支配点，则 $u$ 是 $n$ 的支配点。即支配点具有传递性。根据这个性质，我们可以得出对于节点 $u$ 来说，其支配点集就是 $u$ 自身和其前驱接节点的支配点集的交集。在上图中，8 的支配点集就是集合 $\\{1,2,3,6\\}$ 和集合 $\\{1,2,3,5\\}$ 的交集和 8 自身 $\\{1,2,3,8\\}$。\n我们把除节点 $u$ 以外的距离 $u$ 最近的支配点 $v$ 称作直接支配点，记作 $v\\ idom\\ u$。比如节点 8 有支配点集 $\\{1, 2, 3, 8\\}$，则 3 是 8 的直接支配点，记作 $3\\ idom\\ 8$ 。除了入口节点 $s$ 以外，其余所有节点都有唯一的直接支配点。若将图中的节点与其直接支配点连边，其构成一颗节点数目为 n 的树，称之为支配树。\n这就是我们需要构建的支配树，接下来我们介绍一种快速构建支配树的算法 Lengauer–Tarjan 算法。\nLengauer-Tarjan 算法是构建支配树最有名的算法之一，可以在 $O(n\\alpha(n,m))$ 内求出一个有向图的支配树，这个算法最精华的点就是引入了半支配点的概念，将一个全局的复杂问题降级为局部的易分析问题。\n如果我们对一个图进行 dfs，所经过的点和边可以构成一颗树。令 $dfn(u)$ 表示为节点 $u$ 被遍历到的时间。这些概念在图论里面很常见（\n定义一个集合 $V$ ，其中所有的节点 $v_{i}$ 满足从 $v_{i}$ 出发到节点 $u$，存在一条路径，路径上除了 $u,v_{i}$ 以外，其余所有节点 $x_{i}$ 都有 $dfn(x_{i}) \u003e dfn(u)$。一个节点 $u$ 的半支配点 $v$ 就是集合 $V$ 中 dfn 最小的那个。即 $v = min(V)$。\n给出上图的 dfn 序列：\n节点 8 的半支配点是 3，其中有节点 6,5,7,3 都满足从自身出发，其路径上除了自身和节点 8，路径上的中间结点的 dfn 序都大于 dfn(8)。而 3 是其中 dfn 最小的一个。\n可以观察到，半支配点的产生可能有两种情况，第一种是 $u$ 的直接前驱节点，另一种是如果前驱节点的 dfn 比 $u$ 大，那么就从前驱节点的前驱节点找，这是一个反复执行的过程，直到前驱节点的 dfn 比 $u$ 小。\n形式化的说，一个节点 $u$ 的半支配点 $sdom(u)$ 满足：\n$$ sdom(u) = min(\\{v\\ |\\ \\exists v \\to u, dfn(v) \u003c dfn(u)\\} \\cup \\{sdom(w)\\ |\\ dfn(w) \u003e dfn(u) \\text{ and } \\exists w \\to \\dots \\to v \\to u\\}) $$我们不加证明的给出这个公式，证明可以参考 oiwiki，并且我们可以用带权并查集维护公式的后半部分。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 struct LT_Dsu { std::vector\u0026lt;int\u0026gt; father; std::vector\u0026lt;int\u0026gt; label; //\u0026lt; label[x] 代表从 x 开始，到并查集中 x 的根节点为止（不包括根节点），其中 dfn(sdom) 最小的节点。 const std::vector\u0026lt;int\u0026gt; \u0026amp;dfn; const std::vector\u0026lt;int\u0026gt; \u0026amp;sdom; void find(int x) { if (x == father[x]) { return; } find(father[x]); // 在路径压缩过程中，天然的维护了上述半支配点公式的关系，确保了压缩后可以快速求出 label if (dfn[sdom[label[father[x]]]] \u0026lt; dfn[sdom[label[x]]]) { label[x] = label[father[x]]; } father[x] = father[father[x]]; } LT_Dsu(int n, const std::vector\u0026lt;int\u0026gt; \u0026amp;_dfn, const std::vector\u0026lt;int\u0026gt; \u0026amp;_sdom) : father(n + 1), label(n + 1), dfn(_dfn), sdom(_sdom) { std::iota(father.begin(), father.end(), 0); std::iota(label.begin(), label.end(), 0); } int eval(int x) { if (x == father[x]) { return label[x]; } find(x); return label[x]; } // parent node, child node void link(int u, int v) { father[v] = u; } }; 那么半支配点有什么用呢？\n给出如下算法，记 $x \\overset{*}{\\to} y$ 表示为 $x$ 是 $y$ 的祖先（可能有 $x = y$），记 $x \\overset{+}{\\to} y$ 表示 $x$ 是 $y$ 的真祖先（$x \\neq y$） ，对于任何节点 $w \\neq s$：\n令 $u$ 是 dfs 树路径上 $sdom(w) \\overset{+}{\\to} u \\overset{*}{\\to} w$ 上使得 $sdom(u)$ 的 dfn 最小的节点。\n$$ idom(w) = \\begin{cases} sdom(w) \u0026 \\text{if } sdom(u) = sdom(w) \\\\ idom(u) \u0026 \\text{if } sdom(u) \u003c sdom(w) \\end{cases} $$用普通话说，在 dfs 树上，存在一条路径 $sdom(w) \\to x_{0},x_{1},\\dots,x_{n}\\to w$，路径上除了 $sdom(w)$ 以外的点，构成集合 $X = \\{x_{0},x_{1},\\dots,x_{n},w\\}$，从这个集合选出一个点 $u$，它满足 $\\forall x \\in X, dfn(sdom(u)) \\leq dfn(sdom(x))$。此时 $idom(w)$ 满足上述关系式。\n如上图所示，路径 $sdom(8) \\to 4 \\to 7 \\to 8$ 满足上述 $dfn(sdom(x))$ 最小的点是 4 或者 8，满足 $sdom(4) = sdom(8)$，则 $idom(8)$ 就是 3。\n在这里再给出一个简单的 case：\n考查节点 4，节点 4 的 sdom 是 2，但是 2 不是 4 的 idom。可以观察到路径 $2 \\to 3 \\to 4$ 中，$sdom(3)$ 小于 $sdom(4)$ ，按照算法更新 4 的 idom 为 $idom(3)$ 也就是 1。\nsdom 代表着什么呢？按照我的个人理解，$u$ 的 $sdom(u)$ 代表着从 $sdom(u)$ 出发到 $u$，路径中除去首尾的所有节点都不可能是 $u$ 的 idom，这是一个很强的约束。嗯呢呢，如果把原图 $G$ 中的所有非树边删掉，再对于每个节点 $u$，加上一条由 $sdom(u)$ 到 $u$ 的有向边，那么变化后的新图 $G'$ 和原图 $G$ 的支配树完全相同。也就是说，我们可以假想 $sdom(u)$ 到 $u$ 存在一条直接路径，直接沟通起了 $sdom(u)$ 和 $u$。但这并不意味着 $sdom(u)$ 就一定是 $u$ 的 idom，因为就算中间的所有节点都不是 $u$ 的 idom，但其中某一个节点 $v$ 有可能存在一条 $sdom(v)$ 到 $v$ 的路径绕过 $sdom(u)$。打破 $sdom(u)$ 的支配关系。这个时候就对应上面给出公式的第二种情况。\n下给出证明（以下证明的比较 $\u003c,\\leq,\u003e,\\geq$ 等都表示 dfn 之间的比较）：\n首先给出两条引理，引理一（s 是入口节点）：\n对于任意 $w \\neq s$，直接支配者 $idom(w)$ 必然是半支配者 $sdom(w)$ 的祖先（即 $idom(w) \\overset{*}{\\to} sdom(w)$）\n引理二：\n设 $v, w$ 是 DFS 树上的节点且 $v \\overset{*}{\\to} w$。若树路径 $v \\overset{+}{\\to} y \\overset{*}{\\to} w$ 上的每一个节点 $y$ 都满足 $sdom(y) \\ge v$，则 $v$ 支配 $w$。\n引理都可以使用反证法证明，我就不给出证明了。\n首先考虑情况一，假设 $u$ 是树路径 $sdom(w) \\overset{+}{\\to} u \\overset{*}{\\to} w$ 上使得 $dfn(sdom(u))$ 最小的节点。\n我们有在树路径上 $sdom(w) \\overset{+}{\\to} y \\overset{*}{\\to} w$ 的任意节点 $y$ 都满足 $sdom(y) \\geq sdom(u) = sdom(w)$，根据引理二，$sdom(w)$ 是 $w$ 的支配点。\n既然 $sdom(w)$ 是 $w$ 的一个支配点，根据直接支配点的性质，$idom(w)$ 是最靠近 $w$ 的支配点，$idom(w)$ 必然在 $sdom(w) \\to w$ 的路径上，也就是 $sdom(w)$ 是 $idom(w)$ 的祖先，又根据引理一，$idom(w)$ 是 $sdom(w)$ 的祖先，所以 $idom(w) = sdom(w)$。$\\blacksquare$\n然后是情况二，首先，因为 $sdom(u) \u003c sdom(w)$，这意味着存在一条路径 $sdom(u) \\to \\dots \\to u$ 可以绕过 $sdom(w)$，$sdom(w)$ 就不是 $w$ 的 idom。而因为 $sdom(w) \\to w$ 的路径一定经过 $u$，这样构成的相对路径：\n$$ idom(u) \\overset{+}{\\to} sdom(w) \\overset{+}{\\to} u \\overset{*}{\\to} w $$ 先证 $idom(w)$ 支配 $u$ ，如果 $idom(w)$ 不支配 $u$，则存在一条 $s$ 到 $u$ 避开 $idom(w)$ 的路径，如下：\n显然这样出现了一条 $s \\to w$ 的路径，与 $idom(w)$ 是 $w$ 的支配点矛盾，所以假设不成立，$idom(w)$ 支配 $u$。进而得到 $idom(w) \\overset{*}{\\to}idom(u)$。\n然后证明 $idom(u)$ 支配 $w$，如果 $idom(u)$ 不支配 $w$，则存在一条从 $s$ 出发避开 $idom(u)$ 到 $w$ 的路径，我们称之为 $p$。\n设 $x$ 是路径 $p$ 上最后一个满足 $x \u003c idom(u)$ 的节点（节点 $x$ 一定存在，最起码是 $s$，并且注意这里的 $\u003c$ 是比较的 dfn）。设 $y$ 是路径 $p$ 上最后一个满足 $idom(u) \\overset{*}{\\to} y \\overset{*}{\\to} w$ 的节点（节点 $y$ 一定存在，是 $idom(u)$ 的后代，最起码是 $w$）。\n考查路径 $p$ 中 $x$ 到 $y$ 这一段子路径，由于 $x$ 的定义，该路径上的内部节点 dfn 不能小于 $idom(u)$，且不能是 $idom(u)$ 的后代。根据 $sdom$ 的定义，有：\n$$ sdom(y) \\leq x \u003c idom(u) \\leq sdom(u) $$亦即 $sdom(y) \u003c sdom(u)$ !\n现在考查 $y$ 的位置，$y$ 不能在 $sdom(w) \\overset{+}{\\to}z \\overset{*}{\\to}w$ 上，因为这段路径上，$u$ 的 $sdom$ 最小，如果 $y$ 在这一段，有 $sdom(y) \\geq sdom(u)$ 与 $sdom(y) \u003c sdom(u)$ 矛盾。\n并且 $y$ 不能在 $idom(u) \\overset{+}{\\to} z \\overset{*}{\\to} u$ 上，如果 $y$ 在这里，根据 $sdom(y) \u003c idom(u)$，可以构造出一条等效于 $s \\to sdom(y) \\to y \\to u \\to w$ 的路径，并且这个路径不会经过 $idom(u)$，与 $idom(u)$ 是 $u$ 的直接支配点矛盾！所以，$y$ 只能是 $idom(u)$。但是 $y$ 是 $idom(u)$ 并且 $y$ 位于路径 $p$ 上，与假设路径 $p$ 不经过 $idom(u)$ 矛盾，所以 $idom(u)$ 支配 $w$。\n所以有 $idom(u) \\overset{*}{\\to}idom(w)$，根据上面得到的 $idom(w) \\overset{*}{\\to}idom(u)$，顺利得到 $idom(u)=idom(w)$ 这一结果。$\\blacksquare$\n综上，证毕。\n其实上面的证明也可以这么说，所有从 $s$ 到 $w$ 的路径，都必须经过 $idom(u)$，并且任何比 $idom(u)$ 深的节点，都无法支配 $w$。其实就是证明思路哈哈。\n$$ idom(w) = \\begin{cases} sdom(w) \u0026 \\text{if } sdom(u) = sdom(w) \\\\ idom(u) \u0026 \\text{if } sdom(u) \u003c sdom(w) \\end{cases} $$ 根据这个公式和这个公式\n$$ sdom(u) = min(\\{v\\ |\\ \\exists v \\to u, dfn(v) \u003c dfn(u)\\} \\cup \\{sdom(w)\\ |\\ dfn(w) \u003e dfn(u) \\text{ and } \\exists w \\to \\dots \\to v \\to u\\}) $$我们可以先倒序遍历 dfs 树，求出 sdom 和初步填充 idom，最后再正序遍历把 $sdom(u) \u003c sdom(w)$ 这种情况补上。\n为了求出 $v$ 的直接支配者 $idom(v)$，我们必须在 DFS 树路径 $sdom(v) \\overset{+}{\\to} u \\overset{*}{\\to} v$ 上寻找一个使 $sdom[u]$ 的 DFN 最小的节点 $u$。\n这里存在一个时序冲突，当我们刚求出 $sdom[v]$ 时，逆序 DFN 循环正好执行到节点 $v$。此时，DFS 树路径 $sdom(v) \\overset{+}{\\to} u \\overset{*}{\\to} v$ 上的那些中间节点（由于它们是 $v$ 的祖先，DFN 严格小于 $dfn(v)$），还没有被处理，也没有执行过 link 操作。如果此时直接对 $v$ 调用 eval(v)，并查集无法向上检索到 $sdom(v)$，因为那些树边还没有被加入并查集森林。\n所以我们得想办法把这个操作延迟进行，如下代码的 bucket 就负责了这个功能：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void get(int root) { dfs(root, root); std::iota(sdom.begin(), sdom.end(), 0); LT_Dsu dsu(n, dfn, sdom); for (int i = timer; i \u0026gt;= 2; --i) { int w = vertex[i]; for (auto \u0026amp;v : preds[w]) { int u = dsu.eval(v); if (dfn[sdom[u]] \u0026lt; dfn[sdom[w]]) { sdom[w] = sdom[u]; } // 求 sdom } dsu.link(parent[w], w); bucket[sdom[w]].push_back(w); int p = parent[w]; // 延迟更新 for (auto \u0026amp;u : bucket[p]) { int v = dsu.eval(u); idom[u] = (dfn[sdom[v]] \u0026lt; dfn[sdom[u]]) ? v : p; } bucket[p].clear(); } // 正序更新 idom for (int i = 2; i \u0026lt;= timer; ++i) { int w = vertex[i]; if (idom[w] != sdom[w]) { idom[w] = idom[idom[w]]; } } } 下面是例题 支配树 Luogu P5180 的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;map\u0026gt; #include \u0026lt;numeric\u0026gt; #include \u0026lt;print\u0026gt; #include \u0026lt;vector\u0026gt; struct LT_Dsu { std::vector\u0026lt;int\u0026gt; father; std::vector\u0026lt;int\u0026gt; label; const std::vector\u0026lt;int\u0026gt; \u0026amp;dfn; const std::vector\u0026lt;int\u0026gt; \u0026amp;sdom; void find(int x) { if (x == father[x]) { return; } find(father[x]); if (dfn[sdom[label[father[x]]]] \u0026lt; dfn[sdom[label[x]]]) { label[x] = label[father[x]]; } father[x] = father[father[x]]; } LT_Dsu(int n, const std::vector\u0026lt;int\u0026gt; \u0026amp;_dfn, const std::vector\u0026lt;int\u0026gt; \u0026amp;_sdom) : father(n + 1), label(n + 1), dfn(_dfn), sdom(_sdom) { std::iota(father.begin(), father.end(), 0); std::iota(label.begin(), label.end(), 0); } int eval(int x) { if (x == father[x]) { return label[x]; } find(x); return label[x]; } // parent node, child node void link(int u, int v) { father[v] = u; } }; struct DomTree { int n; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; adj; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; preds; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; bucket; std::vector\u0026lt;int\u0026gt; dfn; std::vector\u0026lt;int\u0026gt; vertex; std::vector\u0026lt;int\u0026gt; parent; std::vector\u0026lt;int\u0026gt; sdom; std::vector\u0026lt;int\u0026gt; idom; int timer; DomTree(int n = 0) : n(n), adj(n + 1), preds(n + 1), bucket(n + 1), dfn(n + 1, -1), vertex(n + 1, 0), parent(n + 1, 0), sdom(n + 1, 0), idom(n + 1, 0), timer(0) {} void add_edge(int u, int v) { adj[u].push_back(v); preds[v].push_back(u); } void dfs(int u, int p) { dfn[u] = ++timer; vertex[timer] = u; parent[u] = p; for (auto \u0026amp;v : adj[u]) { if (dfn[v] == -1) dfs(v, u); } } void get(int root) { dfs(root, root); std::iota(sdom.begin(), sdom.end(), 0); LT_Dsu dsu(n, dfn, sdom); for (int i = timer; i \u0026gt;= 2; --i) { int w = vertex[i]; for (auto \u0026amp;v : preds[w]) { int u = dsu.eval(v); if (dfn[sdom[u]] \u0026lt; dfn[sdom[w]]) { sdom[w] = sdom[u]; } } dsu.link(parent[w], w); bucket[sdom[w]].push_back(w); int p = parent[w]; for (auto \u0026amp;u : bucket[p]) { int v = dsu.eval(u); idom[u] = (dfn[sdom[v]] \u0026lt; dfn[sdom[u]]) ? v : p; } bucket[p].clear(); } for (int i = 2; i \u0026lt;= timer; ++i) { int w = vertex[i]; if (idom[w] != sdom[w]) { idom[w] = idom[idom[w]]; } } } std::vector\u0026lt;int\u0026gt; solve(int root) { get(root); std::vector\u0026lt;int\u0026gt; ans(n + 1, 1); for (int i = timer; i \u0026gt;= 2; --i) { int w = vertex[i]; ans[idom[w]] += ans[w]; } return ans; } }; auto main() -\u0026gt; int { int n, m; std::cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; DomTree solver(n); for (int i = 0; i \u0026lt; m; ++i) { int u, v; std::cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; solver.add_edge(u, v); } auto ans = solver.solve(1); for (int i = 1; i \u0026lt;= n; ++i) { std::print(\u0026#34;{}{}\u0026#34;, ans[i], \u0026#34; \\n\u0026#34;[i == n]); } return 0; } Dominance Frontier 我们构建支配树的最终目的是提升 ir 至 SSA 形式，亦即插入 $\\phi$ 节点。而插入 $\\phi$ 节点这一步骤需要借助支配边界，支配边界可以由支配树快速计算得出。\n如何插入 $\\phi$ 节点和消除 $\\phi$ 节点以及如何基于 SSA 优化并不是本文的重点，这里仅仅介绍支配边界（dominance frontier）这一概念。\n一个节点 $n$ 的支配边界 $DF(n)$ 是满足如下条件的节点 $m$ 集合：\n$n$ 支配 $m$ 的前驱节点。 $n$ 不严格支配 $m$。 很直观对吧（，构建 $DF(n)$ 也很简单，两个 for 循环的事，懒得写了。\nReference oiwiki Luogu 题解 Finding Dominators in Practice 以及 G 指导的大力支持。\n","date":"2026-06-08T03:34:49+08:00","permalink":"https://anfsity.com/p/%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96%E5%85%B6%E4%BA%8C-%E6%94%AF%E9%85%8D%E6%A0%91/","title":"编译优化其二-支配树"},{"content":"HIGH IR Return Insertion 正如字面意思，这个 pass 很简单，就是给所有没有写上 return 指令的函数补上 return 指令。也不需要考虑很多，看见缺了就填，反正后面会进行的 pass 会消除这个 pass 的副作用。\n简单常量传播 在 sysy （c 语言的一个子集） 中，store 语句只会在这几种情况下出现：\nint a = 1 声明语句 a = 2 赋值语句 void foo(int a) 函数声明 load 语句只会在这几种情况下出现：\nint b = a + 1 声明语句 if (a) 条件语句，可以认为循环的 cond 部分也是条件语句 return a 返回语句 foo(a) 函数调用 若在线性顺序下有如下情况：\n1 2 3 4 5 6 7 8 9 int a = 1; int b = a + 1; a = 2; int c = a + 1; foo(a); if (a) { ... } // 跨域分析？ 我们目前只考虑 i32 和 f32 变量，不考虑数组。\n很容易想到，在调用 foo 前的语句，可以把 a 视作常量，这样 b 就可以拿着 a 的值折叠。虽然中间对 a 进行了一次修改，但是我们可以记录这次修改，这样传播到 c 的就是修改后的 a 了。\n在线处理，按照顺序扫描，我们依次插入 a，b，c 到「可以进行常量传播的值集合」。\n由于 sysy 不允许函数传递指针/引用，函数其实是无法修改外部变量的值的。很容易引申出来：函数前记录下来的「可进行常量传播的值集合」不会被 call 函数指令的副作用改变，所以 call 指令对我们的常量传播没什么影响。更加准确的说，call 指令只会影响全局变量。\n进一步，我们只需要注意「可能会修改可以进行常量传播值集合」的指令。在 sysy 中，就是三个指令 call，if，while。\n由于在目前阶段还没有进行 mem2reg，进行活跃变量区间分析比较困难，我采取一个很保守的优化原则。遇到 if，while，则清空当前维护的「常量传播值集合」，遇到 call，则清空「常量传播值集合里面的全局变量」。然后对每一个 op 都尝试进行常量传播即可。顺便也把复制传播做了一下。\n这样的效果应该还不错，因为声明周期很长的变量远少于声明周期短的 tmp 变量。\n简单死代码消除 这个 pass 也比较简单，简述一下流程。\n我们维护一个 liveset 和 worklist。首先标记出不能删除的指令加入到 worklist，他们有：\n外部链接函数 store 指令 ret 指令 控制流终结符 然后进行反向传播（倒序遍历），如果一个指令处于 worklist，那么他自身和其引用的指令也要加入到 liveset 和 worklist，如果一个 if/while 内部有指令在 liveset 中，那么保守起见，整个 if/while 都加入到 liveset 中。反向传播完毕后，就可以清除所有不属于 liveset 中的指令了。这就是 bfs 吧\nInline 优化 Inline 其实是一个非常复杂的优化，在什么时候应该 inline，这是一个 NP hard 的问题，一个函数是否应该 inline，我们需要几个标准来权衡：\ninline 前后是否有性能提升。 在上面这一点，就算 inline 没有显示的性能提升，如果在 inline 后，能够进行更多 pass 来优化，导致最终结果比 inline 前好。 我们是否能够承担 inline 带来的副作用，比如栈开销。 inline 会导致代码体积变大。 \u0026hellip; 不仅仅有上面列出的点，我们还要考虑什么时候进行 inline，以什么顺序，付出多少代价等等。。。是一个非常复杂的问题。\n由于其是一个 NP hard 的问题，我们很难找到一个多项式时间内处理 inline 的算法，通常我们基于启发式算法来处理 inline。\n所以呢，我从 llvm 剽窃 偷学了他们的启发式算法（阉割版）。接下来要做非常复杂的算法了哦~嘿嘿嘿嘿\n参考论文：http://impact.crhc.illinois.edu/shared/papers/p246-chang.pdf\n描述一下宏观流程：\n构建加权调用图 强连通分量分解 内联决策 代码转化 思路是这样的，代码的遍历流程天然和图相符，我们采用图来建模是很自然的想法：将函数视为节点，每个 call 指令视为弧，就能构建出一个有向图。\n这样建模的话，我们可以很轻松的发现递归函数：一个环。在有向图中，环的存在很不好处理，所以我们进行 SCC 缩点，也就是 tarjan 算法。缩点后再跑一遍拓扑排序即可。在 topo 过程中，我们对每个 call 指令计算 cost，再设置一个阈值（threshold），与 cost 比较，来判断是否应该内联，最终依据结果进行内联。内联后再跑一遍 CP 和 DCE。\n思路很简单，难的是如何计算 cost。inline 的 cost 的时刻在变化的，并且他的阈值也不同，举个例子，一个在 while 里面被调用的函数 inline 的性价比明显比在 while 外面的函数 inline 高。为了权衡这些复杂的情况，我们的 cost 的计算也必须是动态的。\n按照 LLVM 的 cost 体系，基础指令/内存访问的 cost 为 5，跳转/分支的 cost 为 10，函数调用的 cost 为 25，昂贵运算 mul/div 等在 10-20 左右，全零初始化的 cost 是 0。\n然后是奖励分，如果能够 SRAO 优化，则减 15-20。如果能够常量折叠，将指令 cost 变为 0（这个很重要，比如一个递归的斐波那契函数 fib(12)，可以不断 inline，常量传播，最终优化成一个具体的数值）。\n我们的初始阈值设为 225，在一定的情况下，我们会对阈值进行调整，比如说在 while 里面的函数，我们采用 $T_{final} = T_{base} + (depth \\times 150)$ 公式 scale up。如果在一个经常为 false 的语句，比如 if (bad situation) { call function; break; } 这种我们就要降低其阈值（$\\div 10$），因为这种内联反而没那么划算。\n最终我们依据 cost \u0026lt; threshold 的结果来判断是否内联。当然存在一些特殊情况，有些很小的函数（指令数 \u0026lt; 10），简单函数包装（包装另一个函数返回结果）我们可以直接内联。还有我们得考虑函数的栈开销，如果全部 inline 了导致爆栈可不太好。\n","date":"2026-05-24T11:56:38+08:00","permalink":"https://anfsity.com/p/%E7%BC%96%E8%AF%91%E4%BC%98%E5%8C%96%E5%85%B6%E4%B8%80/","title":"编译优化其一"},{"content":"来点语言律师题，之前刷 reddit 看到的，作者取了个非常中二的名字（，空闲时间做了把他搬过来了。\n原文仓库: https://github.com/0xd34df00d/you-dont-know-cpp Assigning to references Does this work? If it doesn\u0026rsquo;t, why and what\u0026rsquo;s the easiest fix?\n1 2 3 4 5 6 7 8 9 10 11 constexpr decltype(auto) Get() { static int longLiving = 0; auto\u0026amp; ref = longLiving; return ref; } void DoFoo() { Get() = 42; } What about this one? If this one doesn\u0026rsquo;t, why and what\u0026rsquo;s the easiest fix?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Foo { int a; }; template\u0026lt;int Idx, typename T\u0026gt; constexpr decltype(auto) Get(T\u0026amp; f) { auto\u0026amp; [...fields] = f; // C++26 structured binding introducing a pack return fields... [Idx]; } void DoFoo() { Foo f; Get\u0026lt;0\u0026gt;(f) = 42; } 第一个是对的，第二个是错的，为什么呢？\n根据 decltype 的规定 ，我们可以推导出：\n写法 decltype 推导结果 语法分类 推导出的返回类型 结果 return fields...[Idx]; 规则 1：decltype(entity) id-expression int (传值) 产生 prvalue，报错 return (fields...[Idx]); 规则 2：decltype(expression) lvalue int\u0026amp; (传引用) 产生 lvalue，合法 在不带括号时 fields...[Idx] 是一个 id-expression。其被推导为 decltype(entity) 里的 entity。对于结构化绑定，编译器会直接提取它底层绑定的变量类型。在这里，底层类型是 int。因此，decltype(auto) 将函数返回值推导为 int。函数返回一个 prvalue，你不能对一个 pvalue 赋值。\n而我们加上括号后， 括号强制改变了它的语法属性，使其变成了一个左值表达式，此时编译器触发 decltype(expression) 规则。因为该表达式是一个 lvalue，标准规定 decltype 必须将其推导为引用类型 int\u0026amp;。因此，函数返回了对原始数据的引用，赋值操作完全合法。\n我找到一篇写的很详细的博客 C++ value categories and decltype demystified: https://www.scs.stanford.edu/ ~dm/blog/decltype.html\nDefaulted equality Does this work?\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;compare\u0026gt; // note no operator== struct Foo { int a; std::strong_ordering operator\u0026lt;=\u0026gt;(const Foo\u0026amp;) const = default; }; bool testFoo(Foo f1, Foo f2) { return f1 == f2; } Does this work?\n1 2 3 4 5 6 7 8 9 10 11 12 struct Bar { int a; std::strong_ordering operator\u0026lt;=\u0026gt;(const Bar\u0026amp;) const; }; std::strong_ordering Bar::operator\u0026lt;=\u0026gt;(const Bar\u0026amp;) const = default; bool testBar(Bar b1, Bar b2) { return b1 == b2; } \u0026lt;=\u0026gt; 是 cpp 20 引进的一个新特性 ，他的核心是，比较时不返回 bool，而是返回一个序关系。这是一个很方便的特性，使用它，在重载运算符上就变得十分方便。是我十分喜欢的一个抽象。\n这是一个例子：https://godbolt.org/z/34EjovMca\n在这一节 的 3.2 中，说明：\nIf the member-specification does not explicitly declare any member or friend named operator==, an == operator function is declared implicitly for each three-way comparison operator function defined as defaulted in the member-specification , with the same access and function-definition and in the same class scope as the respective three-way comparison operator function, except that the return type is replaced with bool and the declarator-id is replaced with operator== 机翻：如果成员声明没有显式声明任何名为 operator== 的成员或朋友，则对于在成员声明中定义为默认的三向比较运算符函数，会隐式声明一个 == 运算符函数，具有相同的访问权限和函数定义，并在相应的三向比较运算符函数的相同类作用域中，但返回类型被替换为 bool，声明标识符被替换为 operator==\n在第一个例子中，\u0026lt;=\u0026gt; 被声明为了 default，同时 == 也会被声明。但是在第二个例子中，由于我们没有给出 \u0026lt;=\u0026gt; 的默认实现，编译器不会为我们声明 == 函数，就算我们在后面补充上了 \u0026lt;=\u0026gt; 的实现，== 依旧依赖于我们手动实现。\nSpecialization fun You have this in your header:\n1 2 3 4 5 6 7 8 9 10 11 12 template\u0026lt;typename\u0026gt; constexpr auto IsSimpleContainer = [] { struct Undefined {}; return Undefined {}; } (); template\u0026lt;typename T\u0026gt; constexpr bool IsSimpleContainer\u0026lt;std::vector\u0026lt;T\u0026gt;\u0026gt; = true; // vectors of bools are very special! template\u0026lt;\u0026gt; constexpr bool IsSimpleContainer\u0026lt;std::vector\u0026lt;bool\u0026gt;\u0026gt; = false; template\u0026lt;typename T\u0026gt; constexpr bool IsSimpleContainer\u0026lt;std::deque\u0026lt;T\u0026gt;\u0026gt; = true; How can this bite you?\nIt\u0026rsquo;s alright if only one TU includes this header. But if more than one does, the linker might complain on a CWG 2387-conforming implementation: a fully specialized variable template (the one for std::vector\u0026lt;bool\u0026gt;) is a variable definition, so all the usual variable linkage rules apply.\nThe fix is to add inline to that line only:\n1 2 template\u0026lt;\u0026gt; inline constexpr bool IsSimpleContainer\u0026lt;std::vector\u0026lt;bool\u0026gt;\u0026gt; = false; Also, constexpr doesn\u0026rsquo;t help: unlike constexpr functions, constexpr variables are not implicitly inline.\nBonus points for …\n… immediately thinking \u0026ldquo;unless they are static class data members, of course!\u0026rdquo; when reading the previous sentence.\nrequires-constrained return types 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 template\u0026lt;typename... Args\u0026gt; struct Dummy { int value; void foo() requires(sizeof... (Args) == 0) { } std::optional\u0026lt;Args...\u0026gt; foo() requires(sizeof... (Args) == 1) { return {}; } std::optional\u0026lt;std::tuple\u0026lt;Args...\u0026gt;\u0026gt; foo() requires(sizeof... (Args) \u0026gt; 1) { return {}; } }; int main() { Dummy\u0026lt;\u0026gt; d { 42 }; } Dummy\u0026lt;\u0026gt; does not typecheck. Do you expect it to not typecheck? Why it does not typecheck and how to fix it?\nSolution constraint\nNo, you are not allowed to hide it under auto + deduced type. For the actual type in the actual use case that prompted writing this, the return type then needs to be written in every branch, and it\u0026rsquo;s annoying, and also reduces discoverability of the API.\n看似可以完美的触发 SFINAE，但是不妨注意到，在 std::optional\u0026lt;Args...\u0026gt; foo 中，如果 Args...为空，这里就是一个 hard error，正如编译器给出的 Too few template arguments for class template 'optional'。\n如何修复呢？\n我们可以用一个基类 DummyBase 来包装，再根据包的大小特化 Dummy ，如 https://godbolt.org/z/vdPsojqv3 我们也可以用一个辅助类型来包装，确保 optional 里面永远不为空，就像这样：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 template\u0026lt;typename... Ts\u0026gt; struct OptionalReturn { using type = void; }; template\u0026lt;typename T\u0026gt; struct OptionalReturn\u0026lt;T\u0026gt; { using type = std::optional\u0026lt;T\u0026gt;; }; template\u0026lt;typename... Ts\u0026gt; requires (sizeof...(Ts) \u0026gt; 1) struct OptionalReturn\u0026lt;Ts...\u0026gt; { using type = std::optional\u0026lt;std::tuple\u0026lt;Ts...\u0026gt;\u0026gt;; }; template\u0026lt;typename... Args\u0026gt; struct Dummy { int value; // 此时即使 Args 为空，OptionalReturn\u0026lt;Args...\u0026gt;::type 也只是一个类型定义 // 不会触发 std::optional 的错误展开 typename OptionalReturn\u0026lt;Args...\u0026gt;::type foo(); }; Bonus question\nSome usual approaches don\u0026rsquo;t work:\nMaking foo itself a template with a default template parameter, like 1 2 3 template\u0026lt;typename... MyArgs = Args...\u0026gt; requires(sizeof...(MyArgs) == 1) std::optional\u0026lt;MyArgs...\u0026gt; foo() is not well-formed since packs can\u0026rsquo;t have default values. Using something like template\u0026lt;typename T = std::tuple_element_t\u0026lt;0, std::tuple\u0026lt;Args...\u0026gt;\u0026gt; and then having std::optional\u0026lt;T\u0026gt; in the \u0026ldquo;unary\u0026rdquo; foo() case: std::tuple_element_t hard-errors on out-of-bounds index instead of merely being SFINAEd away. A C++26 variation of that with pack indexing with template\u0026lt;typename T = Args...[0]\u0026gt;: out-of-bounds in pack indexing is also somehow a hard error instead of being SFINAEd away. Given this, what can you say about orthogonality and well-thought-ness of C++?\nhhh, 我不好说。\nIs this valid? 1 2 3 4 5 6 7 8 9 struct Foo { struct Nested { bool field = true; }; void doSmth(const Nested\u0026amp; = Nested{}); }; Answer: see this bugzilla entry .\n为了让我们在类成员函数中使用后面才会定义的变量，编译器不会立即处理两种内容，直到整个类结束后才会构造，这里第一是类成员变量的初始值，第二是函数的默认构造函数。\n在这个代码中，由于 doSmth 还在类中，我们 Nested 不存在构造函数，一旦我们写下 const Nested\u0026amp; = Nested{}，就代表我们需要 Nested 有构造函数。这是什么，循环依赖了，万测尽，悲。\n理解了原理，似乎我们也很好修改，只需要给 Nested 手动添加一个构造函数就行了。但是，如下代码依然会有错误：\n1 2 3 4 5 6 7 8 9 10 struct Foo { struct Nested { bool field = true; Nested() = default; }; void doSmth(const Nested\u0026amp; = Nested{}); }; 因为将构造函数声明为 default 和直接表明我们这个 Nested 类有构造函数 Nested() {}; 是不一样的，default 不代表 Nested 类一定有构造函数，这取决于类的实现。\n值得一提的是，msvc，会编译成功，哈哈哈哈。\nSome covariance (协变) Is this valid?\n1 2 3 4 5 6 7 8 9 struct Base { virtual Base* getFoo() { return nullptr; } }; struct Derived : Base { Derived* getFoo() override { return nullptr; } }; Sure: this is covariance in action.\nWhat about this?\n1 2 3 4 5 6 7 8 9 struct Base { virtual const Base* getFoo() { return nullptr; } }; struct Derived : Base { Base* getFoo() override { return nullptr; } }; Yep, also good: in some sense, Base* is a subtype of const Base*. And, of course, Derived* would\u0026rsquo;ve worked too.\nNow, this is surely valid too, right?\n1 2 3 4 5 6 7 8 9 struct Base { virtual const int* getFoo() { return 0; } }; struct Derived : Base { int* getFoo() override { return 0; } }; Nope: non-class types play by different rules, because otherwise the language would\u0026rsquo;ve been too consistent (see https://eel.is/c++draft/class.virtual #8).\n那么问题来了，我们要怎么正确是实现呢？\n一个显然的思路是包装一下我们的 int，但这样不够通用而且太繁琐了。\n额，如果忽视 runtime 环境的话，用 CRTP 可以很好的解决这个问题：\n1 2 3 4 5 6 7 8 template \u0026lt;typename Derived, typename T\u0026gt; struct Base { T *getFoo() { return static_cast\u0026lt;Derived *\u0026gt;(this)-\u0026gt;getFooImpl(); } }; struct Derived : Base\u0026lt;Derived, int\u0026gt; { int *getFooImpl() { return \u0026amp;data; } int data; }; 还有别的方法吗？我们也可以这么处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct Base { virtual const int *getFoo() const { return getFooImpl(); } protected: virtual const int *getFooImpl() const { return nullptr; } }; struct Derived : Base { using Base::getFoo; int *getFoo() { return const_cast\u0026lt;int *\u0026gt;(getFooImpl()); } protected: const int *getFooImpl() const override { return nullptr; } }; 这么写显然很丑陋。。\nconstexpr string literals Does this compile?\n1 2 3 4 5 constexpr auto f() { return \u0026#34;f\u0026#34;; } constexpr auto g() { return \u0026#34;g\u0026#34;; } static_assert(f() == f()); static_assert(f() != g()); Here the answer is easy: it\u0026rsquo;s an open question, discussed in CWG #2765 .\n可能会有 Static assertion expression is not an integral constant expression ，因为在比较字符串地址。\n如果要比较，请不要使用 auto。\nWhen is this function safe or unsafe to use? 1 2 template\u0026lt;auto V\u0026gt; const auto\u0026amp; foo() { return V; } u1s1, 有点幻视：\n1 2 auto\u0026lt;auto auto\u0026gt; auto auto\u0026amp; auto() { auto auto; } It\u0026rsquo;s safe for class types and unsafe for, say, ints. For some reason the standard threats them differently, so\n1 2 3 4 const auto\u0026amp; v1 = foo\u0026lt;42\u0026gt;(); // bad! dangling reference struct S { int val; }; const auto\u0026amp; v2 = foo\u0026lt;S { 42 }\u0026gt;(); // fine! Finding the corresponding clauses in the standard is left as an exercise for the reader.\nMaps of non-copyable, non-movable types Suppose you have a type that\u0026rsquo;s not copyable nor movable, like\n1 2 3 4 5 struct ThreadedResource { std::unique_ptr\u0026lt;Resource\u0026gt; handle; std::mutex mutex; // mutex isn\u0026#39;t move-constructible nor move-assignable nor copyable }; 假设你需要一个把从 int 映射到 ThreadedResource 的 hashmap。一个方法是使用 shared_ptr 来包装一下 ThreadedResouce。如 std::unoredred_map\u0026lt;int, std::shared_ptr\u0026lt;ThreadedResource\u0026gt;\u0026gt;。空指针表示这里没有映射。\n这很麻烦（？）因为它会有额外的内存开销和访问，导致了性能下降。\n你能做的更好吗？\n一个可能的答案是使用 std::optional,它可以更清晰的表达 \u0026ldquo;no value\u0026rdquo;。\n因为对象不能移动，我们得使用分段构造 piecewise_construct：\n1 2 3 4 5 6 7 auto handle = ...; map.emplace(std::piecewise_construct, // 将 key 和 value 分开构造 // forward as tuple：将构造函数需呀哦的参数打包成元组 std::forward_as_tuple(locale), // key std::forward_as_tuple( std::in_place, // 就地构造 std::move(handle)) 请注意，ThreadedResource 字段的顺序是 mutex 在 handle 之后，\n因此无需向 mutex 传递初始化器，一切都能正常运行。\nIncrementing enums Is this valid?\n1 2 3 4 5 6 enum E { A, B }; E\u0026amp; operator++(E\u0026amp; e) { // some implementation } 虽然我们不能对枚举自增，但是我们可以重载枚举的 ++ 运算符。\n比如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include \u0026lt;iostream\u0026gt; #include \u0026lt;cassert\u0026gt; enum Status { ready, // 0 running, // 1 stopped // 2 }; // 重载前置 ++ Status\u0026amp; operator++(Status\u0026amp; s) { if (s == stopped) { s = ready; } else { s = static_cast\u0026lt;Status\u0026gt;(static_cast\u0026lt;int\u0026gt;(s) + 1); } return s; } int main() { Status myStatus = ready; ++myStatus; // 现在 myStatus 变成了 running assert(myStatus == running); return 0; } operator new Is this valid?\n1 2 3 4 5 6 7 8 9 struct Bar { int n; void* operator new(size_t sz) { return ::operator new(sz + n); } }; How about this?\n1 2 3 4 5 6 7 struct Bar { virtual void* operator new(size_t sz) { return ::operator new(sz); } }; 显然不对，new 操作符是隐式 static 的，而此时 n 还没有创建完成。既然对象不存在，哪里来的分配内存？对于虚函数来说，此时同理没有 vtbl。\nHow are these two functions different? 1 2 3 4 5 template\u0026lt;typename T\u0026gt; T mkT1() { return {}; } template\u0026lt;typename T\u0026gt; T mkT2() { return T {}; } mkT1 是复制列表初始化 ，mkT2 是直接列表初始化。\n他们直接的区别在于对 explicit 构造函数的处理。T1 函数不允许调用标记为 explicit 的构造函数。\n并且在 c++ 17 之前，T{} 会创建一个临时对象，这导致 std::mutex 这种无法使用。\nIs this code valid? 1 2 3 4 5 6 7 8 9 10 11 12 13 struct Foo { int a; Foo() = delete; Foo(int) = delete; }; int main() { Foo foo1 {}; Foo foo2 { 10 }; } 这同样涉及到 cpp 的初始化，在 c++20 之前，一个类被视为聚合体，如果：\n没有用户提供的构造函数 没有私有或保护的非静态数据成员 没有基类，没有虚函数 在这里，Foo() = delete 属于用户声明，但不属于用户提供。所以他被视为聚合体。在聚合初始化中，编译器会绕过构造函数，直接给成员赋值而不需要构造函数。\n这就导致了上面这种看起来很不合理的代码在 c++17 标准下可以编译通过。\nbtw， c++20 修改了聚合体的定义，以上代码无法在 c++20 以上编译。\n(^=\u0026hellip;=^) What does this code do, and on what features of C++17 does it rely?\n1 2 3 4 5 6 template\u0026lt;typename F, typename... Ts\u0026gt; void foo(F f, Ts... ts) { int _; (_ = ... = (f(ts), 0)); } 这个标题幻视我们的反射 [:O_o:]\n这个代码很难读，实际上 cpp 模板里面有很多这样很难读的代码。。。。。。\n它依赖了变长参数模板，折叠表达式，赋值运算符的评估顺序。简单的来说，如果我们调用 foo(func, 1, 2, 3) 其会依次调用 func(3) func(2) func(1)。\n我们详细讲解一下。\n假设我们调用 foo(f, t1, t2) ，(_ = ... = (f(ts), 0)) 是一个二元左折叠。其结构 (Init op ... op Pack)。\n展开后，看起来像这样： ((_ = (f(t1), 0)) = (f(t2), 0))。根据 c++17 标准，在表达式 A = B 中，B 在 A 先执行。所以先执行 t2 然后再 t1。\n这个代码有什么问题呢？当我们偷偷重载了 operator, 或 operator= 的时候，就有可能有问题。\n当然，还有一个最大的问题，就是可读性太差太差了。。。\nConceptual concepts Assume the following declarations:\n1 2 3 4 5 6 7 8 9 10 template \u0026lt;typename T\u0026gt; concept Trivial = std::is_trivial_v\u0026lt;T\u0026gt;; template \u0026lt;typename T, typename U\u0026gt; requires Trivial\u0026lt;T\u0026gt; void f(T t, U u) { std::cout \u0026lt;\u0026lt; 1; } template \u0026lt;typename T, typename U\u0026gt; requires Trivial\u0026lt;T\u0026gt; \u0026amp;\u0026amp; Trivial\u0026lt;U\u0026gt; void f(T t, U u) { std::cout \u0026lt;\u0026lt; 2; } Is f(1, 2) valid? If yes, what would it print?\nWhat if Trivial\u0026lt;T\u0026gt; \u0026amp;\u0026amp; Trivial\u0026lt;U\u0026gt; is replaced by Trivial\u0026lt;T\u0026gt; \u0026amp;\u0026amp; Trivial\u0026lt;T\u0026gt; in the second definition?\nWhat about Trivial\u0026lt;T\u0026gt; || Trivial\u0026lt;U\u0026gt;?\nWhat if the definition of Trivial gets \u0026ldquo;inlined\u0026rdquo;, replacing all Trivial\u0026lt;T\u0026gt;s with sd::is_trivial_v\u0026lt;T\u0026gt;?\n答案是 2,\n1 2 3 4 5 Call to \u0026#39;f\u0026#39; is ambiguousclang(ovl_ambiguous_call) foo.cpp(10, 6): Candidate function [with T = int, U = int] foo.cpp(16, 6): Candidate function [with T = int, U = int] 1 和 ambiguous。\nFun with fun templates What does bar1 print?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 template\u0026lt;typename T\u0026gt; int foo(T) { return 1; } template\u0026lt;\u0026gt; int foo(int*) { return 2; } template\u0026lt;typename T\u0026gt; int foo(T*) { return 3; } void bar1() { int test; std::cout \u0026lt;\u0026lt; foo(\u0026amp;test) \u0026lt;\u0026lt; foo\u0026lt;int\u0026gt;(\u0026amp;test) \u0026lt;\u0026lt; foo\u0026lt;int*\u0026gt;(\u0026amp;test) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } What if we reorder the definitions, as in bar2?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 template\u0026lt;typename T\u0026gt; int foo(T) { return 1; } template\u0026lt;typename T\u0026gt; int foo(T*) { return 3; } template\u0026lt;\u0026gt; int foo(int*) { return 2; } void bar2() { int test; std::cout \u0026lt;\u0026lt; foo(\u0026amp;test) \u0026lt;\u0026lt; foo\u0026lt;int\u0026gt;(\u0026amp;test) \u0026lt;\u0026lt; foo\u0026lt;int*\u0026gt;(\u0026amp;test) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } Can we still specialize the first template after we\u0026rsquo;ve introduced the second one?\nYep:\n1 2 template\u0026lt;\u0026gt; int foo\u0026lt;int*\u0026gt;(int*) { return 2; } 把这个和上面那个放在一起来讲吧。当初学模板元做的笔记：\n在类模板的特化过程中，编译器会先将模板转化成函数模板，借助函数模板重载来决定优先级。\n函数模板偏序规则：\n如果模板 A 能够处理的所有情况，模板 B 都能处理。而模板 B 能处理的情况，模板 A 未必能处理。则模板 A 比 B 更特化。\n文章的意思大概是：编译器捏造一个类型 U，用类型 U 带入模板 A，生成一个具体的函数签名。 然后用这个函数签名，去试图匹配模板 B 如果能匹配，则说明 A 比 B 特化。反过来做一遍，就能够比较 A 和 B 的特化程度了。\n1 2 template \u0026lt;typename T\u0026gt; void foo(T); // #1 template \u0026lt;typename T\u0026gt; void foo(T *); // #2 如果我们要比较 #1 和 #2 的特化程度，首先，尝试 #2 带入 #1 ，我们用一个模板实参 U（比如说 int） 带入 #2。也就是 template \u0026lt;typename T = U\u0026gt; void foo(U *); （foo(int *)）然后尝试 U* 带入 #1，也就是 template \u0026lt;typename T\u0026gt; void foo(U *)（可以想象成 foo(int *) 去匹配 #1）此时，#1 的 T 可以被推导为 U*。\n然后，我们再尝试用 #1 带入 #2，同理用一个模板实参 U 带入 #1 template \u0026lt;typename T = U\u0026gt; void foo(U); 去匹配 #2，得到 T* = U -\u0026gt; 失败\n综上得出：#2 的特化程度比 #1 高。\n函数模版既可以重载，又可以全特化，函数模板的每一个重载都是主模板。实例化过程中，先进行重载决议，然后再特化匹配。也就是说，在重载决议阶段，只考虑主模板，不考虑主模板的全特化。选择主模板后，才进行特化匹配。这样的规则会导致：如果模板特化的位置不同，最终匹配到的模板也有可能不同。所以我们不应该使用函数模板全特化，而是使用函数重载。\n放在这里就是：\n下面的解析是 AIGC。\n1 2 3 template\u0026lt;typename T\u0026gt; int foo(T) { return 1; } // #1 (主模板) template\u0026lt;\u0026gt; int foo(int*) { return 2; } // #1 的特化 (因为此时只有 #1 可见) template\u0026lt;typename T\u0026gt; int foo(T*) { return 3; } // #2 (另一个主模板) foo(\u0026amp;test)： 主模板 #1 (T=int*) 和 #2 (T=int) 都在候选名单中。 根据偏序规则，#2 比 #1 更特化（$T*$ 优于 $T$）。 选择 #2。由于 #2 在此处没有特化版本，返回 3。 foo\u0026lt;int\u0026gt;(\u0026amp;test)： 指定 T=int。只有 #2 匹配（foo(int*)）。返回 3。 foo\u0026lt;int*\u0026gt;(\u0026amp;test)： 指定 T=int*。 #1 变为 foo(int*)，匹配。 #2 变为 foo(int**)，不匹配。 选择 #1。检查 #1 的特化，发现 foo(int*)，返回 2。 结果：332\n1 2 3 template\u0026lt;typename T\u0026gt; int foo(T) { return 1; } // #1 template\u0026lt;typename T\u0026gt; int foo(T*) { return 3; } // #2 template\u0026lt;\u0026gt; int foo(int*) { return 2; } // #2 的特化 (因为 #2 比 #1 更特化) 注意：这里的全特化 template\u0026lt;\u0026gt; int foo(int*) 会关联到当前最匹配的主模板，即 #2。\nfoo(\u0026amp;test)： 选择主模板 #2。检查其特化，发现 foo(int*)。返回 2。 foo\u0026lt;int\u0026gt;(\u0026amp;test)： 选择主模板 #2。检查其特化，发现 foo(int*)。返回 2。 foo\u0026lt;int*\u0026gt;(\u0026amp;test)： #1 匹配，#2 不匹配。 选择 #1。#1 在此处没有特化。返回 1。 结果：221\nI C memset Assume an instance of a struct is memseted to zeroes. What would be the value of the padding?\nFurther assume a field of that structure is updated. What would be the value of the padding after that field? After other fields?\n都是未指定的。\n为了更加直观的了解这个，我们来看看其内存模型。\n假设有这么一个类：\n1 2 3 4 5 6 7 struct Sample { char a; // 1 byte // Padding: 3 bytes int b; // 4 bytes char c; // 1 byte // Padding: 3 bytes }; 在我们 memset 完后：\n如图，无论是执行完 s.a = 'x'; 前后，填充位的值都不可靠。为什么呢，希腊奶。但既然标准这么规定了，就不要依赖这种行为（真不会依赖吗？）\nanyway，在我的电脑上：\nIs this code valid? 1 2 char arr[5] = { 0 }; auto pastEnd = arr + 10; What about this one?\n1 2 char arr[5] = { 0 }; auto pastEnd = arr + 5; 第一个不合法，第二个合法。但是注意不要这么做\n1 2 3 char arr[5]; char *pastEnd = arr + 5; char value = *pastEnd; // ub Which lines are UB, if any? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include \u0026lt;iostream\u0026gt; struct Foo1 { int a; }; struct Foo2 { int a; Foo2() = default; }; struct Foo3 { int a; Foo3(); }; Foo3::Foo3() = default; int main() { Foo1 foo11, foo12 {}; Foo2 foo21, foo22 {}; Foo3 foo31, foo32 {}; std::cout \u0026lt;\u0026lt; foo11.a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; foo12.a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; foo21.a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; foo22.a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; foo31.a \u0026lt;\u0026lt; std::endl; std::cout \u0026lt;\u0026lt; foo32.a \u0026lt;\u0026lt; std::endl; } 找了个稍微老一点的 gcc 版本 https://godbolt.org/z/bTE77GEs3 可以观察到 11 21 31 32 全是 ub。\n11 12 13 是 ub，这很显然。但 32 为什么是呢？\n涉及到 user-provided https://eel.is/c++draft/dcl.fct.def.default 。\n根据 c 嘎嘎标准，在类外提供构造函数，这被视为 user-provided 的构造函数，对于 user-provided 的构造函数来说，编译器直接调用该构造函数，不再进行额外的零初始化。\n所以，如果类外实现构造函数，最好手动初始化所有成员。\nIs this code valid? 1 2 3 4 5 6 7 struct X { int a, b; }; X *make_x() { X *p = (X*)malloc(sizeof(struct X)); p-\u0026gt;a = 1; p-\u0026gt;b = 2; return p; } Depends on the C++ version, and whether it is C++ to begin with.\nUp until C++17, neither an x object nor an int subobjects are created, and this code is UB.\nStarting with C++20, an x object and its int subobjects are implicitly created, and this code is valid.\nIt always has been valid C code, though.\n我以为这是对的（毕竟我只在 c 里面写过这样的代码。\n在 c++20 之前，malloc 并不创建对象。访问 p-\u0026gt;a 时，该内存地址并没有一个真正的 X 对象存在.对象必须使用 new 显示创建。\n如果要正确的写的话：\n1 2 3 4 5 6 7 8 9 10 11 12 13 *make_x() { // Allocate raw memory void *mem = std::malloc(sizeof(X)); if (!mem) return nullptr; // Use placement new to start the lifetime of X at that address. X *p = new (mem) X; p-\u0026gt;a = 1; p-\u0026gt;b = 2; return p; } Is using this function dangerous? 1 2 3 4 auto foo1() { return \u0026#34;Gotta love C++\u0026#34;; } What about this one?\n1 2 3 4 5 auto foo2() { const char *str = \u0026#34;Gotta love C++\u0026#34;; return str; } This one?\n1 2 3 4 5 auto foo3() { const char str[] = \u0026#34;Gotta love C++\u0026#34;; return str; } Nope, nope, yep.\nWhy? What\u0026rsquo;s the crucial difference between these functions? Is there any difference in their types?\nAccessing any element of the \u0026ldquo;array\u0026rdquo; returned by foo1 and foo2 is fine. Try doing that to foo3 and you\u0026rsquo;ll get an UB, since you\u0026rsquo;ll be using an object whose lifetime has ended!\nfoo1 and foo2 return a pointer to a string that is, roughly speaking, allocated and stored somewhere in the executable at compile time.\nThe pointer returned by foo3 references the local array str which is initialized by copying that same string. This array is local to foo3 and its lifetime ends once the function has returned, hence the UB.\nWhile modern compilers output a warning, what\u0026rsquo;s a reliable and somewhat general way to check functions like this?\nMark all these functions constexpr and try using them in a constant evaluated context, like static_assert:\n1 2 3 static_assert(foo1()[0] == \u0026#39;G\u0026#39;); static_assert(foo2()[0] == \u0026#39;G\u0026#39;); static_assert(foo3()[0] == \u0026#39;G\u0026#39;); Say, clang outputs:\n1 2 3 4 5 6 7 8 9 error: non-constant condition for static assertion 22 | static_assert(foo3()[0] == \u0026#39;G\u0026#39;); | ~~~~~~~~~~^~~~~~ error: accessing \u0026#39;str\u0026#39; outside its lifetime 22 | static_assert(foo3()[0] == \u0026#39;G\u0026#39;); | ~~~~~~~~^ note: declared here 16 | const char str[] = \u0026#34;Gotta love C++\u0026#34;; | ^~~ What does this print? 1 2 3 4 5 6 7 8 9 struct Evil { auto begin() { return std::counted_iterator(\u0026#34;Library\u0026#34;, 7); } friend auto begin(Evil\u0026amp;) { return std::counted_iterator(\u0026#34;Core\u0026#34;, 4); } friend auto end(Evil\u0026amp;) { return std::default_sentinel; } }; Evil rg; for (char c : rg) { putchar(c); } std::ranges::for_each(rg, [](char c) { putchar(c); }); borrowed from Arthur O\u0026rsquo;Dwyer\u0026rsquo;s blog, where he also considers this in more detail\n输出是 CoreLibrary。\n这个应该是一个老生长谈的问题了，不过这里展示的比较隐秘，一般是用 std::swap 来讲解 ADL 和 CPO 的。\nCPO 和 tag invoke 是现代 cpp 比较重要的特性，ranges 里面大量使用 cpo 进行实现。\nAre these functions different? 1 2 3 4 5 6 7 8 9 int f() { int x = 0; return *(\u0026amp;x - 1 + 1); } int g() { int x = 0; return *(\u0026amp;x + 1 - 1); } f() 会是好函数吗？（\n事实上，f 是 ub， 而 g 没有问题。\nborrowed from Daniil Zhuravlev\u0026rsquo;s blog, where he explains what language in the C++ Standard makes it UB and shows a proof that function f is fishy\nConclusion 我真是疯了，把这个写完了。你如果问我：“我把这些都学会了，我能变成 cpp 大佬吗”？我觉得不行，毕竟揪着偏门语言特性不放大概率是魔怔人。\n累了，放张图。\n","date":"2026-04-27T16:24:07+08:00","permalink":"https://anfsity.com/p/you-dont-know-cpp-and-neither-i-do/","title":"You Dont Know Cpp and Neither I Do"},{"content":"稍微填一下坑，最近重新写了一遍，感悟又有所不同。\n啊嘞啊嘞嘞，怎么直接从准备到完结了\n按照教程的顺序，应该是先写前端解析出 AST 然后再遍历出 koopa ir 最后进行代码生成。流程很短，事实上少了优化的过程，确实非常简单。\n在函数之前的内容就不赘述了，教程写的很完善，实际也不难。由于不需要考虑浮点数和整数之间的转换，中期的短路求值其实很好写，写一个 if-else 就解决了。\n在写 while 和 if 时，需要注意处理 break 和 continue 语句，我们可以根据代码的层级嵌套关系，使用栈来维护一个 scope 栈来管理基本块，进入 basic block 就 push 进去，离开 bb 就 pop。在 sysy 中，只有这四种情况会产生 scope：\nfunction 内部是一个 scope if 内部是一个 scope，else 内部是一个基本块 while 内部是一个 scope 空的 {} 是一个 scope 我们在全局用一个栈来维护这种 scope 之间的嵌套关系，然后再 visit scope 内部的基本块，比直接在基本块中处理 break 和 continue 要好些的多。可能说的比较抽象。\n在写函数和全局变量时，教程中介绍了寄存器分配：完全不分配。这个部分比较有意思，可以看看 R 大的回答：https://www.zhihu.com/question/29355187/answer/51935409\n工业上一般采用改进后的线性扫描算法，比如 llvm 就是基于变量权重使用优先队列来进行遍历的。\n在我体感中，数组的初始化和访问是教程中最复杂的部分。sysy 数组的初始化和 c 标准相同。\n由于教程中写的不是很详细，我想仔细讲讲这个部分。\n举个例子，这个初始化 int a[2][2] = {1, {1, 2}} 是正确的吗？\n这个 int a[2][2] = {{1}, {1, 2}} 呢？\n下面这个更加复杂的呢？\n1 2 3 4 5 6 7 8 9 10 int arr[2][3][4] = { {1, 2, 3, 4}, {5}, {6}, { {1, 2, 3, 4}, {1, 2, 3, 4}, {1, 2, 3, 4} } }; 以及教程中这个 int arr[2][3][4] = {1, 2, 3, 4, {5}, {6}, {7, 8}}; 例子呢？\n一步一步来，事实上，一个 {} 就对应一个数组：int a[2] = {1, 2}; 中，{} 对应 int[]；int a[2] = {{1, 2}, {1, 2}}; 中，最外面的 {} 对应着 int[][]，里面的 {} 对应着 int[]。\n一个好的数组初始化，它 {} 的嵌套关系和数组维度应该是严格对应的。但数组初始化没有这么简单。如果我没有写 {} 呢？多写了一个 {} 呢，又或者我没有填充满呢？\n但事实上，c 标准没有严格要求一一对应，比如 int x = {4}; 是正常写法，可以编译通过的，这就引申出一条非常重要的规则：「当 {} 高于当前对应的数组维度时，把这个大括号当作对当前单个标量的初始化」。也就是说，int x = {4} 和正常的 int x = 4; 没有区别，但是 int x = {1, 2}; 就不被允许。int x = {{4}} 也不行，因为外面的 {} 对齐到了标量，内部的 {4} 完全没有坑给他。\n而另一方面，如果我少写了 {}，比如 int x[2][2] = {1, 2, 3, 4}，他也会正确初始化成 int x[2][2] = {{1, 2}, {3, 4}}。这个规则可以总结成为：「如果缺少大括号，会按照平坦的内存顺序，从 [0][0] 开始填充」。\n这个例子则可以说明缺少 {} 时的另一个规则 int x[2][2] = {1, 2, {3, 4}}。「当遇到一个左大括号时，编译器会贪心的将这个子数组内容对齐到当前维度下最适合的子维度」。结合上面刚提到的规则，1, 2 填充给了 [0][0] 和 [0][1]。{3, 4} 则刚好适合 x[1]。\n这两个规则同样也可以解释这个例子 int a[2][3][4] = {1, 2, 3, 4, {5}}。1, 2, 3, 4 刚好填充 int a[0][0]。这时的 {5} 则适合对应 int a[0][1]。最终结果就是 int a[2][3][4] = {{{1, 2, 3, 4}, {5, 0, 0, 0}, ...}, ...}。\n这个例子则很好的结合了前面提到的所有规则 int x[2][2] = {1, {1, 2}}。首先 1 会被填充到 [0][0] 位置处，然后遇到了一个 {。现在的问题是，由于只填充了 [0][0], {1, 2} 没有办法给 x[1]，只能给 x[0][1]。则就是前面提到过的 int a = {1, 2} 的错误。这个初始化是不正确的。而 int x[2][2] = {1, {5}, {1, 2}} 则是正确的。\n到了这里，很多东西都能解释的比较通顺了，但你可能会产生疑问 int x[2][2] = {1, {1, 2}} 为什么不能解释成 int x[2][2] = {{1, 0}, {1, 2}} 呢？这就要揭示我们之前隐含的一个东西，我们实际上是在维护一个指针从头开始扫的，这个指针在的维度就是 {} 要贪心填充的维度的父维度。同样是这个例子 int x[2][2] = {1, {1, 2}}。我们是维护一个指针 p 填充，当 1 填充给了 [0][0] 后，这个指针是指向 [0][1] 的，所以 {1, 2} 只能填充给 [0][1]。而这个例子中 int a[2][3][4] = {1, 2, 3, 4, {5}} 中，填充完 1, 2, 3, 4 后，指针是在 [0][1] 这里的，所以 {5} 才能填充给 [0][1]。\n当然，还有一个补零的规则我没有说，但这个其实很显然的。总结一下，我们总共有四条规则：\n线性填充，如果你不使用内部的大括号，那么会按照平坦的顺序从 [0][0][0] 开始填充。 子对象对齐，当遇到一个左大括号 { 时，他会尝试将这个 { 对应的数组填充到当前指针指向的位置。如果当前指针恰好是一个数组的开头位置（对齐），那么这个 { 就负责初始化这整个子数组，如果不是，则是下面的规则。填充这个子数组的规则同样是这四条。 标量括号限制，当你在非对齐的位置遇到了左大括号 {，他不能把这个 { 给子数组，因为没有对齐。他只能把这个 { 给到当前的标量 int/float。 自动补零，只要当前的数组被部分初始化了，所有未显示指定的元素都会被初始化成 0 emmm，就是这样，也许我讲的不是很清楚，我自认为还算清楚。写法是一个递归的过程，过程就和我叙述的一样，当然有一些细节需要处理。\n好啦，最难的部分就是这样，如果你还想继续研究编译器，可以看看 MaxXing 后面的进阶篇。当然，那些内容还不足以说明编译器的广度和深度，但也是一个很好的入门部分呢。\n","date":"2026-02-24T13:12:21+08:00","permalink":"https://anfsity.com/p/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%AE%8C%E7%BB%93%E7%AF%87/","title":"编译原理完结篇"},{"content":"在 C 和 C++ 中，有一个叫做 ub (undefined behavior) 的概念，你也许听说过他，视他为洪水猛兽。但打败敌人的第一步往往需要先了解他，那么首先， ub 是什么呢？\nIntroduction 标准 1上面给出的定义是这样的：\n未定义行为，即标准对该程序运行结果不施加任何要求，其运行可以为任何结果，也无需保持一致或给出诊断。换句话说，如果你运行了一个包含未定义行为的程序，那么下一刻，你的程序可能会崩溃，你的内存可能会爆炸，世界有可能被外星人入侵。因为其行为是“未定义的”，所以你无法否认：上面的事件都有可能发生。\n如果你还没有认识到 ub 存在的危险性，我们先来看一个经典的代码：\n1 2 3 4 5 6 void contains_null_check(int *P) { int dead = *P; if (P == 0) return; *P = 4; } 这个代码看起来人畜无害，但实际上他可能导致外星人入侵。\n1 2 3 4 5 6 void contains_null_check(int *P) { int dead = *P; // 动作 A: 解引用 P (读取值) if (P == 0) // 动作 B: 检查 P 是否为空 return; *P = 4; // 动作 C: 再次使用 P (写入值) } 在进行 int dead = *p 这个语句时，如果 p == nullptr ，这就是 ub ，一旦 ub 发生，之后的任何行为都不再受标准约束，因此编译器可以把后面的 null check 当作“在所有定义良好的执行路径上都不可能为真”来推理并删掉。当然，编译器也可能在发生 ub 后生成向宇宙广播的代码。\n1 2 3 4 void contains_null_check(int *P) { int dead = *P; // 动作 A: 解引用 P (读取值) *P = 4; // 动作 C: 再次使用 P (写入值) } 然后再进行死代码消除（DCE）：\n事实上，在 gcc 之前的版本，gcc （-O2）不会产生这样的汇编，但是 gcc 15.2 产生了。\n这是 Evil 啊~（激动）。\n当然，编译器也有可能先进行死代码消除，这导致程序会进行空指针检查：\n1 2 3 4 5 void contains_null_check(int *P) { if (P == 0) // 动作 B: 检查 P 是否为空 return; *P = 4; // 动作 C: 再次使用 P (写入值) } ub 最可怕的不是“会崩”，而是它让编译器获得了更强的推理权：一旦某条路径触发 ub，优化器就可以把这条路径当作不存在，从而重排、删除你以为必要的逻辑。\n更可怕的是，这样的代码放不胜防，因为同样的代码，在 clang 或 gcc 下都有可能正常编译运行，也有可能发生：当编译器升级后，原来跑的好好的程序突然崩溃了。\n目前 clang，gcc 对 cpp 一致性支持比较好，至于 msvc，它太坏了。\n哪怕是世界上最聪明的程序员，也无法百分百肯定自己不会写出 ub 的代码。\n行为类别 事实上，C++ 标准不仅定义了什么是 UB，还构建了一套完整的行为类别（Behavior Categories）。为了搞清楚 UB 的边界，我们需要先理清以下几个标准术语 。\n术语 Who decides? 编译器行为 确定性 经典案例 后果 Well-defined Standard 必须严格生成对应代码 100% 确定 std::vector\u0026lt;int\u0026gt; v; 程序按预期运行，全平台一致。 Ill-formed Standard 必须报错 (Diagnostic required) N/A void f(int) { return \u0026quot;s\u0026quot;; } 编译失败。 IFNDR (Ill-formed, no diagnostic) Standard 无义务 不确定 违反 ODR 原则；某些模板特化错误 链接错误，或者运行时莫名其妙崩溃。 Implementation-defined Compiler 必须在文档中说明 确定 (对特定编译器) sizeof(long)；FILE* 的底层类型 代码不可移植，但在特定环境下行为稳定。 Unspecified Compiler 无需说明，甚至无需一致 每次编译可能不同 foo(unique_ptr(new A), unique_ptr(new B)) 谁先 new？ 不要写依赖这种行为的代码，否则难以调试。 Undefined Behavior (UB) None 无义务，可做任何事 无限的可能 「」 数组越界；解引用空指针；有符号数溢出 Nasal Demons。逻辑被删除、死循环、安全漏洞。 我们可以这么理解，对于 well-defined ， ill-formed ，IFNDR 这种由标准进行定义的行为类别来说，其运行结果是唯一确定的，你写下这个代码（比如 println(\u0026quot;{}\u0026quot;, 1 + 2)），无论是在 windows，linux 还是 mac 上编译运行，他的运行结果都是一样的（都是 3），该报错报错，无论是在哪个世界线上，都会导向唯一确定的终结。\n如果说，well-defined 和 ill-formed 与程序的行为结果是一个严格的一对一的单射，那么，implementation-defined 和 unspecified 就是一对多的有界集合映射。\n标准对其规定了，我不保证你结果是唯一确定的，但是你的结果必须要落在我给出的这几个可能结果里面，比如 sizeof(int) ，他可以是 4 也可以是 8，但他不能是 114514 。\n对 implementation-defined 来说，其在 x 平台下的行为是会在文档里面准确说明的，但对于 unspecified 来说，其行为就是未知但会落在一个确定的范围里面，比如经典的函数求值顺序 foo(A(), B()) ，他是处于量子态的，可能我今天是先 A 再 B ，明天就可能逆转。但无论如何，在不考虑异常和函数内部的 ub 的情况下，A 和 B 都会执行完成并把返回值传递给 foo。\n在 C++17 之前，A 和 B 的执行甚至是可以交错的（比如 A 执行了一半去执行 B）。C++17 规定了求值是 Indeterminately sequenced（不确定顺序），即要么先完整做 A，要么先完整做 B，不再允许交错，但具体谁先谁后依然是 unspecified。\n这些行为的结果取决于平台，编译器，硬件等等因素，相同的代码在不同的编译器编译出来的产物运行结果可能不同。\nundefined behavior 就如之前所说，你无法知道其行为到底会产生什么结果。\nUB 存在的理由 为什么一定要有 ub 存在呢？为什么不像 jvav，py 一样把所有行为定义清楚？这样不就会非常 safe 了？\n你说的没错，这样确实会 safe 一些，那么，代价是什么呢？\nc++ 是静态编译，对性能非常敏感的语言，其设计原则之一是：零成本抽象（Zero-overhead Principle）。\n举一个简单的例子：\n1 2 3 4 5 6 void foo(int x) { if (x \u0026gt; x + 1) { // process x } // ... } 编译器假设 x 永远不会发生溢出，因为溢出是未定义行为，在良好定义下，这个语句永远为 false。编译器就有可能，把这个分支判断彻底删除。\n在 jvav 等程序中，整数溢出通常被定义为补码回环（Wrap-around），他不允许编译器删除这个 if，这个 if 在特定情形下会被执行。这意味着，每次运行到这里，cpu 都有可能浪费一些时钟周期。如果在循环内，这也会阻止编译器进行 simd 等优化。\n正是因为这是一个 ub，所以编译器才会对其进行优化。编译器优化的前提是，程序员不会写出包含 ub 的代码。\n换句话说，ub 是编译器和程序员的契约，编译器假设程序员永远不会写出包含 ub 的代码，这样编译器才能够让程序运行的尽可能的快。\n这些微小的优化（DCE，RNCE 等等），对性能的影响是巨大的，面对性能和安全这两个永恒的矛盾点，c/cpp 做出了他的 trade off。\n其次，程序在理论上和现实上差距是巨大的，现实世界的种种因素导致程序几乎不可能在所有的硬件平台上保持一致性，对于那些不好定义，甚至无法定义的行为，不妨统一归属于 ub，这既方便，又高效。\n更多关于 ub 的例子参见 cppreference - Undefined behavior 。\n如何检查 ub cpp 程序员无法避免他写出包含 ub 的代码，所以只能尽可能少些可能包含 ub 的代码，或是使用工具。\n笔者未亲自使用过静态分析工具，资料参考自外部。\n开启编译器的全部警告，-Wall -Wextra -Werror -Wshadow 等等。 使用静态代码分析工具，clang-tidy cppcheck PVS Studio 等等。 在运行时捕获 ub，也就是所谓的 sanitizers ，比如 asan 抓取内存错误，ubsan 尽可能检查 ub 等等。 尽可能少些可能导致 ub 的代码，遵循编程范式，多用 modern cpp。 Reference What Every C Programmer Should Know About Undefined Behavior #1/3 What Every C Programmer Should Know About Undefined Behavior #2/3 What Every C Programmer Should Know About Undefined Behavior #3/3 Undefined behavior Working Draft, Standard for Programming Language C++ 笔者才疏学浅，文章可能存在错误和纰漏，请谨慎阅读。\n值得一提的是，在工业界极致的性能优化中，确实存在一些利用‘UB 假设’来辅助编译器生成的黑魔法（例如利用 std::unreachable() 消除分支，或是早年著名 3D 引擎中的 Fast Inverse Square Root 算法）。受限于笔者目前的经验与本文篇幅，这部分高阶内容暂不展开。\n准确来说，这是标准 ISO/IEC 14882:2024 的草案 n4950。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2026-01-13T20:27:02+08:00","permalink":"https://anfsity.com/p/c-%E7%9A%84%E9%AD%94%E6%B3%95%E5%A5%91%E7%BA%A6-undefined-behavior/","title":"C++ 的魔法契约： Undefined Behavior"},{"content":"最近看到一些博客和视频，深有共鸣，实在憋不住了，一吐为快。\n在我短暂的十八年人生中，有几个节点我认为对我来说非常重要。\n我是农村出身，不算什么好出身，父母都是打工人。但长成如今的模样，回首往事，确是令人感慨。\n初中时，遇到了一些好老师。并非什么名门学校，但那些先生们，却令我印象极深。\n我记得那时候上课，无论学什么，都是非常有激情的（好像除了语文，语文老师上课太无聊，总是昏昏欲睡）。老师们教书算不上厉害，但在我记忆中，他们总是很温柔的。\n那时候很喜欢去老师办公室，因为有时候，老师会给点零嘴吃，对于那时完全没有零花钱的我来说，十分具有诱惑力。但去老师办公室，也不能没有理由，一般是借口问问题，一来二去，也算和老师混熟了。\n当然，被叫去背书，补作业，挨训也是在所难免的事。\n记得很深的一件事是，当初做物理实验要用实验箱（我是从老前辈留下的圣遗物里面淘的，hh），纯粹因为好奇（电池功率太小了，电动机转不快），把一个铜线圈做的电动机用班上的交流电接上去了。幸亏运气好，只记得当初从塑料板上冒出一团电花，发出一声爆响，声音极大，然后班上就跳闸黑了。当初是下课，班上的同学都很困惑，哈哈哈。\n现在还是很佩服自己的动手能力，但是这种事情还是太危险了，当初也是初生牛犊不怕虎。\n还有就是一节物理课，好像是要下课了，等着吃饭，我实在太无聊，在下面搞些小动作，记得是抛橡皮玩。本来中午吃饭的时候班上的氛围就很浮躁，我这么一搞，更是火上浇油。\n物理老师比较年轻，平时也经常去他那里玩，也许是一种自大的心理意识，总之那时候没有听他的话，老师少见的发了脾气（可能是唯一一次）。\n记得当初是哭着找他去道歉的。老师最后没说什么，我也不太记得后面面发生什么了，但应该没有责备我。\n从那次以后我就很少在课堂上调皮了。\n我们生物老师换过一次，记得之前生物是一个老爷爷上的，我很喜欢他的课，但很可惜，我也惹过他生气。地理之前是我们的副校长上课，后面换成一个年轻的女老师来教。\n倒是数学和历史三年来一直是一个老师。历史老师人也很好，从我是当的历史课代表可见一窥。\n数学老师是我的班主任，我没惹她生过气，她也对我挺喜欢的。\n我受了她很多的鼓励，对数学的兴趣可以说绝对有她的一份参与。\n初中毕业的时候，我送了她一个发卡，不是什么值钱货，和我妈在一个百货店里面挑的。当初扭扭捏捏的不好意思送给她，最终人都走光了，我跑到她面前，声如蚊呐。但我记得她当时非常开心，眼中满是明亮的惊喜。\n三年后高中毕业回去看望母校，我又见到了她，她现在升职了，很忙。我等她开完会，匆匆见了一面。她很惊喜，但时间太少，我们没有聊很多。\n那句“岁月不饶人”虽老套，却也真是如此，一眼看去，她头发明显白了些，脸上也添了几道深切的褶皱（我爸也是这样，hh），让人感觉到，岁月，确实已经走过了我们的人生。\n不记得别的聊了什么，只记得她说：“你还记得吗（她把她头发上的发卡亮给我看），这个发卡，我戴了三年。每次看见它，我都会跟大家念叨，那个送我的学生，他当初是怎么怎么样的。”\n说实话，我当初很震撼，怎么说比较好，对，五味杂陈，因为我自己几乎都忘了这事。\n我能想象她在课堂上如何将我作为故事的主角，就如我当年，也是听着她讲别人的故事长大的一般。\n文笔不太行，见谅。怎么有点像俗套的言情小说的剧情，哈哈哈哈\n之后寥寥几句，她便匆忙赶去开会了。\n确实有点恋旧，但回忆也确实会被美化，很多细节都忘掉了，也忘记了当时的心情。\n总之，写下这些，是怀恋起了初中的开放，那时候还是走读，每天放学路上和好朋友嘻嘻闹闹。\n当初放学早，6 点左右，虽然家里比较远，走路要一个小时，回家时，可以看到天色渐渐昏暗，马路上灯光逐渐亮起。有时候搭公交车的钱掉了，或者被我拿去买零食了，就只能走路。一个人走的时候，总喜欢东想西想，或者脑袋放空，什么也不想，就光走。回家就可以吃饭了，可惜妈妈不在家的时候饭菜不怎么好吃。\n回家有条道是东西走向的，正对太阳，那条路很宽，落日时，人走在道上，是正对着太阳的，太阳很圆，很大。虽然夏天很热，但冬天的时候却十分的舒服。\n这种孤独但自由的感觉，令我十分的怀恋。\n当然，初中生活并没有回忆滤镜里这么虚幻美好。当初因为调皮经常挨棍棒，也和家里吵过架。现在想来我也确实不听话，有些事干得不像话（有次我在小区里呆到了快十二点，也没和家里人说，害得家里人好一顿找）。\n及至高中，氛围便压抑得多。时间都被学业榨干，上课的事太枯燥，就不提了。\n但印象很深的是我们的化学老师，他上课上的非常好，会引导我们去思考，而知识不仅仅局限于课堂（虽然目的也是向考点靠拢）。他做实验也很有意思，实验毕竟不能和教科书一样复刻，但他会带我们分析原因，读论文研究为什么，告诉我们，考点是写死在书上的，但知识不是写死在书上的。\n高中三年，因为不像初中，只能窝在教室里，一个月也不回家几次（家里没人，我爸做饭也不好吃，hh）。这段时间塑造了我如今很大一部分人生观，价值观。就在几年前说是。\n高中沉迷上了看番，什么都看，虽然没有手机电脑，也能想办法看，办法总是有的。\n那个时候看了很多很多番（当然现在也看），部分对我影响至今。\n谈谈一部对我很有影响的番：《春物》\n我认为我恰好是在最恰当的时候看了这部番，也许青春就是需要大老师告诉你些看似正确的歪理来打破从小到大的固有观念。\n也许是大老师这个刻意营造出来的孤独形象与青少年青春期的叛逆共感，而他用他的方式做了一些看起来很帅的事，你与他的相似性令你不由得带入大老师这个角色，一同经历他的感受。这种深刻的共鸣感令你沉沦其中，而作者时不时乱写的一些哲学道理更加强了你的沉浸感。\n人生中大抵也只有十五六岁的时候，才会深陷其中吧。\n我到底不是大老师，没有侍奉部，也没有小町。当时一度沉迷，后来想通，便不甚在意了。\n那时看的番少，谈不上什么评鉴水平，所幸接触的都是极好的作品。后来杂七杂八看得多了，别的倒没受影响，三观反倒愈发清朗起来。\n学校除了学习，剩下的时间就是看闲书了（体育课时间短，沟槽的学校体育馆周末还关门），当时看了很多很多小说，多是近现代文学，种类繁杂，囊括中西。\n例举几本我印象深的，国内的有：史铁生的《务虚笔记》，余华的《第七天》，余秋雨的《文化苦旅》（这个争议很大），王小波（时代三部曲），鲁迅，老舍，莫言等等。\n日本的有：夏目漱石（《我是猫》和《心》），芥川龙之介（《罗生门》），志贺直哉，川端康成，村上春树等等。\n西方的太多了，许多我都不记得了，随便列几个作者：加缪，米兰·昆德拉，海明威，莎士比亚，马尔克斯等，太多太多了。\n当然也并非全是所谓的高雅之作，通俗文学（light novel）也有，只是不怎么看国内的网文，逆天言情罢了，像金庸，江南（龙族），推理之类的我看的也很是入迷。\n列这么多，其实只是在推荐作品（doge。\n虽然大多只是囫囵吞枣，不求甚解。不是为了读懂这些书而读，想读懂一本真正的好书，我的人生阅历远远不够。或许是单纯消磨时间，或许是文字与内心深处的共鸣，又或者，单单是喜欢书罢了。\n人们总爱称颂史铁生直面死亡的精神，但除了课本上的《我与地坛》，真看过的又有几人？他在《务虚笔记》和《病隙碎笔》里对人生的思索，难道就是所谓生来铁人的一句空话？课本将文章定型，教条式的赏析只会硬套出题人的考点，你不必表达自我的感悟，只需把模板般的废话填得分毫不差。这种套路，怎么能体会到读《务虚笔记》时那种心被捏得紧紧的窒息感？文学本无标准答案，而教育却试图把它塞进标准答案里。所以很遗憾，坐在课堂里，反而是学不会“语文”的。\n读《1984》，被作者创造的世界深深震撼。我由衷地为书中那种寡头政治感到恐惧：历史可以任意编造，而人是可以被驯化成仅具生物学特征的“活物”的，所谓人与人之间的关系被彻底斩断。那句 \u0026ldquo;Old brother is watching you\u0026rdquo; 永远流传，不是没有原因的。\n此外，大刘《三体》的想象力真是登峰造极，不知《三体》耗费了大刘多少有趣的点子。也看过很多国际科幻大奖的作品，也有长篇《银河帝国》之类的。\n《偷影子的人》对情感的描写十分细腻，甚至可以说，令人动情，这也是我喜欢马克·李伟作品的原因。\n而《麦田里的守望者》和《杀死那只知更鸟》让我思考我自己的未来和家人。\n当初还有一本书打破了我对语言学习的认知，可惜我不记得叫什么名字了，依稀记得里面的内容和“习得”理论有关。\n那段时间，我会问自己，你感兴趣的是什么？你喜欢什么？讨厌什么？想做什么？\n那时候，我告诉自己，多干些有意思的事，多认识些有意思的人，不要后悔自己做过的选择，永远保持自己的好奇心。\n我至今仍然很浪漫的相信“我之所以在此，必有其理”。\n这些作品对我的影响是潜移默化的，我也说不大清，但我总觉得，若我没有读过这些书，我便不再是我。\n然而可悲的是，这些活生生的、长着血肉的作品，一到课堂便被抽干了水分。教条的赏析硬套模板，答题变成了对考点的溜须拍马。你不必有切身的痛感，只需把模板一条条套进答案，总会有对的。\n尤其是作文，这天底下，难道要每个学生写出来的东西都要和范文一模一样？都要用同一个素材？都要去讨论同一个论点，提出同一个看法？角度和命题人想要的不一样就不给分？\n在我看来，这些命题能抽象出来的角度要么太简单直接，要么抽象无比，非对上脑电波不可。\n我也看不懂高分作文，我很佩服他们可以用这么多的素材，角度和华丽的辞藻去论证一个本就显而易见的废话论点。\n想当初憋一个八百一千字的作文都苦不堪言，而现在随手就是几千字的长篇大论，虽不成体系。\n对我来说，认同的道理自然遵循，不认同的谁来教训都没用。那些模板作文，打动不了人心（这种文字，AI 也能轻易造出）。而我以为，人生真正紧要的中心，恰恰是别人教不会、只能自己去悟的道理。\n输出了一堆暴论，但也憋了很久了，写出来舒畅不少。\n我深知自己前文罗列的那诸多书名，多少有点掉书袋的嫌疑。我也并非学识如何深厚，只不过自己那点单薄的所思所想，不足以戳破这厚重的铁屋子，便只好打着先人的旗子，为我壮几分声色。\n时至今日，到了大学，我对“考试和教育”的认知，便是看得透彻了。\n我怨恨这套唯分数，唯绩点论的评价体系，它像一个巨大的，精密的绞肉机，把所有生猛的青春，勃发的求知欲，统统绞成整齐划一的靡肉。你不要为什么要学，只需服从。你不能有偏离考点的闲心，否则便是异类。\n可我不能嘲笑那些拼命攫取高分，争夺推免名额的同学。他们紧紧盯着眼前那根名为前途的胡萝卜，在名为绩点的磨盘边，一圈又一圈地没命奔跑。在这逼仄的世道里，多得是想要谋求一点安稳和体面的普通人。既然机器只认这钢印，谁又能苛责他们为了讨生活而不得不妥协呢？大家不过多是这套荒谬体系里的受难者，相互倾轧，却又各自辛酸。\n我是这套体系下的失败者，但我终究咽不下这口气。课讲得烂便罢，却要逼人去读那祖传的包浆 PPT，听些举高临下的训斥，美其名曰“是为你好”。何者为好？\n学生心中自有一杆明秤。\n若你问我，走上一条身边无人问津的野路，抛弃那些肉眼可见的“确定性”，去追求所谓的热爱，激情，难道就一定有个好结局吗？\n我不知道。\n我的心里并没有底。也许有一天，我会撞的头破血流，跌进更深的泥潭里。但倘若留在原地，我可能会把自己憋死。\n现实与理想总会有落差，尽自己的努力来追寻乌托邦，这就是我能够做到的一切了吧。\n我是一个很普通的人，大抵早就认识到了自己和别人有很大的差距，可总会心有不甘，便埋头努力，偶尔以报复性的娱乐麻痹神经。\n我不苛求我学的东西，一定要有个什么成果出来，我只希望我能够好好珍惜他们已经给予给我的内容。\n我喜欢纯粹的热爱，我记得我当初沉迷 Gil Strang 的线性代数课程，晚上三点睡早上十一点起来加训，从早到晚都在想着让 Lab 跑通。那一切确然是纯粹且快乐的。\n听听大家的声音：\n漫士是我很喜欢的科普 up，还有毕导，3b1b，真理元素。\n你想要做什么 我们的工科教育，问题出在了哪里 在南京大学的四年 - 软件工程与纸上谈兵 绿导师是怎样戴帽的：学术跃进运动的来龙去脉 随笔其一 $upd:$\n很感激你看到这里，我也没想过有人看（不是）。进行了小幅修改，当初有点激动了。\n我真诚地希望，每个人都能看清自己的本真实在，留存一点独属于自己的狂热。\n","date":"2025-12-25T21:28:01+08:00","image":"https://i.111666.best/image/kQpaR3pgGDE3cQ5rOcM47u.png","permalink":"https://anfsity.com/p/%E9%9A%8F%E7%AC%94%E5%85%B6%E4%BA%8C/","title":"随笔其二"},{"content":"Docker maxXing 提供为实验提供了 docker 镜像，所以我们只需要将 docker 下载下来拉取镜像即可。\n使用 pacman 下载 docker：\n1 sudo pacman -S docker 关于如何使用基本的 docker 文档里面有足够的讲解 ，在此不再赘述。\n主要来讲解一下引入 docker 导致宿主机的环境问题。\n首先 docker 默认会以 sudo 运行，这涉及到一些历史遗留问题。但客观事实是，这不合常理。所幸，docker 可以通过配置来解决这个问题：\n1 2 # 可以将 `docker` 添加进用户组避免 `sudo`。 sudo usermod -aG docker $USER 其次，docker 会改变修改默认的 IP 转发：\n1 2 3 4 5 6 sudo iptables -nvL FORWARD [sudo] password for anfsity: Chain FORWARD (policy DROP 1735 packets, 177K bytes) pkts bytes target prot opt in out source destination 1735 177K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 1735 177K DOCKER-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0 可以看到，docker 将策略改成了 DROP \u0026hellip;\n如果你之前有跑在宿主机上的类似容器应用，就需要将对应的端口开放给 iptables。\nArticle 关于 docker 的流量转发我没有做过多的了解，可以看官方文档自行了解。\nNetworking overview Packet filtering and firewalls 配置 clangd 如果你使用 c++ 进行 Lab 的话，可能你像我一样使用 clangd 。\n但是由于每次运行都是在 docker 里面进行的，这就造成一个问题\u0026ndash;如果你使用 cmake 来自动生成 cdb 文件的话，它的路径是 docker 里面的路径而不是宿主机里的路径。\n这就导致了 clangd 找不到对应的 cdb ，然后就框框爆红 **file not found 。\n这让我很是头疼，网上搜寻了一番，大致有两种思路：\n在 docker 里面也装一个 clangd ，然后把 docker 里面的 clangd 通信转发到 vscode 里面来。 这个策略有很多不足，一是折腾起来麻烦；二是就算弄好了 clangd 也没有办法享受我宿主机上的 zsh 环境；三是这只适用于 vscode ，如果我用其他的 IDE 那又要折腾一番了。\n把宿主机的目录挂载到 docker 上来，让 docker 的路径和宿主机相同。 这个思路我是在一篇 reddit 的讨论帖上看到的，感觉不错，遂剽窃使用。\n为了发扬懒人精神，我把这些命令整合到了 Makefile 中。\n我只会 Makefile QAQ，而且它也足够简单(简单吗\u0026hellip;?)，只要不写太多东西。犹记得初见 Makefile 时的语法，神似鬼画符🤔\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 IMAGE = maxxing/compiler-dev BUILD_DIR = cmake-build UID := $(shell id -u) GID := $(shell id -g) PWD := $(shell pwd) all: build configure: cmake -S . -B $(BUILD_DIR) build: configure cmake --build $(BUILD_DIR) -j12 clean: rm -rf $(BUILD_DIR) shell: docker run -it --rm \\ -u $(UID):$(GID) \\ -v \u0026#34;$(PWD):$(PWD)\u0026#34; \\ -w \u0026#34;$(PWD)\u0026#34; \\ $(IMAGE) bash docker-build: docker run --rm \\ -u $(UID):$(GID) \\ -v \u0026#34;$(PWD):$(PWD)\u0026#34; \\ -w \u0026#34;$(PWD)\u0026#34; \\ $(IMAGE) \\ sh -c \u0026#34;cmake -S . -B $(BUILD_DIR) \u0026amp;\u0026amp; cmake --build $(BUILD_DIR) -j12\u0026#34; 你可以在根目录下 make shell 直接进入 docker，make 进行编译。\n由于是在 user 模式进入的 docker, 你无法使用 sudo, 这意味着， 你没有办法使用此类需要 root 权限的指令 sudo apt update \u0026amp;\u0026amp; sudo apt install ***。\n同时需要注意的是，我个人习惯的构建目录是 cmake-build 而不是 build。\n在我对编译器进行了模块化改造后，就是使用的第一种方法，这种方法可以使用 devconatiner 插件简单的实现。这是我的 json 配置，你可以参考它。\n好消息是我为上游推送了 clangd 支持，到时候 docker 应该内置 clangd，不过上面的内容依然适用。\nCMake 谈到 CMake ，只能说又爱又恨。众所周知，C++ 没有像 rs，py 那样好用的包管理器，目前流行的包管理器各有各的缺陷。\n不过包管理器相关的知识太过庞杂，而且我也并不熟悉，就不在这里展开叙述了。\n我们来魔改一下 maxXing 的 CMakelists 👍\n按照现代 CMake 的思想，一切皆为 target 和模块化，我们来调整一下 CMakelists。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ~ anfsity  main  zsh  tree -d . ├── debug ├── include │ ├── backend │ └── ir ├── scripts ├── src │ ├── backend │ ├── frontend │ └── ir └── tests 11 directories 我们在顶层目录和 src/include 目录都放一个 CMakelists 来管理。\n这是我学习 Cmake 的入门视频 。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # root CMakelists.txt cmake_minimum_required(VERSION 3.20) project( compiler LANGUAGES CXX DESCRIPTION \u0026#34;PKU Compile Principle LABs.\u0026#34; VERSION 0.1.0 ) # c++ settings set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # binary_dir : the output dir like build/cmake-build set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # library fmt include(FetchContent) FetchContent_Declare( fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 12.1.0 ) FetchContent_MakeAvailable(fmt) # Flex \u0026amp; Bsion find_package(FLEX REQUIRED) find_package(BISON REQUIRED) add_subdirectory(include) add_subdirectory(src) enable_testing() file(GLOB_RECURSE test_cases \u0026#34;tests/*.c\u0026#34;) foreach(test_file ${test_cases}) get_filename_component(test_name ${test_file} NAME_WE) get_filename_component(parent_dir ${test_file} DIRECTORY) get_filename_component(group_name ${parent_dir} NAME) add_test( NAME ${group_name}/${test_name} COMMAND python3 ${CMAKE_SOURCE_DIR}/scripts/test_runner.py $\u0026lt;TARGET_FILE:compiler\u0026gt; ${test_file} ) endforeach() 看了一下 docker 里面的环境配置：\nTool Version Status/Notes CMake 3.28.3 现代版本，但离目前的 head 还是稍旧。 Python3 3.12.3 最新的稳定版本之一。 Rust Toolchain (Cargo) 1.91.1 版本非常新 (构建日期 2025-10-10)，处于前沿。 flex 2.6.4 标准版本。 bison 3.8.2 标准版本 (GNU Bison)。 GCC 13.3.0 构建于 Ubuntu 24.04。支持 C++20 标准。 Clang 21.1.6 版本极新。但是可能由于 libc++ 限制，可能无法使用 std::print 。 LLVM 21.1.6 Clang 的底层框架，版本与 Clang 一致。 环境可以说是非常现代，但是很遗憾无法使用 print 库。\n我早受够用 cout 的 \u0026lt;\u0026lt;/\u0026gt;\u0026gt; 来输出字符串了，真的很难用，便把 print 的原型库 fmt 拉过来使用。\n没想到 fmt 比 print 还好用。\n我的实现有一个缺陷，为了避免引入依赖，我直接使用 cmake 拉取 fmt 仓库。这导致每次测试的时候都要进行一次拉取。如果网络好的时候还算顺畅，但是校园网时常抽风，偶尔要等待半天。\n不过也可以指定输出目录进行增量编译，这也不算是什么大问题了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 # src/CMakelists.txt # generate lexer/parser set(LEXER_SRC frontend/sysy.lx) set(YACC_SRC frontend/sysy.y) # generate the lexer and parser files flex_target(Lexer ${LEXER_SRC} ${CMAKE_CURRENT_BINARY_DIR}/sysy.lex.cpp) bison_target(Parser ${YACC_SRC} ${CMAKE_CURRENT_BINARY_DIR}/sysy.tab.cpp) add_flex_bison_dependency(Lexer Parser) message(STATUS \u0026#34;[INFO] Generated lexer: ${CMAKE_CURRENT_BINARY_DIR}/sysy.lex.cpp\u0026#34;) message(STATUS \u0026#34;[INFO] Generated parser: ${CMAKE_CURRENT_BINARY_DIR}/sysy.tab.cpp\u0026#34;) message(STATUS \u0026#34;[INFO] Generated lexer outpus ${FLEX_Lexer_OUTPUTS}\u0026#34;) message(STATUS \u0026#34;[INFO] Generated parser outpus ${BISON_Parser_OUTPUT_SOURCE}\u0026#34;) set(CORE_SOURCES ir/ast.cpp backend/backend.cpp ${FLEX_Lexer_OUTPUTS} ${BISON_Parser_OUTPUT_SOURCE} ) add_library(compiler_core STATIC ${CORE_SOURCES}) target_include_directories(compiler_core PRIVATE ${CMAKE_CURRENT_BINARY_DIR} # cmake-build/src/* for generated lexer/parser ) # compiler core link libraries target_link_libraries(compiler_core PUBLIC koopa pthread dl fmt::fmt headers ) # complie options target_compile_options(compiler_core PRIVATE -O2 -Wall -Wno-register -Wextra) # executable add_executable(compiler main.cpp) target_compile_options(compiler PRIVATE -O2 -Wall -Wno-register -Wextra) target_include_directories(compiler PRIVATE $ENV{CDE_INCLUDE_PATH}) # compiler link libraries target_link_libraries(compiler PRIVATE compiler_core) target_link_directories(compiler PRIVATE $ENV{CDE_LIBRARY_PATH}/native) 1 2 3 4 5 6 7 8 9 # include/CMakeLists.txt add_library(headers INTERFACE) target_include_directories(headers INTERFACE # include/ ${CMAKE_CURRENT_SOURCE_DIR} ) message(STATUS \u0026#34;[INFO] Compiler Headers Target created: headers\u0026#34;) 这是我最终的目录结构，测试还没写完，从别处剽窃了一些测试过来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44  tree . ├── CMakeLists.txt ├── debug │ ├── hello.asm │ ├── hello.koopa │ └── test_temp │ └── ** ├── include │ ├── backend │ │ ├── backend.hpp │ │ └── koopawrapper.hpp │ ├── CMakeLists.txt │ ├── ir │ │ ├── ast.hpp │ │ ├── ir_builder.hpp │ │ ├── symbol_table.hpp │ │ └── type.hpp │ ├── koopa.h │ └── Log │ └── log.hpp ├── Makefile ├── scripts │ └── test_runner.py ├── src │ ├── backend │ │ └── backend.cpp │ ├── CMakeLists.txt │ ├── frontend │ │ ├── sysy.lx │ │ └── sysy.y │ ├── ir │ │ ├── ast.cpp │ │ └── codegen.cpp │ └── main.cpp └── tests ├── hello.c └── resources ├── functional │ └── ** └── hidden_functional └── ** 16 directories, 620 files 如果你想使用这个 CMake 文件，你必须严格遵循我的目录结构，并且把对应的 CMake 文件放到正确的位置，如果你对 CMake 不了解的话，还是使用课程提供的模板文件比较好。权当我为你提供了一种 CMake 参考配置。\n这个配置是使用了 module 之前的配置，当前仓库的配置是适配了 module 之后的配置。\n模块 什么？都 2026 了，我们还在使用传统 cpp 的 pch Σ(ﾟ∀ﾟﾉ)ﾉ\nmodules 现在处于一个很尴尬的处境，大家都夸他，但是没人用。\n模块的好处及用法可以参见这篇文章 C++20 Modules 用户视角下的最佳实践 。\n经过亲身体验后，我建议这个还是不要碰的好，因为弄好环境其实挺麻烦的。如果一定要引入的话，最好从一开始就原生支持，并且需要修改 CMake 文件。\nCMake 进行模块构建目前好像只能使用 ninja （还有谁我忘了），所以你还需要配置 ninja。\n你可以使用我的 devcontainer 配置，相关环境都已经弄好了 devcontainer.json 。\n怎么用就自行询问 AI 吧。\n简单的日志打印 一个小巧且漂亮的日志打印可以很好的帮助你进行 debug，在 cpp 20 （还是 23 ？我忘了）引进了 source_location，它可以很好的取代部分宏调试的功能，使用起来更加方便和舒适。\nfmt 库的强大功能中包含了颜色调节，这是标准库还没有实现的功能。fmt 看起来比较麻烦，但用起来意外的舒服，很符合“人体工学”。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 /** * @file log.hpp * @brief Logging and error handling utilities for the compiler. */ #pragma once #include \u0026lt;fmt/color.h\u0026gt; #include \u0026lt;fmt/core.h\u0026gt; #include \u0026lt;source_location\u0026gt; #include \u0026lt;string\u0026gt; namespace detail { /** * @brief Formats a message with source location information. * @param loc The source location. * @param fmt_str Format string. * @param args Format arguments. * @return Formatted string including location info. */ template \u0026lt;typename... Args\u0026gt; static auto format_msg(const std::source_location \u0026amp;loc, std::string_view fmt_str, Args \u0026amp;\u0026amp;...args) -\u0026gt; std::string { std::string user_msg = fmt::format(fmt::runtime(fmt_str), std::forward\u0026lt;Args\u0026gt;(args)...); return fmt::format(fmt::fg(fmt::color::alice_blue), \u0026#34;{} (at {}:{} in {})\u0026#34;, user_msg, loc.file_name(), loc.line(), loc.function_name()); } /** * @brief Custom exception for compilation errors. */ class CompileError : public std::runtime_error { public: explicit CompileError(const std::string \u0026amp;message) : std::runtime_error(message) {} }; } // namespace detail /** * @brief Static logging utility. */ class Log { public: /** * @brief Reports a fatal error, prints debug info, and throws a CompileError. * * @param fmt_str Format string for the error message. * @param args Arguments for the format string. * @param loc Source location (defaults to caller site). */ template \u0026lt;typename... Args\u0026gt; static auto panic(std::string_view fmt_str, Args \u0026amp;\u0026amp;...args, const std::source_location \u0026amp;loc = std::source_location::current()) -\u0026gt; void { fmt::print(stderr, fmt::emphasis::bold | fmt::fg(fmt::color::red), \u0026#34;[PANIC] \u0026#34;); std::string msg = fmt::format(fmt::runtime(fmt_str), std::forward\u0026lt;Args\u0026gt;(args)...); fmt::println(stderr, \u0026#34;{}\u0026#34;, msg); fmt::print(stderr, fmt::fg(fmt::color::slate_gray), \u0026#34; --\u0026gt; {}:{}:{}\\n\u0026#34;, loc.file_name(), loc.line(), loc.function_name()); throw detail::CompileError( detail::format_msg(loc, fmt_str, std::forward\u0026lt;Args\u0026gt;(args)...)); } /** * @brief Prints a trace message for debugging. * * @param fmt_str Format string for the trace message. * @param args Arguments for the format string. * @param loc Source location (defaults to caller site). */ template \u0026lt;typename... Args\u0026gt; static auto trace(std::string_view fmt_str, Args \u0026amp;\u0026amp;...args, const std::source_location \u0026amp;loc = std::source_location::current()) -\u0026gt; void { fmt::print(stdout, fmt::fg(fmt::color::cyan), \u0026#34;[TRACE] \u0026#34;); fmt::print(stdout, \u0026#34;{} \u0026#34;, fmt::format(fmt::runtime(fmt_str), std::forward\u0026lt;Args\u0026gt;(args)...)); fmt::print(stdout, fmt::fg(fmt::color::dark_violet), \u0026#34;[{}]\\n\u0026#34;, loc.function_name()); } }; 代码风格 代码风格可以参考 llvm 和 google 的 style 手册，应该在网上一搜就有。\n或者可以使用 clang-format 一键格式化，clangd 会包含它。\n对于某某特性应不应该使用的问题，我觉得只要你在项目保持前后一致性，就没什么问题。由于是 toy project，我会把语言特性拉的尽可能的新。\n代码注释 尽可能的写注释\u0026hellip;\u0026hellip;且要写明白\u0026hellip;..否则你就会像我一样，一个星期不看就看不懂要重新把所有源码再看一遍\u0026hellip;\n为什么会一两个星期没看呢，因为要期末考试\u0026hellip;\nAnyway，就算不是因为这个原因，良好风格的注释在项目中也是非常重要的，\n我目前的观点是，好的代码应该做到：代码即注释。但是对于复杂的逻辑，以及一些危险的操作，需要用注释来补全。\n内存管理 为了支持 RAII，我个人的做法是把所有的函数和过程都用类包装起来了，为了兼容 bison 还写了一套构造函数用于从裸指针构造。\n简单举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 /** * @brief Left-value expression (variable / array access). */ LVal : IDENT { $$ = new LValAST(std::move(*$1), {}); delete $1; } | IDENT ArraySuffix { $$ = new LValAST(std::move(*$1), std::move(*$2)); delete $1; delete $2; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class LValAST : public ExprAST { public: std::string ident; std::vector\u0026lt;std::unique_ptr\u0026lt;ExprAST\u0026gt;\u0026gt; indices; /** * @brief Constructs an LVal node. * @param _ident The variable name. */ LValAST(std::string _ident, std::vector\u0026lt;std::unique_ptr\u0026lt;ExprAST\u0026gt;\u0026gt; _indices) : ident(std::move(_ident)), indices(std::move(_indices)) {}; auto dump(int depth) const -\u0026gt; void override; auto codeGen(ir::KoopaBuilder \u0026amp;builder) const -\u0026gt; std::string override; auto CalcValue(ir::KoopaBuilder \u0026amp;builder) const -\u0026gt; int override; }; ","date":"2025-12-17T13:57:23+08:00","permalink":"https://anfsity.com/p/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%87%86%E5%A4%87%E7%AF%87/","title":"编译原理准备篇"},{"content":"思来想去，还是在 CS143 和 pku 的编译原理之间选择了 pku 的，顺便再看看 LLVM 的 Kaleidoscope 。\nCS143 是经典课程，但是 pku 的 lab 又足够诱人，它给予了你高度的自由从头来设计一个编译器。\n虽然说是从头，但其实大体的框架限制的比较死，如果真的要从头写一个，那恐怕花费的时间就不是这一个课程能够容纳的了。\n前端基本上是交给 yacc \u0026amp; bison，所要做的事仅仅是解析正则和生成 ir，并不需要亲自动手实现文法解析。而遍历 ir 的部分也由 maxXing 写好了，只需要借助相关设施生成的对象写完剩余的 rsicv 生成即可。\n不过虽说做了些简化工作，但是实际动手起来还是很费时。最好的建议是照抄 llvm\n写了一些笔记：\n编译原理准备篇 编译原理完结篇 差不多两个月的时间，如果除去中间因为期末考试和放寒假玩了一个星期导致的真空期，剩下的差不多一个月出头，比我想象的其实要快很多。大抵是搓编译器太有意思了。\n写点感想。这个 lab 是极好的，真好国内有这么高质量的课程。美中不足的是，引导稍微有点不够。但总的来说，课程的开放保持的恰到好处，给予你足够的思索空间，既不像傻子一样全部一步一步的告诉你，也不像某些天书一样不说人话。\n感谢 maxXing 创作了质量如此之高的教程。\n$upd$:\nlibkoopa 有文档了 (笑)\n","date":"2025-12-02T18:23:54+08:00","image":"https://i.111666.best/image/HiQlT92puMfjRJrwWFumCW.jpeg","permalink":"https://anfsity.com/p/compile-principle/","title":"Compile Principle"},{"content":"Install binder We need install binder, because Arknights depended on it , who transform app\u0026rsquo;s message to linux system.\nLinux-zen is one alternative kernel available in official Arch repos.\n$$ \\text{Arknights} \\xrightarrow{\\text{needs}} \\text{Android OS} \\xrightarrow{\\text{needs}} \\text{Binder IPC} \\xrightarrow{\\text{needs}} \\text{Host Kernel Support} $$we can use pacman to install it :\n1 sudo pacman -S linux-zen linux-zen-headers If you use NVIDIA GPU, you may need extra effort to make it work. since I use AMD GPU, I do not very care it.\nAfter install linux-zen, if you use GRUB , you should use instructions below to reboot.\n1 sudo grub-mkconfig -o /boot/grub/grub.cfg If you use systemd-boot, it will updates automatically , but you might need to check your loader entries in /boot/loader/entries to make sure linux-zen is selected.\nyou can use such instruction to check it :\n1 uname -r Waydroid 我们使用 Waydroid 作为模拟器来玩 Arknights。\n使用 pacman 下载 Waydroid。\n使用 waydroid init 下载镜像，如果因为网络问题无法安装，可以使用 archlinuxcn 源安装 waydroid-image 。\n然后再 waydroid init。\n下载脚本waydroid srcipt ，安装 Arm 翻译层。\n为提高翻译性能，推荐在 AMD CPU 上使用 libndk，在英特尔 CPU 上使用 libhoudini。然而部分应用仅支持一种翻译层，因此当某个游戏不工作或性能极差时，您可能需要把两个翻译层都试一遍。\n需要使用 py 虚拟环境。\n安装 libndk arm 翻译层\n1 sudo python3 main.py install libndk 安装 libhoudini arm 翻译层\n1 sudo python3 main.py install libhoudini 我的电脑上仅能使用 libhoudini，libndk 会导致应用黑屏。\n如果无法安装，可能是因为网络问题：\n导入端口：\n1 2 3 export http_proxy=http://127.0.0.1:7897 export https_proxy=http://127.0.0.1:7897 export all_proxy=socks5://127.0.0.1:7897 通过端口安装：\n1 sudo -E venv/bin/python main.py 查看安卓版本：\n1 waydroid prop get ro.build.version.release 设置 Waydroid 像素 我使用的是 hyprland，没有找到好用的方法让界面自使用平铺窗口大小。只能给这个窗口添加浮动属性。\n为 waydroid 自定义规则：\n1 windowrulev2 = size 1600 900, float, class:^(Waydroid)$ 可以调整宽高和 dpi：\n1 2 3 4 sudo waydroid prop set persist.waydroid.width 576 sudo waydroid prop set persist.waydroid.height 1024 sudo waydroid shell wm density 250 # 250 时显示比较好 我的评价是别设置这个长宽让 hyprland 来调整它。\nGoogle play 本来想弄一个 google play 的，但是 google 那边似乎除了点问题，只能等待修复了。\n讨论帖Unable to register device in Google uncertified registration page 。\nBUG 未知 BUG This is likely due to the audio server dying \u0026hellip; see Issue 576 and Issue 829 for details.\nA workaround is to run:\n1 # sysctl -w kernel.pid_max=65535 You can make it permanent by creating a .conf file in /etc/sysctl.d/ and adding kernel.pid_max=65535 to it.\n1 2 /etc/sysctl.d/99-sysctl.conf kernel.pid_max=65535 难评，难修。\n暂时未定位出原因，未修复，猜测是音频问题。\nDocker 禁用 ip 转发 waydroid 无法联网(ping 丢包)，初始我猜测是 TUN 模式的问题，但是排查了一遍后，还是无法联网。\n1 2  sysctl net.ipv4.ip_forward net.ipv4.ip_forward = 1 询问哈基米关于 IP 问题时，哈基米告诉我有可能是因为 Docker 与 waydroid 冲突导致的。\nDocker 默认把 iptables 的转发策略改成 DROP ：\n1 2 3 4 5 6  sudo iptables -nvL FORWARD [sudo] password for anfsity: Chain FORWARD (policy DROP 1735 packets, 177K bytes) pkts bytes target prot opt in out source destination 1735 177K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 1735 177K DOCKER-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0 对转发策略进行修改：\n1 2 3 4 5 6 7 8 9 10 11  sudo iptables -I FORWARD 1 -i waydroid0 -j ACCEPT  sudo iptables -I FORWARD 1 -o waydroid0 -j ACCEPT  sudo iptables -t nat -A POSTROUTING -s 192.168.240.0/24 -o wlan0 -j MASQUERADE # 开启 NAT  sudo iptables -nvL FORWARD Chain FORWARD (policy DROP 1973 packets, 212K bytes) pkts bytes target prot opt in out source destination 0 0 ACCEPT all -- * waydroid0 0.0.0.0/0 0.0.0.0/0 28 1680 ACCEPT all -- waydroid0 * 0.0.0.0/0 0.0.0.0/0 1973 212K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0 1973 212K DOCKER-FORWARD all -- * * 0.0.0.0/0 0.0.0.0/0 修复成功：\n1 2 3 4 5 6 7 8 9 10  sudo waydroid shell [sudo] password for anfsity: :/ # ping bing.com PING bing.com (150.171.28.10) 56(84) bytes of data. 64 bytes from 150.171.28.10: icmp_seq=1 ttl=114 time=71.9 ms 64 bytes from 150.171.28.10: icmp_seq=2 ttl=114 time=89.8 ms ^C --- bing.com ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 71.993/80.922/89.851/8.929 ms 成功后保存规则：\n1 2 sudo iptables-save | sudo tee /etc/iptables/iptables.rules sudo systemctl enable --now iptables 快捷键 使用 ydotool\n1 sudo pacman -S ydotool bc 由于我是双屏，hyprland 像素和 ydotool 像素坐标不一样，测的我要🤮了。暂时搁置。\n其他知识 waydroid shell 和 adb shell 类似，但因其是容器，所以比 ADB 更快，权限更高。\nwaydroid 的文件路径保存在 .local/share/waydroid/data/media/0\n访问需要 root 权限：\n1 2 3 4 5 6 7 8 9  ls .local/share/waydroid/data/media/0 \u0026#34;.local/share/waydroid/data/media/0\u0026#34;: Permission denied (os error 13)  sudo ls .local/share/waydroid/data/media/0 [sudo] password for anfsity: Alarms\tAudiobooks Documents\tMovies\tNotifications Podcasts Ringtones Android DCIM\tDownload\tMusic\tPictures Recordings  sudo ls .local/share/waydroid/data/media/0/Download app-release.apk\tclash-for-android.apk\tNeteaseCloudMusic_Music_official_9.4.15.251120174454_32614.apk clash-for-android-1.apk netease\tTapTap_2.89.0-rel.100100_rep.apk 安装 APK 可以在终端安装：\n1 waydroid app install /path/to/your-app.apk 列出已安装应用：\n1 waydroid app list 调试日志信息：\n1 2 3 4 5 waydroid logcat # 只看报错 waydroid logcat *:E # 使用 grep 过滤信息 waydroid logcat | grep \u0026#34;com.bilibili\u0026#34; 除了使用 logcat ，由于共用内核，还可以使用 dmesg 来抓取日志。\n1 sudo dmesg -w | grep -iE \u0026#34;waydroid|binder|lxc\u0026#34; /var/lib/waydroid/waydroid_base.prop 里是 Android 的配置文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 cat /var/lib/waydroid/waydroid_base.prop sys.use_memfd=true ro.adb.secure=1 ro.debuggable=0 gralloc.gbm.device=/dev/dri/renderD128 debug.stagefright.ccodec=0 ro.hardware.gralloc=gbm ro.hardware.egl=mesa ro.hardware.vulkan=radeon ro.hardware.camera=v4l2 ro.opengles.version=196610 waydroid.updater.disabled=true waydroid.tools_version=1.6.0 ro.vndk.lite=true ro.product.cpu.abilist=x86_64,x86,arm64-v8a,armeabi-v7a,armeabi ro.product.cpu.abilist32=x86,armeabi-v7a,armeabi ro.product.cpu.abilist64=x86_64,arm64-v8a ro.dalvik.vm.native.bridge=libhoudini.so ro.enable.native.bridge.exec=1 ro.dalvik.vm.isa.arm=x86 ro.dalvik.vm.isa.arm64=x86_64 waydroid 有两种 UI 模式，Multi-Window 和 Full-UI。\nMulti-window 可以让应用成为独立的 wayland 窗口。\nFull-UI 会渲染出完整的 Android 桌面。\n参考连接 waydroid docs archwiki waydroid $upd:$\n这个 bug 实在太多，我也不强求一定要在 linux 上玩游戏，遂以抛弃 waydroid，特此纪念。\n","date":"2025-12-02T17:54:36+08:00","permalink":"https://anfsity.com/p/play-arknights-in-archlinux/","title":"Play Arknights In ArchLinux"},{"content":" 这是 Lewis Baker 关于 C++ 协程系列文章的第一篇 。\nThis is the first of a series of posts on the C++ Coroutines TS , a new language feature that is currently on track for inclusion into the C++20 language standard.\n这是关于 C++ Coroutines TS 系列博文的第一篇，一个崭新的语言特性正在按计划纳入 C++20 的语言标准。\nIn this series I will cover how the underlying mechanics of C++ Coroutines work as well as show how they can be used to build useful higher-level abstractions such as those provided by the cppcoro library.\n在这个系列中，我将会涵盖 C++ 协程如何工作的底层机制，同时展示如何利用他们构建一个有用的如由 cppcoro 库提供的高层抽象。\nIn this post I will describe the differences between functions and coroutines and provide a bit of theory about the operations they support. The aim of this post is introduce some foundational concepts that will help frame the way you think about C++ Coroutines.\n在这篇博客中，我将会描述函数和协程之间的不同，并且提供一点点他们支持操作的理论。这篇博客的目的是介绍一些基础概念来帮助你形成思考 C++ 协程的方式。\nCorountines are Functions are Coroutines A coroutine is a generalisation of a function that allows the function to be suspended and then later resumed.\n一个协程是一个能让函数被挂起然后恢复的泛化函数。\nI will explain what this means in a bit more detail, but before I do I want to first review how a “normal” C++ function works.\n我将会更加详细的解释这意味着什么，但是在我解释之前，我希望先来复习一下一个 “普通的” c++ 函数是如何工作的。\n\u0026ldquo;Normal\u0026rdquo; Functions A normal function can be thought of as having two operations: Call and Return (Note that I’m lumping “throwing an exception” here broadly under the Return operation).\n一个普通的函数可以可以被认为有两种操作：Call 和 Return （注意我在这里把 “抛出异常” 归结为广泛的 Return 操作）。\nThe Call operation creates an activation frame, suspends execution of the calling function and transfers execution to the start of the function being called.\nCall 操作创建一个激活帧，暂停调用函数的执行并且转移执行权到被调用函数的开始。\nThe Return operation passes the return-value to the caller, destroys the activation frame and then resumes execution of the caller just after the point at which it called the function.\nReturn 操作将返回值传递给调用者，销毁激活帧，然后紧接着在调用该函数的位置之后恢复调用者的执行。\nLet’s analyse these semantics a little more…\n让我们进一步分析这些语义。。。\nActivation Frames So what is this ‘activation frame’ thing?\n所以，这个 “激活帧” 到底是什么呢？\nYou can think of the activation frame as the block of memory that holds the current state of a particular invocation of a function. This state includes the values of any parameters that were passed to it and the values of any local variables.\n你可以把激活帧视作一块维护当前特定函数调用状态的内存。这个状态包括被传入函数的参数的值和其他任何局部变量。\nFor “normal” functions, the activation frame also includes the return-address - the address of the instruction to transfer execution to upon returning from the function - and the address of the activation frame for the invocation of the calling function. You can think of these pieces of information together as describing the ‘continuation’ of the function-call. ie. they describe which invocation of which function should continue executing at which point when this function completes.\n对 “普通” 函数来说，激活帧也包括返回地址 - 即从函数返回后，(CPU) 要执行指令的地址 - 和调用函数此次调用的激活帧地址。你可以认为这些信息共同描述了函数调用的“延续性”。也就是说，他们描述了哪个函数的哪个调用应该在函数完成的哪个位置继续执行。\nWith “normal” functions, all activation frames have strictly nested lifetimes. This strict nesting allows use of a highly efficient memory allocation data-structure for allocating and freeing the activation frames for each of the function calls. This data-structure is commonly referred to as “the stack”.\n对于 “普通” 函数，每个激活帧都有严格嵌套的生命周期。这种严格嵌套的关系，使得高效率内存分配数据结构的分配和释放每个函数调用的激活帧得以成立。这种数据结构通常被称为 “栈”。\nWhen an activation frame is allocated on this stack data structure it is often called a “stack frame”.\n当激活帧被分配到这个栈数据结构时，通常被称为 “栈帧”。\nThis stack data-structure is so common that most (all?) CPU architectures have a dedicated register for holding a pointer to the top of the stack (eg. in X64 it is the rsp register).\n这种栈数据结构是如此的常见，以至于绝大多数 CPU 架构都有一个专用的寄存器来保存栈顶指针（如：X64 的 rsp 寄存器）。\nTo allocate space for a new activation frame, you just increment this register by the frame-size. To free space for an activation frame, you just decrement this register by the frame-size.\n为了给新的激活帧分配空间，你只需给这个寄存器的值加上帧大小。为了释放激活帧的空间，你只需给这个寄存器的值减去帧大小。\nThe \u0026lsquo;Call\u0026rsquo; Operation When a function calls another function, the caller must first prepare itself for suspension.\n当一个函数调用另一个函数时，调用者必须先为挂起做好准备。\nThis ‘suspend’ step typically involves saving to memory any values that are currently held in CPU registers so that those values can later be restored if required when the function resumes execution. Depending on the calling convention of the function, the caller and callee may coordinate on who saves these register values, but you can still think of them as being performed as part of the Call operation.\n这个 “挂起” 步骤通常涉及将当前 CPU 寄存器的值保存进内存，以便稍后函数恢复执行时，能够在需要的情况下还原这些值。取决于函数的调用约定，调用者和被调用者也许会协调谁来保存这些值，但你仍然可以将这一过程视为调用操作的一部分。\nThe caller also stores the values of any parameters passed to the called function into the new activation frame where they can be accessed by the function.\n调用者会将被传进被调用函数的参数值储存进一个新的可以被函数访问的激活帧。\nFinally, the caller writes the address of the resumption-point of the caller to the new activation frame and transfers execution to the start of the called function.\n最终，调用者将调用者的恢复点地址写入新的激活帧，并且转移执行到被调用函数的开始。\nIn the X86/X64 architecture this final operation has its own instruction, the call instruction, that writes the address of the next instruction onto the stack, increments the stack register by the size of the address and then jumps to the address specified in the instruction’s operand.\n在 X86/X64 架构中，这个最终操作有他自己的指令 - call 指令。这个指令将下一个指令的地址写入栈中，给栈指针的值加上地址大小，然后跳转到在指令操作数里面指定的地址。\nThe \u0026lsquo;Return\u0026rsquo; Operation When a function returns via a return-statement, the function first stores the return value (if any) where the caller can access it. This could either be in the caller’s activation frame or the function’s activation frame (the distinction can get a bit blurry for parameters and return values that cross the boundary between two activation frames).\n当函数经过 return 语句时返回时，函数首先把返回值（如果存在）存进一个调用者可以访问的位置。这个位置可以是调用者的激活帧或者函数的激活帧（当参数和返回值跨越两个激活帧的边界时，界限可能会有些模糊）。\nThen the function destroys the activation frame by:\nDestroying any local variables in-scope at the return-point. Destroying any parameter objects Freeing memory used by the activation-frame 然后函数通过以下方式销毁激活帧：\n销毁所有在 return-point 作用域内的局部变量。 销毁所有参数对象。 释放激活帧占用的内存。 And finally, it resumes execution of the caller by:\nRestoring the activation frame of the caller by setting the stack register to point to the activation frame of the caller and restoring any registers that might have been clobbered by the function. Jumping to the resume-point of the caller that was stored during the ‘Call’ operation. 最终，调用者通过：\n通过设置栈指针指向调用者的激活帧，来还原调用者的激活帧。还原那些可能被函数覆写后的寄存器。 跳转到在 Call 操作中储存的调用者恢复点。 恢复调用者执行。\nNote that as with the ‘Call’ operation, some calling conventions may split the responsibilities of the ‘Return’ operation across both the caller and callee function’s instructions.\n请注意，与“调用”操作类似，某些调用约定也许会将“返回”操作的责任分割到调用者和被调用者的指令中。\nCoroutines Coroutines generalise the operations of a function by separating out some of the steps performed in the Call and Return operations into three extra operations: Suspend, Resume and Destroy.\n协程通过把函数操作内调用和返回的部分执行步骤细分， 将其泛化为三个额外的操作：挂起，恢复，销毁。\nThe Suspend operation suspends execution of the coroutine at the current point within the function and transfers execution back to the caller or resumer without destroying the activation frame. Any objects in-scope at the point of suspension remain alive after the coroutine execution is suspended.\n挂起操作在函数内部的当前位置暂停协程的执行，同时在不销毁激活帧的情况下，将执行权转移回调用者 (caller) 或恢复者 (resumer)。在协程执行挂起后，任何在挂起点作用域内的对象都会保持存活。\nNote that, like the Return operation of a function, a coroutine can only be suspended from within the coroutine itself at well-defined suspend-points.\n注意到如同函数的返回操作，一个协程仅能在协程内部的一个明确定义的挂起点被挂起。\nThe Resume operation resumes execution of a suspended coroutine at the point at which it was suspended. This reactivates the coroutine’s activation frame.\n恢复操作在挂起点恢复执行协程。这重新激活了协程的激活帧。\nThe Destroy operation destroys the activation frame without resuming execution of the coroutine. Any objects that were in-scope at the suspend point will be destroyed. Memory used to store the activation frame is freed.\n销毁操作销毁销毁协程的激活帧，而不再恢复协程的执行。任何在挂起点作用域内的对象都将被销毁。激活帧占用的内存也会被释放。\nCoroutine activation frames Since coroutines can be suspended without destroying the activation frame, we can no longer guarantee that activation frame lifetimes will be strictly nested. This means that activation frames cannot in general be allocated using a stack data-structure and so may need to be stored on the heap instead.\n因为协程可以不销毁激活帧挂起，所以我们不能再保证激活帧的生命周期是严格嵌套的。这意味着激活帧通常无法使用栈数据结构来分配内存，因此可能需要分配在堆上。\nThere are some provisions in the C++ Coroutines TS to allow the memory for the coroutine frame to be allocated from the activation frame of the caller if the compiler can prove that the lifetime of the coroutine is indeed strictly nested within the lifetime of the caller. This can avoid heap allocations in many cases provided you have a sufficiently smart compiler.\n在 C++ Coroutines TS 中有些机制，如果编译器能够证明协程的生命周期确实是严格嵌套在调用者的生命周期内部的，可以允许协程帧的内存分配到调用者的激活帧上。只要你有一个足够聪明的编译器，这可以在很多情况下避免堆分配。\nWith coroutines there are some parts of the activation frame that need to be preserved across coroutine suspension and there are some parts that only need to be kept around while the coroutine is executing. For example, the lifetime of a variable with a scope that does not span any coroutine suspend-points can potentially be stored on the stack.\n对于协程，激活帧的某些部分需要在协程挂起期间被保留，而有些部分只需要在协程执行期间存在。例如，如果一个变量的作用域没有横跨任何协程挂起点，那么他就有可能被储存在栈上面。\nYou can logically think of the activation frame of a coroutine as being comprised of two parts: the ‘coroutine frame’ and the ‘stack frame’.\n你可以在逻辑上认为协程的激活帧由两个部分组成：“协程帧”和“栈帧”。\nThe ‘coroutine frame’ holds part of the coroutine’s activation frame that persists while the coroutine is suspended and the ‘stack frame’ part only exists while the coroutine is executing and is freed when the coroutine suspends and transfers execution back to the caller/resumer.\n“协程帧”保存了协程激活帧中在协程挂起期间依然持久存在的那部分数据，而“栈帧”部分仅在协程执行期间存在，当协程挂起并将执行权交还给调用者/恢复者时，这部分就会被释放。\nThe ‘Suspend’ operation The Suspend operation of a coroutine allows the coroutine to suspend execution in the middle of the function and transfer execution back to the caller or resumer of the coroutine.\n协程的挂起操作允许协程在函数中间暂停执行，并且将执行权转移回协程的调用者或恢复者。\nThere are certain points within the body of a coroutine that are designated as suspend-points. In the C++ Coroutines TS, these suspend-points are identified by usages of the co_await or co_yield keywords.\n在协程的内部有些位置被指定为挂起点。在 C++ Coroutines TS 中，这些挂起点通过 co_await 和 co_yield 关键字识别。\nWhen a coroutine hits one of these suspend-points it first prepares the coroutine for resumption by:\nEnsuring any values held in registers are written to the coroutine frame Writing a value to the coroutine frame that indicates which suspend-point the coroutine is being suspended at. This allows a subsequent Resume operation to know where to resume execution of the coroutine or so a subsequent Destroy to know what values were in-scope and need to be destroyed. 当协程执行到了这些挂起点之一时，它首先会通过以下方式为恢复协程做准备：\n确保所有储存在寄存器里面的值被写进协程帧。 向协程帧里面写入一个值，表明在协程里面的哪些一个挂起点正在被挂起。这保证了后续的恢复操作知道协程在哪里恢复执行，或者让后续的销毁操作知道哪些值在作用域内，需要被销毁。 Once the coroutine has been prepared for resumption, the coroutine is considered ‘suspended’.\n一旦协程完成了恢复准备，这个协程就被认为是“挂起”。\nThe coroutine then has the opportunity to execute some additional logic before execution is transferred back to the caller/resumer. This additional logic is given access to a handle to the coroutine-frame that can be used to / later resume or destroy it.\n然后协程在执行权被转移回调用者/恢复者之前，有机会去执行一些额外的逻辑。这些额外的逻辑能够访问一个句柄，该句柄后续能被用来恢复或者销毁协程帧。\nThis ability to execute logic after the coroutine enters the ‘suspended’ state / allows the coroutine to be scheduled for resumption without the need for synchronisation / that would otherwise be required if the coroutine was scheduled for resumption prior to entering the ‘suspended’ state / due to the potential for suspension and resumption of the coroutine to race. I’ll go into this in more detail in future posts.\n这种协程在进入“挂起”状态后仍能执行逻辑的能力，允许我们为协程的恢复进行调度，而不需要同步（如果我们为协程恢复调度先于进入“挂起”状态，由于协程潜在的挂起恢复竞争，会导致需要进行同步）。我会在未来的博客进一步深入讨论这个。\nThe coroutine can then choose to either immediately resume/continue execution of the coroutine or can choose to transfer execution back to the caller/resumer.\n随后，协程可以选择立即恢复/继续协程的执行，或者将执行权转移回调用者/恢复者。\nIf execution is transferred to the caller/resumer the stack-frame part of the coroutine’s activation frame is freed and popped off the stack.\n如果执行权转移回调用者/恢复者，协程激活帧的栈帧部分将会被释放，并从栈顶弹出。\nThe ‘Resume’ operation The Resume operation can be performed on a coroutine that is currently in the ‘suspended’ state.\n恢复操作可以针对当前处于“挂起”状态的协程来执行。\nWhen a function wants to resume a coroutine it needs to effectively ‘call’ into the middle of a particular invocation of the function. The way the resumer identifies the particular invocation to resume is by calling the void resume() method on the coroutine-frame handle provided to the corresponding Suspend operation.\n当一个函数想要恢复协程时，他实际上需要‘call’到函数特定调用的中间位置。恢复者识别这种特定的恢复调用的方式是，在那个由相应挂起操作提供的协程帧句柄上，调用 void resume() 方法。\nJust like a normal function call, this call to resume() will allocate a new stack-frame and store the return-address of the caller in the stack-frame before transferring execution to the function.\n就如普通的函数调用，resume() 的调用会分配一个新的栈帧，并在执行权被转移回函数之前，将调用者的返回地址储存到栈帧里面。\nHowever, instead of transferring execution to the start of the function it will transfer execution to the point in the function at which it was last suspended. It does this by loading the resume-point from the coroutine-frame and jumping to that point.\n然而，不同于将执行权转移回函数的开始，这个调用会把执行权转移到函数上一次被挂起的位置。它通过加载协程帧里的恢复位置并跳转到这个位置来实现这个操作。\nWhen the coroutine next suspends or runs to completion this call to resume() will return and resume execution of the calling function.\n当协程的下次挂起或运行完成时，resume() 的调用将会返回，并恢复调用函数的执行。\nThe ‘Destroy’ operation The Destroy operation destroys the coroutine frame without resuming execution of the coroutine.\n销毁操作销毁协程帧，而不恢复协程的执行。\nThis operation can only be performed on a suspended coroutine.\n这个操作仅能在被挂起的协程上执行。\nThe Destroy operation acts much like the Resume operation in that it re-activates the coroutine’s activation frame, including allocating a new stack-frame and storing the return-address of the caller of the Destroy operation.\n销毁操作在重新激活协程帧这一点上，与恢复操作十分类似，这包括分配一个新的栈帧和储存销毁操作调用者的返回地址。\nHowever, instead of transferring execution to the coroutine body at the last suspend-point it instead transfers execution to an alternative code-path that calls the destructors of all local variables in-scope at the suspend-point before then freeing the memory used by the coroutine frame.\n然而，不同于将执行权转移到函数上一个挂起点的协程内部，它把执行权转移到另一个代码路径上。在协程帧占用的内存被释放之前，这个代码路径调用挂起点处作用域内所有局部变量的析构函数。\nSimilar to the Resume operation, the Destroy operation identifies the particular activation-frame to destroy by calling the void destroy() method on the coroutine-frame handle provided during the corresponding Suspend operation.\n与恢复操作类似，销毁操作在相应挂起操作提供的协程帧句柄上，调用 void destroy() 方法，来识别将要被销毁的特定激活帧。\nThe ‘Call’ operation of a coroutine The Call operation of a coroutine is much the same as the call operation of a normal function. In fact, from the perspective of the caller there is no difference.\n协程的调用操作与普通函数的调用操作非常相似。事实上，从调用者的视角来看，这没有任何区别。\nHowever, rather than execution only returning to the caller when the function has run to completion, with a coroutine the call operation will instead resume execution of the caller when the coroutine reaches its first suspend-point.\n然而，函数只有运行完成后才将执行权返回给调用者，而协程不同，协程的调用操作会在协程到达第一个挂起点时，就恢复调用者的执行。\nWhen performing the Call operation on a coroutine, the caller allocates a new stack-frame, writes the parameters to the stack-frame, writes the return-address to the stack-frame and transfers execution to the coroutine. This is exactly the same as calling a normal function.\n当正在协程中执行调用操作时，调用者分配一个新的栈帧，将参数、返回地址写入栈帧，并转移执行权给协程。这与普通函数的调用是完全相同的。\nThe first thing the coroutine does is then allocate a coroutine-frame on the heap and copy/move the parameters from the stack-frame into the coroutine-frame so that the lifetime of the parameters extends beyond the first suspend-point.\n协程做的第一件事是，在堆上分配一个协程帧，把参数从栈帧复制/移动到协程帧上面，以便参数的生命周期能够延续到第一个挂起点之后。\nThe ‘Return’ operation of a coroutine The Return operation of a coroutine is a little different from that of a normal function.\n协程的返回操作和普通函数有稍微的不同。\nWhen a coroutine executes a return-statement (co_return according to the TS) operation it stores the return-value somewhere (exactly where this is stored can be customised by the coroutine) and then destructs any in-scope local variables (but not parameters).\n当协程执行一个 return 语句（依据 TS 是 co_return）时，它把返回值储存到某些地方（具体在哪些地方可以由协程自定义），然后析构在作用域内的所有局部变量（不包括参数）。\nThe coroutine then has the opportunity to execute some additional logic before transferring execution back to the caller/resumer.\n在转移执行权给调用者/恢复者之前，协程有机会去执行一些额外的逻辑。\nThis additional logic might perform some operation to publish the return value, or it might resume another coroutine that was waiting for the result. It’s completely customisable.\n这些额外的逻辑也许会执行一些操作去发布返回值，或者恢复另一个正在等待结果的协程。这完全是自定义的。\nThe coroutine then performs either a Suspend operation (keeping the coroutine-frame alive) or a Destroy operation (destroying the coroutine-frame).\n然后协程会执行挂起操作（保持协程帧存活）或者销毁操作（销毁协程帧）。\nExecution is then transferred back to the caller/resumer as per the Suspend/Destroy operation semantics, popping the stack-frame component of the activation-frame off the stack.\n随后，执行权会按照挂起/销毁操作的语义转移回调用者/恢复者，并将激活帧的栈帧部分从栈中弹出。\nIt is important to note that the return-value passed to the Return operation is not the same as the return-value returned from a Call operation as the return operation may be executed long after the caller resumed from the initial Call operation.\n很重要的一点是，传递给 Return 操作的返回值，和 Call 操作的返回的返回值是不一样的，因为 return 操作执行的时间点，可能比调用者从初始 Call 操作的时间点要晚很久。\nAn illustration To help put these concepts into pictures, I want to walk through a simple example of what happens when a coroutine is called, suspends and is later resumed.\n为了以图解的方式阐释这些概念，我希望带你梳理一些关于协程被调用，挂起，以及稍后被恢复时的简单例子。\nSo let’s say we have a function (or coroutine), f() that calls a coroutine, x(int a).\n假如我们有一个函数（或者协程）f(), 它调用一个协程 x(int a)。\nBefore the call we have a situation that looks a bit like this:\n在调用之前我们的（内存）情况大致如下：\n1 2 3 4 5 6 7 8 STACK REGISTERS HEAP +------+ +---------------+ \u0026lt;------ | rsp | | f() | +------+ +---------------+ | ... | | | Then when x(42) is called, it first creates a stack frame for x(), as with normal functions.\n然后当 x(42) 被调用，它首先为 x() 创建一个栈帧，如同普通的函数。\n1 2 3 4 5 6 7 8 9 10 STACK REGISTERS HEAP +----------------+ \u0026lt;-+ | x() | | | a = 42 | | | ret= f()+0x123 | | +------+ +----------------+ +--- | rsp | | f() | +------+ +----------------+ | ... | | | Then, once the coroutine x() has allocated memory for the coroutine frame on the heap and copied/moved parameter values into the coroutine frame we’ll end up with something that looks like the next diagram. Note that the compiler will typically hold the address of the coroutine frame in a separate register to the stack pointer (eg. MSVC stores this in the rbp register).\n然后，一旦协程 x() 为协程帧在堆上分配了内存，并复制/移动参数值到协程帧里，我们最终得到类似如下图表的情况。注意到编译器通常会把协程帧的地址保存在和栈指针不同的寄存器（eg. MSVC 把这个储存在 rbp 寄存器）。\n1 2 3 4 5 6 7 8 9 10 STACK REGISTERS HEAP +----------------+ \u0026lt;-+ | x() | | | a = 42 | | +--\u0026gt; +-----------+ | ret= f()+0x123 | | +------+ | | x() | +----------------+ +--- | rsp | | | a = 42 | | f() | +------+ | +-----------+ +----------------+ | rbp | ------+ | ... | +------+ | | If the coroutine x() then calls another normal function g() it will look something like this.\n如果协程 x() 接下来调用了另一个普通函数 g()，情况如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 STACK REGISTERS HEAP +----------------+ \u0026lt;-+ | g() | | | ret= x()+0x45 | | +----------------+ | | x() | | | coroframe | --|-------------------+ | a = 42 | | +--\u0026gt; +-----------+ | ret= f()+0x123 | | +------+ | x() | +----------------+ +--- | rsp | | a = 42 | | f() | +------+ +-----------+ +----------------+ | rbp | | ... | +------+ | | When g() returns it will destroy its activation frame and restore x()’s activation frame. Let’s say we save g()’s return value in a local variable b which is stored in the coroutine frame.\n当 g() 返回时，它将会摧毁自己的激活帧，并且恢复 x() 的激活帧。假设我们把 g() 的返回值保存在了储存在协程帧上的局部变量 b 上。\n1 2 3 4 5 6 7 8 9 10 STACK REGISTERS HEAP +----------------+ \u0026lt;-+ | x() | | | a = 42 | | +--\u0026gt; +-----------+ | ret= f()+0x123 | | +------+ | | x() | +----------------+ +--- | rsp | | | a = 42 | | f() | +------+ | | b = 789 | +----------------+ | rbp | ------+ +-----------+ | ... | +------+ | | If x() now hits a suspend-point and suspends execution without destroying its activation frame then execution returns to f().\n如果现在 x() 遇到了挂起点并挂起执行，且没有销毁其激活帧，那么执行权会返回给 f()。\nThis results in the stack-frame part of x() being popped off the stack while leaving the coroutine-frame on the heap. When the coroutine suspends for the first time, a return-value is returned to the caller. This return value often holds a handle to the coroutine-frame that suspended that can be used to later resume it. When x() suspends it also stores the address of the resumption-point of x() in the coroutine frame (call it RP for resume-point).\n这导致 x() 的栈帧部分从栈里弹出，而协程帧则保留在堆上。当协程首次挂起时，会将一个返回值返回给调用者。这个返回值通常保存了一个指向挂起的协程帧的句柄，稍后可以用于恢复协程。当 x() 被挂起时，它还会把 x() 的恢复点地址储存在协程帧中（称之为 RP）。\n1 2 3 4 5 6 7 8 9 STACK REGISTERS HEAP +----\u0026gt; +-----------+ +------+ | | x() | +----------------+ \u0026lt;----- | rsp | | | a = 42 | | f() | +------+ | | b = 789 | | handle ----|---+ | rbp | | | RP=x()+99 | | ... | | +------+ | +-----------+ | | | | | | +------------------+ This handle may now be passed around as a normal value between functions. At some point later, potentially from a different call-stack or even on a different thread, something (say, h()) will decide to resume execution of that coroutine. For example, when an async I/O operation completes.\n这个句柄现在也许可以作为普通值在函数之间传递。在稍后的某个时间点，可能是不同的调用栈，甚至是在另一个线程上，某个函数（比如 h()）决定恢复该协程的执行。比如，当一个异步 I/O 操作完成时。\nThe function that resumes the coroutine calls a void resume(handle) function to resume execution of the coroutine. To the caller, this looks just like any other normal call to a void-returning function with a single argument.\n协程恢复函数调用 void resume(handle) 来恢复协程的执行。对调用者来说，这就像其他普通的对 void-returning 单参数函数调用一样。\nThis creates a new stack-frame that records the return-address of the caller to resume(), activates the coroutine-frame by loading its address into a register and resumes execution of x() at the resume-point stored in the coroutine-frame.\n这创建了一个新的栈帧，其中记录了 resume() 调用者的返回地址，通过加载它的地址到寄存器中来激活协程帧，并在储存于协程帧的恢复点处，恢复 x() 的执行。\n1 2 3 4 5 6 7 8 9 10 STACK REGISTERS HEAP +----------------+ \u0026lt;-+ | x() | | +--\u0026gt; +-----------+ | ret= h()+0x87 | | +------+ | | x() | +----------------+ +--- | rsp | | | a = 42 | | h() | +------+ | | b = 789 | | handle | | rbp | ------+ +-----------+ +----------------+ +------+ | ... | | | In summary I have described coroutines as being a generalisation of a function that has three additional operations - ‘Suspend’, ‘Resume’ and ‘Destroy’ - in addition to the ‘Call’ and ‘Return’ operations provided by “normal” functions.\n我已经把协程描述为函数的泛化，除了“普通”函数提供的 call 和 Return 操作外，它还有三个额外的操作 - Suspend（挂起），Resume（恢复）和 Destroy（销毁）。\nI hope that this provides some useful mental framing for how to think of coroutines and their control-flow.\n我希望这能提供一些有用的思维框架，帮助你理解协程及其控制流。\nIn the next post I will go through the mechanics of the C++ Coroutines TS language extensions and explain how the compiler translates code that you write into coroutines.\n在下一篇文章中，我将深入讲解 C++ Coroutines TS 语言拓展的机制，并解释编译器是如何将你编写的代码转换为协程的。\n","date":"2025-11-28T08:21:14+08:00","permalink":"https://anfsity.com/p/c-coroutines-1/","title":"C++ Coroutines (1)"},{"content":"一些有用的资料 Lua-guide Neovim Docs Nvim lua guide Build your first Neovim configuration in lua Stack overflow Programming in Lua Lua 5.4 Reference Manual Lua Beginners Guide 以及, 你可以在命令模式下输入 :h lua-guide 中查看文档.\n更加简单的选择-NvChad 偶然间看到一文环境配置指南/编辑器 – Neovim 安装配置教程（基于 NvChad） 便决定跟随作者进行配置。\n本文基于该文章的内容进行补充叙述。\n我们使用 NvChad 来简化我们的配置流程和添加更加易用的主题功能。\n基本配置 下拉仓库后， 我们首先来修改 options.lua。\n我们打开 ~/.config/nvim/lua/options.lua ， 默认配置可以在这里访问 NvChad 。\n接下来我们对他的默认配置做一些讲解。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 local opt = vim.opt local o = vim.o local g = vim.g -------------------------------------- options ------------------------------------------ o.laststatus = 3 o.showmode = false o.splitkeep = \u0026#34;screen\u0026#34; o.clipboard = \u0026#34;unnamedplus\u0026#34; o.cursorline = true o.cursorlineopt = \u0026#34;number\u0026#34; -- Indenting o.expandtab = true o.shiftwidth = 2 o.smartindent = true o.tabstop = 2 o.softtabstop = 2 opt.fillchars = { eob = \u0026#34; \u0026#34; } o.ignorecase = true o.smartcase = true o.mouse = \u0026#34;a\u0026#34; -- Numbers o.number = true o.numberwidth = 2 o.ruler = false -- disable nvim intro opt.shortmess:append \u0026#34;sI\u0026#34; o.signcolumn = \u0026#34;yes\u0026#34; o.splitbelow = true o.splitright = true o.timeoutlen = 400 o.undofile = true -- interval for writing swap file to disk, also used by gitsigns o.updatetime = 250 -- go to previous/next line with h,l,left arrow and right arrow -- when cursor reaches end/beginning of line opt.whichwrap:append \u0026#34;\u0026lt;\u0026gt;[]hl\u0026#34; -- disable some default providers g.loaded_node_provider = 0 g.loaded_python3_provider = 0 g.loaded_perl_provider = 0 g.loaded_ruby_provider = 0 -- add binaries installed by mason.nvim to path local is_windows = vim.fn.has \u0026#34;win32\u0026#34; ~= 0 local sep = is_windows and \u0026#34;\\\\\u0026#34; or \u0026#34;/\u0026#34; local delim = is_windows and \u0026#34;;\u0026#34; or \u0026#34;:\u0026#34; vim.env.PATH = table.concat({ vim.fn.stdpath \u0026#34;data\u0026#34;, \u0026#34;mason\u0026#34;, \u0026#34;bin\u0026#34; }, sep) .. delim .. vim.env.PATH laststauts ： 状态栏显示模式。 0 : 从不显示 1 : 只有超过一个窗口才显示 2 : 总是显示 3 : 总是显示， 并且是全局的。 具体区别可以使用控制变量法直观查看。\nshowmode ： 字面意思， 时候显示当前模式。 cursorline ： 高亮显示光标所在的当前行。 cursorlineopt line 高亮整行。 number 高亮行号。 both 都高亮。 expandtab ：按下 Tab 后， 是否将 /t 转化为空格。 shiftwidth ： 当执行自动缩进时，包括智能缩进， 一次缩进或取消缩进的宽度。 ignorecase ： 搜索时忽略大小写。 mouse 鼠标支持， a (all) 表示所有模式下都支持鼠标。 实在是很多， 每个都解释太过麻烦。查阅文档或者 :h options 。\nvim.o options NvChad 提供的配置已经非常完善了，我仅仅对其做了小部分修改以符合我的个人习惯。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 require \u0026#34;nvchad.options\u0026#34; local o = vim.o local opt = vim.opt -------------------- options --------------------- -- Common o.cursorlineopt =\u0026#39;both\u0026#39; o.list = true opt.listchars = { tab = \u0026#34;» \u0026#34;, trail = \u0026#34;·\u0026#34;, nbsp = \u0026#34;␣\u0026#34; } -- Indenting o.expandtab = false o.shiftwidth = 4 o.showmode = true o.tabstop = 4 o.softtabstop = 4 如果你对 vim.o ，vim.opt 等感到疑惑，这些内容也许可以帮助你。\nDifference between vim.o and vim.opt? neovim入门指南(一)：基础配置 其实官方文档也是一个不错的选择， 但是选择官方文档不太可能。太难阅读了。\n他的定位更像是字典而不是教科书，更适合查阅，而不是理解。\n快捷键 快捷键的使用在 coding 中的使用体感是非常重要的。\nnvim 支持你使用 lua 自定义快捷键操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 require \u0026#34;nvchad.mappings\u0026#34; local map = vim.keymap.set map(\u0026#34;n\u0026#34;, \u0026#34;;\u0026#34;, \u0026#34;:\u0026#34;, { desc = \u0026#34;CMD enter command mode\u0026#34; }) map(\u0026#34;i\u0026#34;, \u0026#34;\u0026lt;A-h\u0026gt;\u0026#34;, \u0026#34;\u0026lt;ESC\u0026gt;^i\u0026#34;, { desc = \u0026#34;move beginning of line\u0026#34; }) map(\u0026#34;i\u0026#34;, \u0026#34;\u0026lt;A-l\u0026gt;\u0026#34;, \u0026#34;\u0026lt;End\u0026gt;\u0026#34;, { desc = \u0026#34;move end of line\u0026#34; }) map(\u0026#34;n\u0026#34;, \u0026#34;F\u0026#34;, \u0026#34;%\u0026#34;, { desc = \u0026#34;jump between match-pair\u0026#34; }) -- use windows like keymaps map({ \u0026#34;n\u0026#34;, \u0026#34;i\u0026#34;, \u0026#34;v\u0026#34; }, \u0026#34;\u0026lt;C-s\u0026gt;\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; w \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;file save\u0026#34; }) map({ \u0026#34;n\u0026#34; }, \u0026#34;\u0026lt;C-a\u0026gt;\u0026#34;, \u0026#34;ggVG\u0026#34;, { desc = \u0026#34;select all file\u0026#34; }) map({ \u0026#34;n\u0026#34;, \u0026#34;i\u0026#34;, \u0026#34;v\u0026#34; }, \u0026#34;\u0026lt;C-z\u0026gt;\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; undo \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;history undo\u0026#34; }) map({ \u0026#34;n\u0026#34;, \u0026#34;i\u0026#34;, \u0026#34;v\u0026#34; }, \u0026#34;\u0026lt;C-y\u0026gt;\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; redo \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;history redo\u0026#34; }) map(\u0026#34;n\u0026#34;, \u0026#34;\u0026lt;C-/\u0026gt;\u0026#34;, \u0026#34;gcc\u0026#34;, { desc = \u0026#34;comment toggle\u0026#34;, remap = true }) map(\u0026#34;i\u0026#34;, \u0026#34;\u0026lt;C-/\u0026gt;\u0026#34;, \u0026#34;\u0026lt;Esc\u0026gt;gcc^i\u0026#34;, { desc = \u0026#34;comment toggle\u0026#34;, remap = true }) -- visual studio code like keymaps map(\u0026#34;n\u0026#34;, \u0026#34;gb\u0026#34;, \u0026#34;\u0026lt;C-o\u0026gt;\u0026#34;, { desc = \u0026#34;jump back\u0026#34; }) map(\u0026#34;n\u0026#34;, \u0026#34;gh\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; lua vim.lsp.buf.hover() \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;LSP hover\u0026#34; })map(\u0026#34;n\u0026#34;, \u0026#34;ge\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; lua vim.diagnostic.open_float() \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;LSP show diagnostics\u0026#34; }) map(\u0026#34;n\u0026#34;, \u0026#34;ge\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; lua vim.diagnostic.open_float() \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;LSP show diagnostics\u0026#34; }) map({ \u0026#34;n\u0026#34;, \u0026#34;i\u0026#34; }, \u0026#34;\u0026lt;A-j\u0026gt;\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; :m +1 \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;move one line down \u0026#34;}) map({ \u0026#34;n\u0026#34;, \u0026#34;i\u0026#34; }, \u0026#34;\u0026lt;A-k\u0026gt;\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt; :m -2 \u0026lt;cr\u0026gt;\u0026#34;, { desc = \u0026#34;move one line up \u0026#34;}) map(\u0026#34;v\u0026#34;, \u0026#34;\u0026lt;A-j\u0026gt;\u0026#34;, \u0026#34;:m \u0026#39;\u0026gt;+1\u0026lt;CR\u0026gt;gv=gv\u0026#34;, { desc = \u0026#34;Move selected lines down\u0026#34; }) map(\u0026#34;v\u0026#34;, \u0026#34;\u0026lt;A-k\u0026gt;\u0026#34;, \u0026#34;:m \u0026#39;\u0026lt;-2\u0026lt;CR\u0026gt;gv=gv\u0026#34;, { desc = \u0026#34;Move selected lines up\u0026#34; }) 这是一份我的个人快捷键配置表。当然，这也包括了 NvChad 默认提供给你的快捷键 。\nNvChad 实现的有关终端的一套快捷键逻辑使用的十分舒适。\n接下来我们来解析一下常用的 lua 语句。\n\u0026lt;C\u0026gt; 代表 ctrl ， \u0026lt;A\u0026gt; 代表 Alt ，默认的主键 \u0026lt;Leader\u0026gt; 是空格。\nremap 作为递归标志来使用，如果说映射 A 指向 B ，现在我想要创建一个新的映射 C 通过指向 A 来做到使用 B 的效果，此时就需要告诉 map 函数，我要创建一个这样的连续映射，请帮我创建。在代码中 gcc 本身其实就已经是一个映射了，所以我们需要使用 remap。\n由于我使用 linux 作为自己的主力机器，不保证该配置在 windows 上同样有用。\n插件 NvChad 通过 LazyNvim 进行管理，需要注意的是，有的时候懒加载会导致异步问题，对于常用的功能，我并不推荐进行懒加载处理。\n关于 lazy.nvim 的逻辑，请参考该文 的内容 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 -- 自定义的 lazy.nvim 安装路径 local lazypath = vim.fn.stdpath \u0026#34;data\u0026#34; .. \u0026#34;/lazy/lazy.nvim\u0026#34; -- 如果 lazy.nvim 不存在，则通过 Git 克隆它到指定路径 if not vim.uv.fs_stat(lazypath) then local repo = \u0026#34;https://github.com/folke/lazy.nvim.git\u0026#34; vim.fn.system { \u0026#34;git\u0026#34;, \u0026#34;clone\u0026#34;, \u0026#34;--filter=blob:none\u0026#34;, repo, \u0026#34;--branch=stable\u0026#34;, lazypath } end -- 将 lazy.nvim 的安装路径添加到 Neovim 的运行时路径中，以便 Neovim 能找到它 vim.opt.rtp:prepend(lazypath) -- 这里引入的文件就是 `lua/configs/lazy.lua`，它包含了 lazy.nvim 的一些基本配置 local lazy_config = require \u0026#34;configs.lazy\u0026#34; -- 通过 lazy.nvim 加载插件 -- lazy.nvim 会自动下载在 `.setup` 中指定的插件并加载它们 require(\u0026#34;lazy\u0026#34;).setup({ -- 先加载 NvChad { \u0026#34;NvChad/NvChad\u0026#34;, lazy = false, branch = \u0026#34;v2.5\u0026#34;, import = \u0026#34;nvchad.plugins\u0026#34;, }, -- 然后从 `plugins/` 目录（也就是你当前配置文件夹的 `lua/plugins/` 目录）中查找插件并加载 { import = \u0026#34;plugins\u0026#34; }, }, lazy_config) 该逻辑引用自此文环境配置指南/编辑器 – Neovim 安装配置教程（基于 NvChad） 这里 require(\u0026quot;lazy\u0026quot;).setup() 需要一个 table 作为作为返回值来接受。\n同理，在 plugins 文件夹里面的不必是一个 init.lua ，也可以是一个很多的 *.lua 。\n让我们看看 lazy.nvim 安装插件的通用格式 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 return { -- the colorscheme should be available when starting Neovim { \u0026#34;folke/tokyonight.nvim\u0026#34;, lazy = false, -- make sure we load this during startup if it is your main colorscheme priority = 1000, -- make sure to load this before all the other start plugins config = function() -- load the colorscheme here vim.cmd([[colorscheme tokyonight]]) end, }, -- I have a separate config.mappings file where I require which-key. -- With lazy the plugin will be automatically loaded when it is required somewhere { \u0026#34;folke/which-key.nvim\u0026#34;, lazy = true }, { \u0026#34;nvim-neorg/neorg\u0026#34;, -- lazy-load on filetype ft = \u0026#34;norg\u0026#34;, -- options for neorg. This will automatically call `require(\u0026#34;neorg\u0026#34;).setup(opts)` opts = { load = { [\u0026#34;core.defaults\u0026#34;] = {}, }, }, }, { \u0026#34;dstein64/vim-startuptime\u0026#34;, -- lazy-load on a command cmd = \u0026#34;StartupTime\u0026#34;, -- init is called during startup. Configuration for vim plugins typically should be set in an init function init = function() vim.g.startuptime_tries = 10 end, }, { \u0026#34;hrsh7th/nvim-cmp\u0026#34;, -- load cmp on InsertEnter event = \u0026#34;InsertEnter\u0026#34;, -- these dependencies will only be loaded when cmp loads -- dependencies are always lazy-loaded unless specified otherwise dependencies = { \u0026#34;hrsh7th/cmp-nvim-lsp\u0026#34;, \u0026#34;hrsh7th/cmp-buffer\u0026#34;, }, config = function() -- ... end, }, -- if some code requires a module from an unloaded plugin, it will be automatically loaded. -- So for api plugins like devicons, we can always set lazy=true { \u0026#34;nvim-tree/nvim-web-devicons\u0026#34;, lazy = true }, -- you can use the VeryLazy event for things that can -- load later and are not important for the initial UI { \u0026#34;stevearc/dressing.nvim\u0026#34;, event = \u0026#34;VeryLazy\u0026#34; }, { \u0026#34;Wansmer/treesj\u0026#34;, keys = { { \u0026#34;J\u0026#34;, \u0026#34;\u0026lt;cmd\u0026gt;TSJToggle\u0026lt;cr\u0026gt;\u0026#34;, desc = \u0026#34;Join Toggle\u0026#34; }, }, opts = { use_default_keymaps = false, max_join_length = 150 }, }, { \u0026#34;monaqa/dial.nvim\u0026#34;, -- lazy-load on keys -- mode is `n` by default. For more advanced options, check the section on key mappings keys = { \u0026#34;\u0026lt;C-a\u0026gt;\u0026#34;, { \u0026#34;\u0026lt;C-x\u0026gt;\u0026#34;, mode = \u0026#34;n\u0026#34; } }, }, -- local plugins need to be explicitly configured with dir { dir = \u0026#34;~/projects/secret.nvim\u0026#34; }, -- you can use a custom url to fetch a plugin { url = \u0026#34;git@github.com:folke/noice.nvim.git\u0026#34; }, -- local plugins can also be configured with the dev option. -- This will use {config.dev.path}/noice.nvim/ instead of fetching it from GitHub -- With the dev option, you can easily switch between the local and installed version of a plugin { \u0026#34;folke/noice.nvim\u0026#34;, dev = true }, } 配置来自于lazy.nvim 里面其实返回了一个包含多个 Plugin Spec 的数组，你肯定发现了，每一个数组的长短都不一定相同， lazy.nvim 支持你返回单个 Spec 或者包含多个 Spec 的数组，方便你更灵活的组织你的插件配置。\n\u0026quot;folke/tokyonight.nvim\u0026quot; 代表的是 github 仓库名称，用于让 lazy.nvim 自动从 github 上拉取代码。\nlazy = false 代表是否启用懒加载， false 代表启用。默认是 false。 需要注意的是，同时有一个 event 指令，同样代表启用懒加载 ，并且在相关的 event 发生时，启用插件。\nopts 和 config 是传递给该插件的 setup() 函数的一个 Lua table 或者一个返回 Lua table 的函数。由于逻辑的问题，我更推荐在下面这种情景使用 opts 而不是 config ，虽然两者是等价的 ：\n1 2 3 4 5 6 return { \u0026#34;stevearc/conform.nvim\u0026#34;, config = function() require(\u0026#34;conform\u0026#34;).setup({}) end } 1 2 3 4 return { \u0026#34;stevearc/conform.nvim\u0026#34;, opts = {} } config 支持的逻辑更为复杂，也就是说，当要进行逻辑操作时，我们要使用 config ，而仅仅声明配置和描述需求时，我们应该使用 opts 。在绝大部分的使用场景，我们都应该使用 opts 而不是 config 。\n值得注意的是，如果你想要安装的插件是属于 vim 的原生插件，我们需要调用 init 方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 return { { \u0026#34;mg979/vim-visual-multi\u0026#34;, lazy = false, init = function () vim.g.VM_maps = { [\u0026#34;Find Under\u0026#34;] = \u0026#39;\u0026lt;C-f\u0026gt;\u0026#39;, } vim.g.VM_maps_disable = { [\u0026#34;i\u0026#34;] = \u0026#34;A\u0026#34;, } end, }, } dependencies 选项描述了该仓库所需要的依赖项，便于 lazy.nvim 拉取和维护。\n接下来我们便需要为 lsp 服务来做准备了，顺便对 nvim 文件夹下面的框架进行整理和调整，让它更符合我们个人的使用习惯 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ├── lua │ ├── autocmds.lua │ ├── chadrc.lua │ ├── configs │ │ ├── conform.lua │ │ ├── highlight.lua │ │ ├── lazy.lua │ │ ├── lsp.lua │ │ ├── luasnip.lua │ │ └── ui.lua │ ├── lua_snippets │ │ └── snippets.lua │ ├── mappings.lua │ ├── options.lua │ └── plugins │ ├── comments.lua │ ├── conform.lua │ ├── highlight.lua │ ├── lsp.lua │ ├── luasnip.lua │ ├── motions.lua │ ├── tools.lua │ └── ui.lua 如果你需要接着看插件相关的内容介绍，你可以查阅之前知乎上的那篇文章。接下来我们就需要大跨步朝着我们的目标迈进 \u0026ndash; 配置 LSP。\nbtw, 关于那篇文章没有提到的 snippets ， 这是 IDE 里面一个很常见的功能，而在 NeoVim 当中， 我们要使用插件 luasnip 来获得这样的功能。\nluasnip 提供了多个相关的 API ， luasnip.s 提供就是 snippet 的接口。我使用的有两个接口， 一种是 luasnip.extras.fmt ，是 luasnip 提供的一个格式化工具 ，另一个是 luansip.t ，使用的是 vscode 的 snippet 格式。所以你可以完全无痛的从 vscode 迁移过来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 local luaSnip = require(\u0026#34;luasnip\u0026#34;) -- snippet local s = luaSnip.s -- insert node local i = luaSnip.i -- text node local t = luaSnip.t -- formatter tool local fmt = require(\u0026#34;luasnip.extras.fmt\u0026#34;).fmt luaSnip.add_snippets(\u0026#34;cpp\u0026#34;, { s( { trig = \u0026#34;demo\u0026#34;, name = \u0026#34;Competitive Programming Template\u0026#34;, dscr = \u0026#34;A template for competitive programming. \u0026#34;}, t({ -- some vsocde like snippets here }) ) } 最后， 你需要关掉插件的 lazyload ，Lazy.nvim 会出现些许 bug 导致插件无法被正确启用。我没有细究里面的细节，大概的怀疑方向是懒加载导致的异步问题。\nLSP(Language Server Protocol) 需要承认的是， LSP 可能是配置 Neovim 最复杂的一部分。这或许也是这篇文章最重要的一部分，只有配置好了它，你才能在 Neovim 上享受完整的代码补全能力 \u0026mdash;\u0026mdash; but first, what is LSP ?\n简单来说 , LSP 协议包含两个核心组件， Language Client 和 Language Server ，正如字面意思，Language Client 负责用户界面的渲染 (高亮，悬浮提示) ，监听用户的行为并将其转化特定语言的请求转发给 Language Server 。而 Language Server 负责接收 Language Client 的信息进行处理，并且将结果 (代码补全，错误信息等) 回转给 Language Client 。\n协议使得完整语言支持的 \u0026ldquo;前端\u0026rdquo; 和 \u0026ldquo;后端\u0026rdquo; 逻辑分离了。自此，编辑器/IDE 只需要负责实现 Language Client ，而语言的开发者和维护者只需要负责实现 Language Server 。会大大减小开发者的工作量和用户的体验舒适程度。\n在历史上，本来是由各个 编辑器/IDE 负责实现对应语言的相关功能，这就导致了各个 编辑器/IDE 对相同语言支持有着不同的实现方法，对各种语言的支持程度也各不相同，开发者不得不维护多个平台对应功能的实现，做大量的重复劳动。而且也导致在不同编辑器上使用的体感差异很大。为了解决这个问题，微软提出了 LSP 协议。\n这就是对 lsp 一个非常粗略的理解，但这毕竟不是本文的重点。如果你对 lsp 协议很感兴趣，不妨尝试阅读一下微软的文档 。\nNeoVim 在 0.5+ 版本以后，内置了语言服务器的接口 。你可以使用 NeoVim 自身的接口来实现功能完善的 lsp 客户端，但是我们不必如此造轮子 (纵使这么做对单独一个语言来说并不复杂，但是你需要维护的语言越来越多的时候，这种操作就越来越复杂，而且也不便于迁移备份) \u0026mdash;\u0026mdash; 早有先人为你做好了准备。一个叫做 nvim-lspconfig 的插件包含了许多主流语言的 lsp 配置，只要加载该插件，这些配置就会自动的加载到 NeoVim 当中。 而你只需要一句简单的 vim.lsp.enable(...)。\ngraph TD subgraph \u0026#34;📦 Package Management \u0026amp; Installation\u0026#34; A[nvim-mason] --\u0026gt;|Installs| B[Language Servers, e.g., clangd, rust_analyzer]; A --\u0026gt;|Installs| C[Formatters \u0026amp; Linters, e.g., prettier, stylua]; end subgraph \u0026#34;🔌 Core Configuration \u0026amp; Integration\u0026#34; D[nvim-lspconfig] --\u0026gt;|Reads User Config| B; D --\u0026gt;|Configures \u0026amp; Attaches| E[Neovim\u0026#39;s Built-in LSP Client]; B --\u0026gt;|Communicates via LSP Protocol| E; end subgraph \u0026#34;🧠 Enhanced Syntax \u0026amp; Parsing\u0026#34; F[nvim-treesitter] --\u0026gt;|Provides Rich Syntax Trees| E; F --\u0026gt;|Improves| G[Syntax Highlighting]; F --\u0026gt;|Enables| H[\u0026#34;Text Objects, e.g., [a] for argument\u0026#34;]; end subgraph \u0026#34;✨ User Experience \u0026amp; UI\u0026#34; E --\u0026gt;|Provides Completion Data| I[nvim-cmp]; J[luasnip, etc.] --\u0026gt;|Provides Snippet Data| I; I --\u0026gt;|Renders UI| K[Autocomplete Popup Menu]; E --\u0026gt;|Provides Signature Help| L[Signature Help Popup]; I --\u0026gt;|Triggers Auto-Pairing| M[nvim-autopairs]; K -- Triggers --\u0026gt; M; end subgraph \u0026#34;Legenda\u0026#34; subgraph \u0026#34;Plugins\u0026#34; A; D; F; I; J; M; end subgraph \u0026#34;LSP Servers / Tools\u0026#34; B; C; end subgraph \u0026#34;Neovim Core\u0026#34; E; end subgraph \u0026#34;User Features\u0026#34; G; H; K; L; end end %% Styling style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#bbf,stroke:#333,stroke-width:2px style E fill:#9f9,stroke:#333,stroke-width:2px style F fill:#fcf,stroke:#333,stroke-width:2px style I fill:#ff9,stroke:#333,stroke-width:2px 上面这个图表讲述了 Neovim 内置的 LSP Client 和其插件之间的通信。常见的插件有 lsp-config 简化配置，nvim-autopairs 和 nvim-cmp提供插件的代码补全，Mason 是包管理器， nvim-treesitter 和 legenda 负责更加完善的代码高亮和错误提示的 UI。\nlsp-config 的配置并不复杂，下面我们以配置自己的 lua_ls 为例，讲解一下整个的配置流程。\n首先要提的是，lsp-config 虽然会帮你配置 lsp ，但是它不会帮你安装他。我们使用 Mason 插件来自动的安装所需的 lsp，DAP ，linter 等等。使用 :Mason 调用 Mason 的面板，用 g? 查看相关的快捷键，使用 / 搜索，用法很简单，提示也给的很全，这里就不再赘述了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 local mr = require \u0026#34;mason-registry\u0026#34; local nvlsp = require \u0026#34;nvchad.configs.lspconfig\u0026#34; local eagerly_installed_langs = { ... } --- 一个预安装列表 local ensure_installed = { [\u0026#34;*\u0026#34;] = { \u0026#34;typos_lsp\u0026#34; }, --- typos_lsp 是一个检查拼写的语言服务器 Bash = { \u0026#34;bashls\u0026#34;, \u0026#34;shellcheck\u0026#34;, \u0026#34;shfmt\u0026#34; }, C = { \u0026#34;clangd\u0026#34;, \u0026#34;clang-format\u0026#34; }, ... } --- 必须安装的列表 --- vim.api.nvim_create_autocmd 创建一个自动命令 --- 自动命令指的是，当某个特定时间发生时，自动执行一个回调函数 --- LspAttach 是我们监听的事件名称，当一个语言服务器成功附加到一个缓冲区时，事件被触发 vim.api.nvim_create_autocmd(\u0026#34;LspAttach\u0026#34;, { callback = function(args) nvlsp.on_attach(_, args.buf) --- nvchad 实现的 on_attach 函数 --- 这个函数通常负责这个缓冲区里面和 lsp 相关的快捷键 --- _ 在 lua 中表示被抛弃的变量 end, }) --- vim.lsp.config 是 nvim-lspconfig 插件的一个核心配置函数 vim.lsp.config(\u0026#34;*\u0026#34;, { --- on_attach 是 nvim-lspconfig 提供的一个回调函数 --- client 是当前正在和缓冲区进行通信的语言服务器， bufnr 是当前缓冲区的编号 on_attach = function(client, bufnr) if client.supports_method(\u0026#34;textDocument/inlayHint\u0026#34;) or client.server_capabilities.inlayHintProvider then vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) end --- 如果当前的语言服务器支持 inlayhint, 我们就开启他 --- 功能类似 if client.supports_method(\u0026#34;textDocument/codeLens\u0026#34;, { bufnr = bufnr }) then vim.lsp.codelens.refresh { bufnr = bufnr } vim.api.nvim_create_autocmd({ \u0026#34;BufEnter\u0026#34;, \u0026#34;InsertLeave\u0026#34; }, { --- 监听两个事件， bufenter 进入这个缓冲区， insertlaeve 离开插入模式 buffer = bufnr, --- 这个命令只对当前缓冲区生效 callback = function() vim.lsp.codelens.refresh { bufnr = bufnr } end, }) end end, on_init = nvlsp.on_init, capabilities = nvlsp.capabilities, }) --- Start: LuaLS config --- dofile(vim.g.base46_cache .. \u0026#34;lsp\u0026#34;) --- dofile 是 lua 的一个内置函数，用来直接执行一个 lua 文件的代码 --- vim.g.base56_cache 是 nvchad 设置的路径 require(\u0026#34;nvchad.lsp\u0026#34;).diagnostic_config() --- 这个也是 nvchad 实现的诊断信息的样式 local lua_ls_settings = { Lua = { hint = { enable = true, paramName = \u0026#34;Literal\u0026#34;, }, --- 启用行内提示，这个功能我很喜欢 --- 字面量，仅在函数参数为字面量的时候才显示参数名称 codeLens = { enable = true, }, --- 显示函数的被引用次数 workspace = { maxPreload = 1000000, --- 设置 lua_ls 启动时预加载和分析的最大总大小，单位是 bytes preloadFileSize = 10000, --- 设置 lua_ls 预加载的单个文件最大大小，单位是 bytes }, }, } -- If current working directory is Neovim config directory local in_neovim_config_dir = (function() local stdpath_config = vim.fn.stdpath \u0026#34;config\u0026#34; --- 匿名函数，这一句是在获取 nvim 的配置目录路径 local config_dirs = type(stdpath_config) == \u0026#34;string\u0026#34; and { stdpath_config } or stdpath_config --- 确保 config_dirs 总是一个 list ---@diagnostic disable-next-line: param-type-mismatch for _, dir in ipairs(config_dirs) do if vim.fn.getcwd():find(dir, 1, true) then return true end end end)() --- 下面的配置只针对在 nvim 的配置目录下启用，并不会影响常规的 lua 项目 if in_neovim_config_dir then -- Add vim to globals for type hinting lua_ls_settings.Lua.diagnostic = lua_ls_settings.Lua.diagnostic or {} lua_ls_settings.Lua.diagnostic.globals = lua_ls_settings.Lua.diagnostic.globals or {} table.insert(lua_ls_settings.Lua.diagnostic.globals, \u0026#34;vim\u0026#34;) -- Add all plugins installed with lazy.nvim to `workspace.library` for type hinting lua_ls_settings.Lua.workspace.library = vim.list_extend({ vim.fn.expand \u0026#34;$VIMRUNTIME/lua\u0026#34;, vim.fn.expand \u0026#34;$VIMRUNTIME/lua/vim/lsp\u0026#34;, \u0026#34;${3rd}/busted/library\u0026#34;, -- Unit testing \u0026#34;${3rd}/luassert/library\u0026#34;, -- Unit testing \u0026#34;${3rd}/luv/library\u0026#34;, -- libuv bindings (`vim.uv`) }, vim.fn.glob(vim.fn.stdpath \u0026#34;data\u0026#34; .. \u0026#34;/lazy/*\u0026#34;, true, true)) --- 以上代码的作用在于，自动的将所有插件的源代码目录包含进 lua_ls 的补全范围内。 end vim.lsp.config(\u0026#34;lua_ls\u0026#34;, { settings = lua_ls_settings, }) 这个逻辑可能稍显复杂，不过除了一个判断是否在 nvim 配置文件目录下的一个函数，和与此对应的处理方式以外，其余的逻辑都是非常简单的。\n环境配置指南/编辑器 – Neovim 安装配置教程（基于 NvChad） 文章的作者还使用了 mason-lspconfig 以自动化安装对应的语言服务器，我没有这个方面的需求，而且代码也比较长，就没有仔细看了。\n掌握了这些，就足以配置其他语言服务器特有的功能了。这里提一嘴 clangd，上面的有关 inlayhint 的检测不会被 clangd 触发，如果你想使用 clangd 提供的 inlayhint ，可以像我这样硬编码它，反正我们也知道它支持这个功能。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vim.lsp.config(\u0026#34;clangd\u0026#34;, { on_attach = function (_, bufnr) vim.lsp.inlay_hint.enable(true, { bufnr = bufnr }) end, single_file_support = true, cmd = { \u0026#34;clangd\u0026#34;, \u0026#34;--clang-tidy\u0026#34;, \u0026#34;--j=12\u0026#34;, \u0026#34;--background-index\u0026#34;, \u0026#34;--header-insertion=never\u0026#34;, \u0026#34;--inlay-hints=true\u0026#34;, \u0026#34;--fallback-style=LLVM, indent=4\u0026#34;, } }) 有关 lsp 的主体就大概这么多内容了，其实并不复杂，但是刚接触的时候确实很容易混淆。\n到目前为止，你已经可以拥有一个性能不错的编辑器了。NeoVim 还支持许多拓展，比如格式化，Copilot，背景美化等等，这些内容你可以查看知乎的那篇文章。可以说，你在 vscode 上能够体验的功能，NeoVim 都能够实现，而且更快，缺点是配置起来比较麻烦。但一旦配好了，你就可以随时随地的从 github 上下拉配置，而且自己亲手打造一个自定义的编辑器，也很有趣不是吗?\n结语 其实这篇文章更像是我对于知乎那篇文章的一些细节与遗漏的一些内容进行补充与完善，写它也是为了加深我对这个工具的熟悉程度。最后附上我自己的仓库连接 。\n","date":"2025-09-23T01:31:05+08:00","permalink":"https://anfsity.com/p/neovim/","title":"Neovim"},{"content":" Around the right track baby we ain\u0026rsquo;t going back\n― Fly To Meteor (Milthm Edit) Encounter 起因是因为这么一段代码\n1 2 #define trace(...) \\ RecursionTracer tracer_##__LINE__(__func__, #__VA_ARGS__, ##__VA_ARGS__) 在我多次调用的时候,出现了如下报错\n1 2 3 4 5 6 7 8 9 int f(int x) { trace(x); if(x == 1) { return 1; } int res = x * f(x - 1); trace(x, res); return res; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 clang++ -std=c++23 -g -Wall -Wextra new.cpp -o new new.cpp:55:5: error: redefinition of \u0026#39;tracer___LINE__\u0026#39; 55 | trace(x, res); | ^ ./debug.hpp:213:21: note: expanded from macro \u0026#39;trace\u0026#39; 213 | RecursionTracer tracer_##__LINE__(__func__, #__VA_ARGS__, ##__VA_ARGS__) | ^ \u0026lt;scratch space\u0026gt;:328:1: note: expanded from here 328 | tracer___LINE__ | ^ new.cpp:47:5: note: previous definition is here 47 | trace(x); | ^ ./debug.hpp:213:21: note: expanded from macro \u0026#39;trace\u0026#39; 213 | RecursionTracer tracer_##__LINE__(__func__, #__VA_ARGS__, ##__VA_ARGS__) | ^ \u0026lt;scratch space\u0026gt;:326:1: note: expanded from here 326 | tracer___LINE__ | ^ 1 error generated. make: *** [Makefile:10: new] Error 1 这个时候我对宏还几近一无所知, 对于这样违背常识的报错感到很困惑. 查阅资料解决问题后便决定写一篇总结。\nC Preprocessor 我们知道, 你写下的 C/C++ Code 从源代码到可执行文件一般会经历四个步骤 :\n细节可以查阅这篇文章The four stages of the gcc compiler: preprocessor, compiler, assembler, linker. C preprocessor 是一个 text file processor ( 文本文件处理器 ), 它主要在编译过程的第一个阶段 \u0026ndash;预处理阶段\u0026ndash; 对源代码文件进行操作，主要提供四个功能1 :\nfile inclusion (文件包含) macro expansion (宏展开) conditional compilation (条件编译) line control 不过要注意的是, C preprocessor 仅仅是一个文本处理器, 它并不明白 C/C++ 的语法, 这在很多时候, 会导致一些危险的行为。\nFeatures File inclusion C 预处理器中有两个用于包含文件内容的指令 :\n#include (source file inclusion ) 。 #embed (resource inclusion )。 Source file inclusion 就是常见的 #include \u0026lt;iostream\u0026gt;, C 预处理器会将 iostream 里面的内容包含到源代码中。\n对于标准库和系统级头文件一般使用 \u0026lt;\u0026gt;, 对于本地或者用户自定义的头文件, 则使用 \u0026quot;\u0026quot; . C 预处理器会针对这种形式上的不同使用不同的搜索策略。\nResource inclusion 在 C23 和 C++26 中引入 #embed 预处理指令, 允许你在编译期间将二进制文件的内容嵌入到源代码当中，生成一个静态的常量数组。\nConditional compilation 可以理解成适用 C 预处理器的 if-else 结构.\n比如 :\n1 2 3 #ifdef VERBOSE std::cerr \u0026lt;\u0026lt; \u0026#34;trace message\u0026#34; \u0026lt;\u0026lt; std::endl; #endif 相关文档介绍Conditional compilation Macro string replacement 直观上的来讲, 宏就是一个 snippet 的别名, 在预处理阶段， C preprocessor 会扫描源代码，将所有的宏替换成其预先定义好的内容。\nObject-like object-like macro 定一个别名, 最终预处理器将其替换为实际内容. 它不接受参数, 没有办法实例化. 例如 :\n格式为 # define identifier replacement-list new-line\n1 2 #define PI 3.14 #define int long long Function-like function-like macro 行为类似于函数，定义的宏后面必须紧跟一对括号，不能有空格。支持传入参数, 也可以让参数为空. 例如 :\n1 #define MAX(a, b) std::max(a, b) Operators Defined operator defined 是一个一元谓词, 表示当 ** 宏被定义时, defined 为真, 否则为假.\n一下两种方式都可以调用 defined :\n1 2 #if defined(MY_MACRO) #if defined MY_MACRO Token stringification operator # 是一个 operator, 代表一个运算, 而不是一个标识. # 将一个标记转化为一个字符串, 并且会自动添加转义符号.比如 :\n1 #define str(s) #s str(\\n) expands to \u0026quot;\\n\u0026quot; and str(p = \u0026quot;foo\\n\u0026quot;;) expands to \u0026quot;p = \\\u0026quot;foo\\\\n\\\u0026quot;;\u0026quot;.\n1 2 3 4 5 6 7 8 void printFunctionName(std::string s) { //... } void foo() { //... printFunctionName(#__func__); } Token concatenation 也就是 ## , ## 作为一个 operator, 把标记两个标记连接成一个. 也就是把两个字符串拼接. 比如 :\n1 #define DECLARE_STRUCT_TYPE(name) typedef struct name##_s name##_t DECLARE_STRUCT_TYPE(g_object) expands to typedef struct g_object_s g_object_t.\nThe Order Of Expansion 除了上面提到过的那些, 还有一些常见的 features, 比如 预定义宏, #warning , Line control 之类的。\n不过, 这些都不重要！现在我们把目光放回最开始的那个 bug 。\n我的本意是想要自动创建一个独一无二的对象, 但是编译器提醒我们, 重复定义了 tracer__LINE__ . 根据编译的报错提示，我们发现，问题的根源在于 __LINE__ 根本没有展开 。\n接下来就是本节的难点了。\nObject-like Macro Expansion 首先来思考这样一个问题，考虑如下代码 ：\n1 2 #define A B #define B A 在我们使用宏 A 的时候，会不会无限展开下去呢？\n答案是肯定不会的。\n我们用一个例子来说明 object-like Macro 的递归展开规则 ：\n1 2 3 4 5 6 #define arg1 arg1 | arg2 | arg3 #define arg2 arg1 2 #define arg3 arg2 3 arg1 // 被展开为 arg1 | arg1 2 | arg1 2 3 递归定义禁用集 U 表示 ：「从递归的上一层的 U 与上一个宏的并集」, 最开始 U 被定义为 $∅$ ，表示当前宏不是被任何其他宏展开得来的。\n第一步，arg1 维护的 U 是空集，将 arg1 (1) 展开为 arg1 | arg2 | arg3 (2)， 这里对应图里的第一个 expand 。 第二步，我们从左向右扫描，首先遇到 arg1 (2) ，他的维护的集合已经包含了 {arg1} ，那么 arg1 (2) 就不应该被展开，它保持原样。接着遇到 | ，| 不是宏，跳过。然后我们遇到第二个宏 arg2 (2) , 他维护的集合 {arg1} 不包括 arg2 ，arg2 (2) 被展开为 arg1 2 。跳过 | ，展开 arg3 (2) 为 arg2 3 ，此时 arg3 维护的集合变成 {arg1, arg3} 。 第三步，展开从上一个 arg3 继承来的 arg2 (3) ，它维护的集合是 {arg1, arg3} ，将其展开为 arg1 1 。 展开过程结束，最终结果为 arg1 | arg1 2 | arg1 2 3 。\n我这图写的稍微有些误导性，需要指出的是，这个 expand 不是像 bfs 那样逐层展开的，而是像 dfs 那样遇到就展开到底部再返回。\n可以发现，整个递归过程构成一颗先序遍历的递归树。我们可以用这种方式很好的理解整个 object-like 宏的展开规则。\n回到先前的 case ：\n1 2 #define A B #define B A 那么答案就很显然了，依赖于使用的宏是 A 还是 B ，而且只会被展开一次。\nFunction-like Macro Expansion function-like 宏以如下顺序展开2:\nStringification operations are replaced with the textual representation of their argument\u0026rsquo;s replacement list (without performing expansion). Parameters are replaced with their replacement list (without performing expansion). Concatenation operations are replaced with the concatenated result of the two operands (without expanding the resulting token). Tokens originating from parameters are expanded. The resulting tokens are expanded as normal. 还有一个额外的特性：\n每次展开结束后，identifier 会向后看一个 token 判断是否构成一个新的 function-like 宏。3 我们来看几个例子来解释这几个步骤 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 #define COMMA , #define CALL(f, args) f(args) #define FUNC(a, b) a - b CALL(FUNC, 1 COMMA 2) // 1 - 2 #define EMPTY #define FOO(a, b) a + b #define BAR(x) FOO x BAR((1, 2)) // 1 + 2 参数优先展开。\n首先展开 CALL ，f 对应 FUNC, args 对应 1 COMMA 2 , COMMA 是宏，优先展开为 , ，展开后变成 FUNC(1, 2) 。\n每次展开结束后，identifier 会向后看一个 token 判断是否构成一个新的 function-like 宏。\n预处理器向后看，发现 FUNC(1, 2) 可以被匹配函数式宏，展开为 1 - 2。\n1 2 3 4 5 6 7 8 9 10 11 12 #define STRINGIZE_IMPL(x) #x #define STRINGIZE(x) STRINGIZE_IMPL(x) #define CAT_IMPL(a, b) a##b #define CAT(a, b) CAT_IMPL(a, b) #define VAL 123 // STRINGIZE(VAL) -\u0026gt; \u0026#34;123\u0026#34; // STRINGIZE_IMPL(VAL) -\u0026gt; \u0026#34;VAL\u0026#34; // CAT(VAL, VAL) -\u0026gt; 123123 // CAT_IMPL(VAL, VAL) -\u0026gt; VALVAL 参数列表里的参数会被优先展开。但如果该参数在替换列表中被 # 或 ## 所调用，那么该参数不展开。\nSTRINGIZE(VAL) 被展开为 \u0026ldquo;123\u0026rdquo; ，但是 STRINGIZE_IMPL(VAL) 就会先展开为 #VAL 再展开为 \u0026quot;VAL\u0026quot; 。\nCAT 同理。\n1 2 3 4 5 #define A(x) B(x) #define B(y) A(y) A(1) // A(1) 过程和 object-like 一样，首先 A(1) 的禁用集 (U) 是 $∅$，被展开为 B(1) ， U 中添加 {A} ，接着再展开成 A(1) ，U 中为 {A, B} 终止展开。\nU 中元素包含所有从上一次展开的宏，不一定是递归展开的宏。\n1 2 3 4 5 6 7 8 9 10 11 #define BAR() 1 BAZ() #define BAZ() BAR #define FOO(x) BAR() - x() FOO(BAR()) // BAR() -\u0026gt; 1 BAZ() // BAZ() -\u0026gt; BAR // FOO(BAR()) -\u0026gt; FOO(1 BAR) -\u0026gt; BAR() - 1 BAR() // ...........................U{FOO}...U{BAZ, BAR} // BAR() - 1 BAR() -\u0026gt; 1 BAR - 1 BAR() // --- end --- // 理解了以上内容后，我之前遇到的 bug 也就很容易明白错在哪里了，也就是上面 function-like Marco 展开的第二个例子说的。\n也很容易修复，利用一个辅助宏，先展开参数再 ## 上去就行了。\n1 2 3 4 5 #define TRACE_EXPAN(counter) tracer_##counter #define TRACE_HELPER(counter) TRACE_EXPAN(counter) #define trace(...) \\ RecursionTracer TRACE_HELPER(__COUNTER__)(__func__, #__VA_ARGS__, ##__VA_ARGS__) variadic macro __VA_ARGS__ 比较简单，需要需要注意的是使用的时候应该加上 ## :\n这是为了防止传入参数个数为 0 的时候，, 剩余。使用 ## 可以把这个 , 吞掉。\n1 2 3 #define LOG(fmt, ...) printf(fmt, ##__VA_ARGS__) LOG(\u0026#34;User %s\u0026#34;, \u0026#34;Alex\u0026#34;) // -\u0026gt; printf(\u0026#34;User %s\u0026#34;, \u0026#34;Alex\u0026#34;); LOG(\u0026#34;System started.\u0026#34;); // -\u0026gt; printf(\u0026#34;System started.\u0026#34;); 在 gcc 拓展 中，实现了一个宏 __VA_OPT__ 表示一个参数是 optional 的，于是上面的代码可以改成 ：\n1 #define LOG(fmt, ...) printf(fmt __VA_OPT__(,) __VA_ARGS__) 表示如果 ... 不为空，就在这里插入一个 , 。\nDelayed expansion 1 2 3 4 5 6 7 8 #define A() 123 #define EMPTY() #define DEFER(id) id EMPTY() #define EXPAND(...) __VA_ARGS__ DEFER(A)() EXPAND(DEFER(A)()) 考虑 DEFER(A)() 宏，当他展开到 A EMPTY()() 的时候，EMPTY() 被展开，此时结果为 A () ，注意这一轮扫描已经结束了。在 DEFER(A)() 这一次宏展开的重新扫描过程中，A 和 () 无法构成一次函数式宏调用，因此展开被延迟了。注意在此时 A () 被展开成 A () 的前一时刻的 U 是 {DEFFER, EMPTY} ，但是当生成 A () 后，U 被销毁，重新变成空集。\n当我们给这个宏的外面再套一层壳的时候，EXPAND() 宏使得预处理器重新扫描 A () ，它被识别为一个函数式宏，展开成 123 。注意这个时候 A () 的 U 被消除了，展开完后的 U 是 {A} 而不是 {DEFFER, EMPTY, A} 。\n我们在这里重新提到了禁用集 U ，是因为它在接下来这个魔法中发挥了至关重要的作用。\nA Little Magic 1 2 3 4 5 6 7 #define BAR_I() BAR #define BAR() 1 BAR_I BAR () () () // U {} -\u0026gt; BAR_I () () // U {BAR} -\u0026gt; BAR () // U {BAR_I} 注意！在执行上一步的展开时，U 被消除了 -\u0026gt; BAR_I // U {BAR} 此处也消除了上一轮的 U 也就是说，每当我展开过程中出现一个新的 function-like 宏时，这个新的 function-like 宏不会继承它源头的 U 。\n我们利用刚才的延迟展开，可以实现以下代码 ：\n1 2 3 4 5 6 #define BAR_I() BAR #define BAR() DEFER(BAR_I)()() 1 BAR() -\u0026gt; BAR_I()() 1 EXPAND(BAR()) -\u0026gt; BAR_I()() 1 1 EXPAND(EXPAND(BAR())) -\u0026gt; BAR_I()() 1 1 1 这说明宏可以构成一个有限的递归栈，进而说明了宏是图灵完备的。\n以上代码来自于宏定义黑魔法-从入门到奇技淫巧 (5) - 实现图灵完备的宏 。\nX-Macros 假设我们有一个结构体 User ，我们需要将它序列化为 JSON 字符串，也要能够从 JSON 字符串中解析出来。\n1 2 3 4 struct User { std::string name; int id; }; 我们的目的是自动生成下面功能的函数\nvoid toJSON (const User \u0026amp;user, std::ostream \u0026amp;os); void fromJSON (User \u0026amp;user, const JsonObj \u0026amp;json); 我们创建一个 userMembers.def 文件，列出 User 结构体的所有成员。\n1 2 3 4 // userMembers.def // X(type, name) X(std::string, name) X(int, id) 在我们的主代码中 ：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include ... // 各种头文件包含 struct User { #define X(type, name) type name; #include \u0026#34;userMembers.def\u0026#34; #undef X // serMembers.def 的内容被展开后，立马 undef X，自动生成了结构体 User 。 }; void toJSON (const User \u0026amp;user, std::ostream \u0026amp;os) { os \u0026lt;\u0026lt; \u0026#34;{\u0026#34;; bool first = true; #define X(type, name) \\ if (!first) { os \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; } \\ os \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; #name \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;:\u0026#34; \u0026lt;\u0026lt; json_quote(user.name); \\ first = false; // 宏定义结束 #include \u0026#34;userMembers.def\u0026#34; // userMembers.def 里面的内容被自动展开为上面的内容 // 比如 ： // Expands to // if (!first) { // os \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; // } // os \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; \u0026#34;name\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;:\u0026#34; \u0026lt;\u0026lt; json_quote(user.name); // first = false; // if (!first) { // os \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; // } // os \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;\u0026#34; \u0026lt;\u0026lt; \u0026#34;id\u0026#34; \u0026lt;\u0026lt; \u0026#34;\\\u0026#34;:\u0026#34; \u0026lt;\u0026lt; json_quote(user.id); // first = false; #undef X os \u0026lt;\u0026lt; \u0026#34;}\u0026#34;; } void fromJSON (User \u0026amp;user, const JsonObj \u0026amp;json) { #define X(type, name) json.get_to(#name, user.name); #include \u0026#34;userMembers.def\u0026#34; #undef X } 通过 X 宏，可以实现自动生成结构体，自动生成对应的解析函数，唯一要做的修改就是在 userMembers.def 里面添加或删除变量。\n1 2 3 4 5 // userMembers.def // X(type, name) X(std::string, name) X(int, id) X(int, score) Macros FAQ Operator Precedence 1 2 3 #define SQUARE(x) x * x int result = SQUARE(3 + 2); 我们期望得到结果 5 ，但是实际上得到是 3 + 2 * 3 + 2 。\nRepeated Evaluation of Arguments 1 2 3 4 5 6 #define MAX(a, b) ((a) \u0026gt; (b) ? (a) : (b)) int x = 5; int y = 8; int z = MAX(x++, y++); // x 期望是 6, y 期望是 9, z 期望是 8 但实际上宏在展开的过程中 x++ ， y++ 都出现了两次，这个行为是未定义的，结果未知，但肯定和期望值不同。\nName Clashes 宏的定义是全局的，这就很容易造成命名冲突。\n不过在 c++20 中，引入了模块化来解决 #include 和宏所带来的全局污染问题。\nSemicolon Swallowing 1 2 3 4 5 6 7 8 9 10 11 #define LOG(msg) printf(\u0026#34;%s\\n\u0026#34;, msg); if (condition) LOG(\u0026#34;It was true\u0026#34;); else do_something_else(); // if (condition) // printf(\u0026#34;%s\\n\u0026#34;, \u0026#34;It was true\u0026#34;);; // else // do_something_else(); 当然你也可以在第一个分支里选择不加分号，不过这种别扭的行为还是禁止的为好。\n常用的技巧是使用 do-while(0) 语句来形成一个完整的语义。\n1 2 3 4 5 6 7 8 9 10 #define LOG(msg) \\ do { \\ printf(\u0026#34;%s\\n\u0026#34;, msg); \\ } while(0) // 展开后: // if (condition) // do { ... } while(0); // 这是一个单一的语句，需要一个分号 // else // ... 宏还有一些缺点，比如无法调试，阅读困难等等。现有的序列化和反序列化，枚举转化为字符串 ，ORM 等等操作都需要借助宏来实现，标准库的源代码也总会有宏的身影。总的来说，宏并不是一个很好的东西，但他也是一个不可或缺的东西。在现代 cpp 中，可以使用 template , constexpr 等等来替换宏，但仍然有很多地方宏是不可被替代的。这就是为什么 c++26 的反射被那么多人期待。\nReferences 参考自维基百科 \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n参考自维基百科 \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n宏定义黑魔法-从入门到奇技淫巧 (3) - function-like 的宏展开 \u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-08-06T12:36:02+08:00","permalink":"https://anfsity.com/p/macro-in-c-cpp/","title":"Macro in C/CPP"},{"content":"Index 1400 Summary 1300-1400 Summary 1400A Summary 1400B Summary 1400C-D ","date":"2025-07-03T21:43:26+08:00","image":"https://i.111666.best/image/nTg9J6fvwtRlQlqoZluwMQ.jpeg","permalink":"https://anfsity.com/p/grinding-codeforces/","title":"Grinding Codeforces"},{"content":"目前施工正在进行中，目前仅做完 Arch Lab\n目录 Data Lab Bomb Lab Attack Lab Arch Lab Cache Lab\nShell Lab\nMalloc Lab\nProxy Lab\n实用链接 CSAPP Lab 备份 CSAPP 电子汉化版 不周山的博客 CSAPP 重点解读 CSAPP Notes 感想 ","date":"2025-06-27T19:29:03+08:00","image":"https://i.111666.best/image/WlxWjkwsJem51r4djTr0Bl.jpeg","permalink":"https://anfsity.com/p/csapp/","title":"CS:APP"},{"content":" 以下所有内容引用自 上海交大生存手册 突击备考 新书或者根本没买过书；没听过讲，或者没上过课，没独立写做过作业甚至没交过作业，但是离考试只有三天时间，这样的情况，对我们中的大部分人，对大部分课程的考试，并不意味着绝望。只要你有心，三天造十万支箭都没有问题，更何况考试？\n我并不是一个“优秀”的学生，因为在大学四年内，我没有一个光鲜的GPA。虽然我可以很骄傲地说，这四年，我把本该上课的时间用来做了更重要更有意义的事情。但是面对考试能拿到一个过得去的分数也是我的底线。我相信，并不是每个交大的学生都会很乐意的接受挂科、甚至退学这样的经历。\n并不是所有同学都能够像我一样幸运地找到一些比上课正确一百倍的事情去做。而我们在这一节里分享的，更多是拿到70分而不是90分的经验。如果你在阅读本书《立志篇》之后，你的目标仍然是得到全A。或者说，你找不到比上课更有意义的事情，那么你们应该去自习室，而不是在这里听我扯淡。\n突击备考的一个必要前提是，我们把时间用来做了更重要的事情。\n从学习知识而言，上课是一件奢侈的事情。对突击复习考试而言，所谓的“慢工出细活”也是奢侈的。如果你认为这门课不重要，请不要在两个星期以前开始复习，因为两个星期足够让你忘掉其中一大半的知识。通常，有效的复习是在3天之内开始的，因人略有差异。我不否认个人之间的IQ存在差异，但是这种差异是如此之小，以至于甚至不能成为左右考试成绩的主导因素。充其量，有些同学可以以笨鸟先飞的名义，把一门课程需要的复习时间从3天延长到5天，不会更多。当然，我们这里说的是针对大多数靠突击有可能能拿高分的课程。某些课程（诸如高等数学），显然是排除在我们的讨论范围之外的。一般一门课如果在历史上曾经挂了学院1/3以上的同学，那么对于该课程就需要提高警惕，慎重起见，甚至不要突击复习。\n注意复习范围 通常老师在复习课上都会把考点和考试范围告诉大家。对考点的正确解读可以让你事半功倍。比如，有一些脸皮比较薄的老师在划考点的时候，不喜欢明目张胆地说“这章不考”，而是会换一种更加委婉的说法，比如“这章的内容主要是介绍性的，有兴趣的同学们可以进一步拓展”。我相信大部分同学对于这样的话都能有正确的理解。至于考点的准确性，我认为我们不应该心中有任何疑问。虽然的确存在过老师说不考的地方真的考了，但是那种事情出现几率非常低，每学年近二十科考试，能出一次就不错。而且真的没复习到所带来的影响也不是决定性的。所以我们在考前最后一节课上，一定要毫无保留地相信老师、相信考点，并且，还要相信不同老师考点的交集。\n三天冲刺 3天之内，你需要准备的：\n课本（没错，就是这门课的教材）\n上课 PPT ，如果你的老师的PPT只是把教材原样照搬的话\n一位懂得这门课程的朋友\n平时作业列表\n全书考点（或者不考的点）的列表\n首先，请懂得这门课程的朋友吃饭，让他用半小时的时间，用通俗语言，按章节给你介绍这门课是做什么的，每一部分的考点是什么。吃饭结束之后，和他约考试前一天下一次见面的时间。然后翻开教材，看目录，将不需要考的内容划掉。回忆那位朋友的话，想象每一章的标题所代表的内容。以3倍速的速度将书翻一遍，无论看懂不看懂，进一步领会朋友所说的话，如果是理工类课程，争取每章自己总结一个能够说服自己的理论。\n找出平时作业列表，当然了，通常来说你肯定是一道题目也不会做。这很正常，你不必为此感到紧张或绝望，不上课直接做作业本这本身就是让人绝望的。你需要做的是，把作业和答案放在一起，开始扫荡，用半天的时间保证你知道答案的那些数是怎么算出来的。注意：你没有时间一道一道题目去做，把你高中老师告诉你的要扎扎实实的说法抛到脑后吧，扎扎实实你应该好好上课去。\n如果顺利搞明白了作业，你现在大约可以考40分了。你会遇到一些情况，比如作业题目你看不懂，你不知道答案上面那些鬼东西是在说什么。勾上相关的章节。将刚刚勾过的章节逐一以两倍速浏览，注意看公式和黑体字。你会发现你小学套公式，中学套公式，到了大学，还是在套公式。接下来你只需要把你不会的题代到相关章节的相关公式下，做好字母和中文的对应翻译工作。\n现在你不会的题目应该很少了。这会儿开始看 PPT。注意，不要一开始就看 PPT，当然，更不要一边看 PPT 一边看 B站 。知识是书上来的，PPT 是演讲稿，不要因为懒不去翻书。\n有些关键点，看课本看不懂，就去看 PPT （因为教课书在关键点上总喜欢使用脑残体），看 PPT 看不懂（部分 PPT 写的和书一样脑残，而且还有错），就去看课本；再不行还有 Google 和 GPT。如果在非常关键的知识点遇到了无论如何都不明白的情况，那就赶紧抄起电话，找到你的那个朋友求助。\n在考试之前前一天之内，把所有的公式或者解题步骤都写在一张纸上，用剩下除睡觉之外的时间去熟悉这张纸，根据例题的形式在脑中按照这个套路演练。但是不要试图拿着这张纸作弊——风险太大回报太低不合算；把上面的内容抄到桌子上也不可取——在考试时，你根本不会有机会去把桌面当图书馆查来查去，这样做只会让你心虚，一边惦记这个事儿一边浪费大量时间。考试的时候，尽量不要抄周围同学的，因为你周围的人复习的不一定比你好。\n如果你前面工作做的足够好，那你一定要有信心。题目要么你都会做，要么大家都不会做。关于选择题，请尽量用你的大脑，不要胡猜。要记住这是你和出题人 IQ 的比拼，而不是 rp 的比拼。\n考试结束后 一哭二闹三上吊是下下策，只有当其他的手段都无效时，再考虑这样的办法。对于某些老师，你唯一的选择就是考到80+。对于另外一些比较好说话的老师，你可以跟他们谈谈你对课程的理解，以及对这门学科的看法。在谈话中，最佳的切入点是学术。你要尽可能地讨论学术方面的问题，而不是去强调你要出国，你要保研。每个人都有各种各样的私人借口来要求一个好的成绩，但是这些借口并不一定都能成为让老师帮你一把的理由。\n请注意，如果你希望老师帮你一把，尤其是你大概率要挂科的时候，请一定线下前往老师的办公室或电话沟通，不要用邮件或微信让老师难办。如果老师在提交成绩前放出了成绩预览，那么这就是你努力的时机了。但即使你的成绩单上已经是Fail，也并不是没有办法的，虽然修改已经提交的成绩超过一定比例会认定为教学事故，不过要是你面临着退警，请不要放弃任何机会。但请注意不要违反法律，不要出卖自己的尊严。\n复习箴言 1.请保证这三天时间80%以上的利用率，睡觉也要尽量克制些。\n2.不要打游戏，也不要刷B站，如果考试跟电脑无关，尽量少开电脑。\n3.多参加专业群内的讨论，狠狠鄙视那些不愿意跟大伙分享心得的家伙。\n4.你可以总结出对这门课程的几个独创理论，比如你对某一章节的独特理解，分享这些经验，这会帮助很多人。\n5.如果你有考点搞不太清楚，不要浪费过多的时间试图让自己“真的搞懂”。只要你能把解题步骤记牢，保证大部分习题都算对就是胜利。\n6.有一些课程没有习题，或者说老师布置的作业没有代表性，那你必须需要去购买一本习题集，或者看别人的上课笔记\n7.做题一定要找有答案的，你没有时间去自己确定你做的每一个答案是否正确。\n8.如果可能，把那些不是作业题的里面的一些有趣的内容也看一下。\n9.不要不去考试，无论如何，应该尝试一下。而且很有可能补考卷子和考试卷子重合度较高。\n10.考试结束后，无论感觉好坏，别忘了请辅导你的那个朋友吃饭。\n$end$\n个人感想 作为一个从小到大从来没有过考前突击经验的人，我当初刚看见这篇文章是十分震惊的。震惊的是，我从来没想过考试可以这么做。但震惊之余，我马上觉悟这个方法的可行性。\n当然，那个时候的我仅仅是对这篇文章留有比较深刻的印象，并有没亲自去做出实践。原因是我并没有找到我认为是更有意义的事，但现在不太一样了。\n在我亲自体验过这个方法之后，我决定写下一些感想以及总结。\n首先，面对考试拿一个还算过得去的分数也是我的底线，我并不乐意接受挂科，甚至退学的经历。\n突击备考只能针对突击备考有可能考到高分的课程。对于某些课程，由于其内容太过庞杂，比如数学分析，是不应该这么做的。同样的，对于一门课挂科率超过 30% 的话，对这门课就要提高警惕。\n但是，这是针对上海交大而言，对于我这种破大专就不一样了。个人估计，我这门专业在学校里面，几乎所有的课程都可以在 4days 以内速成。绝大多数的课程有三天是完全充足的，并且如果你完全按照计划执行的话，大概率分数不会低。至于那几个特例嘛，是因为那两门课的挂科率在 50% 左右。不过这也是我觉得这个专业为数不多有价值的课程了。\n先讲讲不在这三天之内要做的事情。首先，你需要收集这门课程的历年试卷，如果这门课一直是一个老师，那这门课程的考试内容往往持续十几年都不会改变。就算老师一直在变化，以前的试卷也有很大的参考价值。\n如果可能的话，提前准备全书考点，每次上课后向听课的同学请教这节课的知识点。这花不了多少时间，但是对后期的准备是十分方便的。打听打听这门课程老师的脾性。如果可能的话，尽可能把平时分刷高（虽然不太可能）。\n然后就是突击备考中间的事情了：\n你这门课的课本可能极其的差(如果你听不懂这门课，绝对不是你太蠢，而是这本教材/老师太垃圾)。这个时候你就需要去网上找资料，这个过程可能是十分费劲的，为了应对这种情况，我建议你可以提前准备。 关于资料，肯定是应试资料，一定要带有答案和习题，可以没有解题过程，考研资料也许是一个不错的选择。\n你可能找不到一门懂得这么课程的朋友。或者说有这么一个人，但是很难沟通。也有可能，他是你的朋友，他也明白，但是不知道怎么讲给别人听。这个时候就只能求己了，所幸现在有各种 LLM 辅助，让这个环节不至于如此的致命。或者也可以去网上的相关交流群去提问，提问的时候请遵循提问的智慧。\n有些老师是不会给你具体的复习范围的，这个时候就需要你自己动手了。网上关于这门课的考点不一定是你的考点，你只能从作业和往年试卷中追求蛛丝马迹。\n有些老师十分恶心，所有的做过的作业全部私有化处理，不过这两年来我也只见过这一个老师，我无意评价老师的学识，责任。但这也无可厚非，毕竟，学校的作业\u0026ndash;说的难听些\u0026ndash;本就是依托💩。\n作业和习题是十分重要的。但是有些时候，老师布置的作业只会给你判断对错，并不给你答案。这个时候你需要去找一本贴合你的教材的习题参考册，结合你作业类型的题去刷。最重要的是，习题参考册并不总是这么简单就可以找到的。请提前准备防止这种情况发生。\n睡觉少睡一些，如果考试在上午的话，可以考虑通宵。当然，长达20多个小时的长线作战是十分痛苦的。你不可能完完整整的利用这几十个小时的每一分，每一秒，你应该尽可能高效的利用这每一分，每一秒。请劳逸结合，但也不能过度放松。\n请相信墨菲定律，你看见而不想学的知识点，极有可能你会在考场上遇到。所以，请不要遗漏任何一个知识点。\n如作者所言，不要去看视频听课。你不是学会一个知识点，而是学会做这个知识点的题。给每个章节一个或几个结论，可以很好的帮助你备考。\n文中提到了一些课程内的交流群，如果你的课程交流群一个活人都没有，请放弃这条道路。不过，向老师提问是一个不错的选择。\n向老师提问不如向 LLM 提问，原因不必多说，但各个学校有各个学校的情况，这我也不好笃定如此。总之，随机应变即可。\n相信这本书的内容，我认为里面的内容是极为科学，可行性极强的。如果不知道这种方法，针对突击备考会难上几个系数。 有些可惜，高中的我，没有掌握所谓的应试技巧。虽然说学校的层次不会影响我的学习，但是学校的资源会影响我的学习和我身边的人。抛开这些不谈，深谙考试技巧的我们，没有道理会在这些所谓的课程中拿到不及格的成绩。\n最后的最后，请记住，突击备考的前提是：\n我们把时间，用来做了更加重要的事。\n","date":"2025-06-25T00:29:21+08:00","permalink":"https://anfsity.com/p/three-day-sprint/","title":"Three Day Sprint"},{"content":"起因是我重装 win10 的时候把我整块磁盘都清空了，事后才发现我没有给我的博客备份😭。遂想，反正已经要 remake 了，索性换一个框架，于是便从 hexo 换成了 hugo。\n虽然说所有的数据我在 vercel 上都有，但是都不是 markdown 的储存形式，我也懒得再从网上找工具实现 html 转 markdown 了。就仅仅迁移了几篇还算有意义的文章。\n其实因为是在 linux 上写的，本地有数据 (。\n新 hugo 主题对比我原来的 hexo 主题有些部件是不完全的，要自己造轮子。比如回到顶部的小按钮，代码块折叠等等。扯句别的，说实话我想吐槽以下那些 live2d 小人，全屏的动态效果和一些鼠标特效等等，难道看的视觉上不会很难受吗()。\n其实也没什么好总结的，无非就是看看文档，找些博客做参考，解决一些疑难杂症。现在有 LLM 辅助，应该是比以前要舒服很多的。但是还是不能依赖于 AI, AI 在某些场景太弱智了。\n不过既然写了一篇博客，就水水字数吧。\n最近把刚上学的时候感兴趣的一个东西实现了，那个时候是在是不懂这些。唉唉，我已经老了啊。\n我记得在 csdiy 上看到一个评论说可以借助 Azure 的 vps 自建节点，便想自己动手试一试，就搞出来了一些明堂。\n首先你要拥有一个 Azure 的学生认证账户，国外学生认证的申请模式都大同小异，也有很多教程，在此就不再赘述。不过如果申请了 github education 的话是会方便很多的，而且 github 的福利也是非常不错的。\n总的来说，思路就是利用 Azure 的虚拟机，通过 OpenVPN，WireGuard 等虚拟加密网络隧道，绕过 GFW,以实现俗称 “翻墙”，“魔法”的效果。\n实现过程倒是很简单，按按鼠标就行。假设你已经拥有了 Azure 的一个学生认证账户(学生认证账户每年可以获得 100$ 的免费额度使用，可重复申请)。那么现在你要做的操作就是打开 Azure 的主页，创建一个虚拟机。\nUbunutu 上就没有好用一点的截图工具吗。。。其他的默认就好不需要修改。\n其他的一路默认就行，注意一下在网络那部分，如果 IP 能该动态就改动态，静态相对而言是很贵的。\n等待虚拟机创建完毕，使用 ssh 进行连接。\n新装的 Ubuntu，输入命令\n1 sudo apt update 爽一爽国外的网速。\n1 sudo apt install wireguard 按照提示即可，然后获得一个 conf\n1 cat *.conf 把输出内容往你本地下载 wireguard app上一丢就行，注意不要同时和另一个软件进行代理，可能会出现不知名的错误。尝试访问 google 和 youtube，如果成功就代表一切都 OK 了。好耶。\nbtw,好快的网!\n1 2 3 4 5 6 7 8 9 10 11 12 13 azureuser@anfsity:~$ ./speedtest Speedtest by Ookla Server: Misaka Network, Inc. - Seattle, WA (id: 50679) ISP: Microsoft Azure Idle Latency: 4.73 ms (jitter: 0.22ms, low: 4.64ms, high: 5.15ms) Download: 5400.52 Mbps (data used: 6.6 GB) 7.65 ms (jitter: 2.84ms, low: 4.57ms, high: 18.57ms) Upload: 922.00 Mbps (data used: 426.3 MB) 8.25 ms (jitter: 1.37ms, low: 4.27ms, high: 12.19ms) Packet Loss: 0.0% Result URL: https://www.speedtest.net/result/c/aae9caa4-acc5-418c-b1b2-419950514d5b 羡慕啊。\n$upd:$\n当初写这个的时候并没有部署到 vercel 上，推上去才发现本地能加载的 svg 图片在上面不行。。。然后我开了一个 fixbug 分支尝试去修复他(我不懂前端哇),进行了一些列改动后，愉悦的出现了神秘 bug ：\n1 2 3 500: INTERNAL_SERVER_ERROR Code: MIDDLEWARE_INVOCATION_FAILED ID: hkg1::5qkdl-1750825759600-8b738d10c1a2 最神秘的是，我在 fixbug 行不通的改动，同步到 main 反而行了，很神秘啊。既然能跑，我就不做改动了。\n","date":"2025-06-24T22:42:13+08:00","image":"https://i.111666.best/image/bFGnilXaGbPyJep2pc2y4L.png","permalink":"https://anfsity.com/p/%E8%BF%81%E7%A7%BB%E6%80%BB%E7%BB%93/","title":"迁移总结"},{"content":"素数筛法 引入 我们先来看一个最基本的一个判断素数的方法， 基于 $6k \\pm 1$ 优化。\n$Prove$\n对于所有正整数，都可以表示为 $6k + i$。\n由于以下情况 :\n$6k \\equiv 0 \\pmod{2}$\n$6k+2 \\equiv 0 \\pmod{2}$\n$6k+3\\equiv 0 \\pmod{3}$\n$6k+4\\equiv 0 \\pmod{2}$\n也就是说， 只有 $6k \\pm 1$ 存在可能为质数的可能， 所以我们只要检查 $6k \\pm 1$ 有没有可能是质数就行了， 这种方法也可以进行推广，但是要筛的数的范围也会随之变大， 不过时间复杂度也会变得更加优秀。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 template \u0026lt;typename type\u0026gt; bool isPrime(type n) { if(n \u0026lt;= 1) return false; if(n \u0026lt;= 3) return true; if(n % 2 == 0 || n % 3 == 0) return false; for(type i = 5; i \u0026lt;= n / i; i += 6) { if(n % i == 0 || n % (i + 2) == 0) return false; } return true; } 这种方法在求较小的质数时相当优秀和简洁， 但是在大范围查询的时候， 就不够适用了，这个时候，就该引入我们的质数筛法了。\n埃拉托斯特尼筛法 朴素版本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::vector\u0026lt;int\u0026gt; prime; std::vector\u0026lt;int\u0026gt; vis(N, 0); void Eratosthenes() { for(int i = 2; i \u0026lt;= N; ++i) { if(!vis[i]) { prime.push_back(i); if(1LL * i * i \u0026gt; N) continue; for(int j = i * i; j \u0026lt;= N; j += i) { vis[j] = 1; } } } } 时间复杂度为$O(n\\log \\log(n))$。\n平方根优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 std::vector\u0026lt;int\u0026gt; prime; std::vector\u0026lt;int\u0026gt; vis(N, 0); void Eratosthenes() { for(int i = 2; i * i \u0026lt;= N; ++i) { if(!vis[i]) { for(int j = i * i; j \u0026lt;= N; j += i) { vis[j] = 1; } } } for(int i = 2; i \u0026lt;= N; ++i) { if(!vis[i]) { prime.push_back(i); } } } 该版本对比朴素版本， 仅仅添加了平方根的优化， 时间复杂度为 $O(n\\log \\log(\\sqrt{ n })\\ +\\ n)$, 然而这样会显著优化时间。\n由于我们知道， 合数不可能为素数， 所以我们只需要对奇数进行检验即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 std::vector\u0026lt;int\u0026gt; prime; std::vector\u0026lt;int\u0026gt; vis(N, 0); void Eratosthenes() { prime.push_back(2); for(int i = 3; i * i \u0026lt;= N; i += 2) { if(!vis[i]) { for(int j = i * i; j \u0026lt;= N; j += (2 * i)) { vis[j] = 1; } } } for(int i = 3; i \u0026lt;= N; i += 2) { if(!vis[i]) { prime.push_back(i); } } } 虽然这些优化可以让埃筛达到一个很好的优化，甚至在某些场景比欧拉筛还要快捷，但是埃筛的时间复杂度终究不是 $O(N)$。\n欧拉筛 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void euler(int n) { std::vector\u0026lt;bool\u0026gt; vis(n + 1, 0); std::vector\u0026lt;int\u0026gt; primes; for(int i = 2; i \u0026lt;= n; ++i) { if(!vis[i]) { primes.push_back(i); } for(auto \u0026amp;prime : primes) { if(1LL * prime * i \u0026gt; n) break; vis[prime * i] = true; if(i % prime == 0) break; } } } [!hint]- 说明 核心 ： 欧拉筛确保每一个合数只被他自己的最小质因数筛去。\n$for\\ i\\ \\dots n$ ， 维护一个 prime 数组，表示到 $i$ 为止的所有质数。\n设 $p_{i}$ 为 $i$ 的最小质因子，在 $prime$ 数组中 : prime: $[\\underbrace{\\dots\\dots}_{\\text{p}_{j}}\\ , \\quad \\underset{\\substack{\\uparrow \\\\ \\text{pi}}}{\\boxed{p_i}}, \\quad \\dots ]$\n$p_{j}$ 表示所有小于 $p_{i}$ 的质数。 考虑 $newNum=i \\cdot p_{j}$ 。 已知 $p_{j}","date":"2025-05-25T14:07:56+08:00","permalink":"https://anfsity.com/p/%E6%B5%85%E8%B0%88%E7%AD%9B%E6%B3%95/","title":"浅谈筛法"},{"content":"简介 在算法计算中, 常常由于数据原因对答案进行大数取模. 对于加法和减法, 处理起来相对容易, 但是对于除法来说, 就较为复杂. 为了解决这个问题, 我们来探讨一下乘法逆元.\n定义 我们把 $a^{-1}$ 叫作 $a \\bmod p$ 的乘法逆元, 如果满足 $a\\cdot a^{-1} \\equiv 1 \\pmod p$.\n在高中数学中, 如果 $b\\cdot b^{-1}=1$, 就把 $b^{-1}$ 叫作 $b$ 的倒数, 即 $\\frac{a}{b} = a\\cdot b^{-1}$.\n乘法逆元的定义与之类似. 只不过作用域在 $\\bmod p$ 下.\n求解逆元 朴素枚举 由模运算的性质知道 $x \\equiv y \\pmod p$ 必有 $\\ 0\\leq y\\leq p-1$ 。\n我们可以从 $1$ 到 $p-1$ 枚举判断是否有 $i$ 满足 $a\\cdot i \\equiv 1 \\pmod p$。\n线性时间复杂度, 方法的优点是简单易想, 缺点嘛, 显而易见.\n费马小定理 如果 $gcd(a,p)=1$ 则 $a^{p-1}\\equiv 1 \\pmod p$。\n关于费马小定理的证明有很多种，这里给出一种我认为最优雅的一种证明。\n考虑两个序列 :\n$i:[1,2,3,\\dots,p-1]$\n$j:[a,2a,3a, \\dots,(p-1)a]$\n现在证明 $i,j$ 在 $\\bmod p$ 意义下同构。\n也就是说, 如果二者都有序, 序列 $i$ 和 序列 $j$ 在 $\\bmod p$ 意义下是完全相同的.\n假设 $j$ 序列中存在 $xa \\equiv ya \\pmod p \\ (1\\leq x,y\\leq p-1，x \\neq y)$， 由模运算的性质得 $x \\equiv y \\pmod p$， 由于 $1\\leq x,y\\leq p-1$ $x$ 一定等于 $y$. 假设不成立，原命题得证。\n然后，我们把 $i，j$ 序列乘起来得到 $(p-1)!\\cdot a^{p-1} \\equiv (p-1)! \\pmod p$ ，由于$gcd((p-1)!,p)=1$，化简为 $a^{p-1}\\equiv 1 \\pmod p$。\n看向乘法逆元的定义 $：a\\cdot a^{-1} \\equiv 1 \\pmod p$， 对费马小定理进行变形得到 $a\\cdot a^{p-2} \\equiv 1 \\pmod p$，这就说明 $a^{p-2}$ 是 $a$ 的乘法逆元。注意者只有在 $p$ 是质数的时候成立。\n对于计算 $a^{p-2}$ ，可以用快速幂做到在 $O(log(p-2))$ 计算。\n快速幂 : 1 2 3 4 5 6 7 8 9 10 11 12 i64 qpow(i64 base, i64 exp, i64 mod) { if(exp == 0) return 0LL; i64 res = 1; while(exp) { if(exp \u0026amp; 1) res = static_cast\u0026lt;i128\u0026gt;(res) * base % mod; base = static_cast\u0026lt;i128\u0026gt;(base) * base % mod; exp \u0026gt;\u0026gt;= 1; } return res; } 拓展欧几里得算法 拓展欧几里得算法是对欧几里得算法的拓展. 用来求解满足Bézout\u0026rsquo;s identity 的不定方程$a\\cdot x + b \\cdot y=gcd(a, b)$ 的解.\n对于这个方程 $$a\\cdot x + b \\cdot y=gcd(a, b)$$ 稍做变形, 有 $$a\\cdot x \\equiv gcd(a, b) \\pmod b$$在 $b$ 为质数的情况下, $x$ 为 $a$ 的逆元.\n显然, $x=1,\\ y=114514$ 是在 $b=0$ 时满足情况的一组解().\n现在假设我们已知 $x_{0},y_{0}$ 是当前状态的一组解, 令当前计算的 $gcd$ 为 $gcd(a', b')$ , 上一个方程为 $gcd(a, b)$ . 有 $b'=a\\ \\bmod \\ b,\\ a'=b$ .\n也就是 $$\\begin{aligned} \u0026 x \\cdot a + y \\cdot b = gcd(a,b) \\\\ \u0026 x_{0} \\cdot a' + y_{0} \\cdot b' = gcd(a', b') \\end{aligned}$$ 那么 $$x_{0} \\cdot b + y_{0} \\cdot(a\\ \\bmod \\ b) = gcd(a', b') = gcd(a, b)$$ 化简 $$x_{0} \\cdot b + y_{0} \\cdot\\left( a - \\left\\lfloor \\frac{a}{b} \\right\\rfloor \\cdot b \\right) =gcd(a, b)$$ 移项 $$y_{0} \\cdot a + \\left( x_{0} - \\left\\lfloor \\frac{a}{b} \\right\\rfloor \\cdot y_{0} \\right) \\cdot b = gcd(a, b)$$ 新的解 $x,y$ 为 $x = y_{0}, \\ y = x_{0} - \\left\\lfloor \\frac{a}{b} \\right\\rfloor \\cdot y_{0}$.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 template \u0026lt;typename T\u0026gt; constexpr std::tuple\u0026lt;T, T, T\u0026gt; Exgcd(const T \u0026amp;a, const T \u0026amp;b) { if(b == T{}) { return {a, T{1}, T{}}; } auto [gcd, x0, y0] = Exgcd(b, a % b); T x = y0, y = x0 - (a / b) * y0; return {gcd, x, y}; } template \u0026lt;typename T\u0026gt; constexpr T normalize(T x, T mod) { x %= mod; if(x \u0026lt; 0) { x += mod; } return x; } template \u0026lt;typename T\u0026gt; constexpr T Inv(T val, T mod) { auto [gcd, x, y] = Exgcd\u0026lt;decltype(mod)\u0026gt;(val, mod); assert(gcd == T{1} \u0026amp;\u0026amp; \u0026#34;The modular inverse does not exist.\u0026#34;); return normalize(x); } 线性递推 想象一下, 如果你的知识储备不够充裕, 你能否凭空想出一种 $O(1)$ 的算法来求解逆元. 这恐怕十分困难.\n不过如果有人告诉你，存在一种方法，可以在线性时间内求出连续 $N$ 范围内的逆元，你会怎么去思考？\n如果是线性时间的话，那要么我在 $O(1)$ 内求出一个数的逆元，要么我知道一个公式可以直接计算出逆元，要么我可以通过递推方程来得出，要么我可以通过某种方式均摊到 $O(1)$。\n明显，直接套公式有点不太现实，费马没想到，欧拉没想到，他们都没想到，我不太认为我自己可能会想到。直觉告诉我，递推是最有可能的方案。\n不妨试一试，令 $i\\ 是\\ 1\\dots n$ 里面的任意一个数， $base$ 情况是 $1^{-1} \\equiv 1 \\pmod p$ 。\n$$ i^{-1} \\equiv \\begin{cases} 1, \u0026 \\text{if } i = 1, \\\\ \\text{a previous situation}, \u0026 \\text{otherwise}. \\end{cases} \\pmod p $$我们大胆猜测这个已知情况和一个小于 $i$ 的数有关，让我们想一想，哪里可以存在小于 $i$ 的数又和 $i$ 有关呢？\n不妨注意到 $p = k\\cdot i + r$， 这个 $r$ 不就小于 $i$ 吗？ (注意到这个不是没有理由的， 我们已知的信息就只有 $p,i$ ， 而把一个数写成 $ki+x$ 的形式是一个非常常见的技巧. )\n让我们来好好把玩把玩这个等式 $: p=k\\cdot i+r$ ，$r=p \\bmod i$。\n先对等式两边取模得到 $$ 0 \\equiv k\\cdot i+r \\pmod p $$ 移项 $$ -k\\cdot i \\equiv r \\pmod p $$ 乘 $i^{-1}$ $$ -k \\equiv r \\cdot i^{-1} \\pmod p $$ 把 $i^{-1}$ 拆出来 $$ i^{-1} \\equiv -k\\cdot r^{-1} \\pmod p $$ 代换 $r$ $$ i^{-1} \\equiv -k\\cdot (p \\bmod i)^{-1} \\pmod p $$ 重写 $k$ $$ i^{-1} \\equiv -\\left\\lfloor \\frac{p}{i} \\right\\rfloor\\cdot (p \\bmod i)^{-1} \\pmod p $$ 改成正数 $$ i^{-1} \\equiv (p-\\left\\lfloor \\frac{p}{i} \\right\\rfloor)\\cdot (p \\bmod i)^{-1} \\pmod p $$哈哈，这是什么，我们得到了一个式子，它完美的符合我们的猜想。\n$$ i^{-1} \\equiv \\begin{cases} 1, \u0026 \\text{if } i = 1, \\\\ (p-\\left\\lfloor \\frac{p}{i} \\right\\rfloor)\\cdot (p \\bmod i)^{-1}, \u0026 \\text{otherwise}. \\end{cases} \\pmod p $$ 1 2 3 4 5 6 7 8 constexpr std::vector\u0026lt;i64\u0026gt; Inv(N, 1); constexpr int mod = 998244353; void Inverse() { Inv[1] = 1; for(int i = 2; i \u0026lt; N; ++i) { Inv[i] = ((i64)(mod - mod / i) * Inv[mod % i]) % mod; } } 写出这个 $code$ ，易如反掌。\n不过这么写只能求出 $1\\dots n$ 的逆元，如果求给定任意 $a_{i}\\dots a_{i+n}$ 这 $n$ 个数的逆元，就要稍稍变通一下.\n利用类似前缀和的思想，首先计算 $n$ 个数的前缀积，记为 $s_i$，然后使用快速幂或扩展欧几里得法计算 $s_n$ 的逆元，记为 $sv_n$。\n因为 $sv_n$ 是 $n$ 个数的积的逆元，所以当我们把它乘上 $a_{i+n}$ 时，就会和 $a_{i+n}$ 的逆元抵消，于是就得到了 $a_i$ 到 $a_{i+n-1}$ 的积逆元，记为 $sv_{n-1}$。\n同理我们可以依次计算出所有的 $sv_i$，于是 $a_i^{-1}$ 就可以用 $s_{i-1} \\times sv_i$ 求得。\n所以我们就在 $O(n + \\log p)$ 的时间内计算出了 $n$ 个数的逆元。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 i64 modInv(i64 t) { return binpow(t, mod - 2, mod); } void Inverse(int n) { std::vector\u0026lt;i64\u0026gt; S(n + 1, 1), sInv(n + 1, 0); for(int i = 1; i \u0026lt;= n; ++i) { S[i] = S[i - 1] * arr[i - 1] % mod; } sInv[n] = modInv(S[n]); for(int i = n; i \u0026gt;= 1; --i) { sInv[i - 1] = sInv[i] * arr[i - 1] % mod; } for(int i = 0; i \u0026lt; n; ++i) { inv[i] = sInv[i + 1] * S[i] % mod; } } 模板化 显然每次都重新编写这些是没有必要的, 而且这些内容十分适合模板化.\n编写成静态类 $ModInt$.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 template \u0026lt;auto mod\u0026gt; class ModInt { private : static_assert(mod \u0026gt;= 1LL, \u0026#34;The modulus must be a positive integer.\u0026#34;); using T = decltype(mod); T value; static constexpr std::tuple\u0026lt;T, T, T\u0026gt; Exgcd(const T \u0026amp;a, const T \u0026amp;b) { if(b == T{}) { return {a, T{1}, T{}}; } auto [gcd, x0, y0] = Exgcd(b, a % b); T x = y0, y = x0 - (a / b) * y0; return {gcd, x, y}; } public : constexpr ModInt() : value(T{}) {} template \u0026lt;typename V\u0026gt; constexpr ModInt(const V \u0026amp;v) { value = (static_cast\u0026lt;T\u0026gt;(v % mod) + mod) % mod; } constexpr ModInt inv() const { auto [gcd, x, y] = Exgcd(value, mod); // It could be hold that -mod \u0026lt; x \u0026lt; mod. assert(gcd == T{1} \u0026amp;\u0026amp; \u0026#34;The modular inverse does not exist.\u0026#34;); return ModInt(x); } // calculation operator overloading constexpr ModInt operator-() const { return ModInt(value == 0 ? 0 : mod - value); } constexpr ModInt\u0026amp; operator+=(const ModInt \u0026amp;rhs) { value += rhs.value; value %= mod; return *this; } constexpr ModInt\u0026amp; operator-=(const ModInt \u0026amp;rhs) { value += mod; value -= rhs.value; value %= mod; return *this; } constexpr ModInt\u0026amp; operator*=(const ModInt \u0026amp;rhs) { value = static_cast\u0026lt;T\u0026gt;((static_cast\u0026lt;i128\u0026gt;(value) * rhs.value) % mod); return *this; } constexpr ModInt\u0026amp; operator/=(const ModInt \u0026amp;rhs) { *this *= rhs.inv(); return *this; } friend constexpr ModInt operator+(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { ModInt res = a; res += b; return res; } friend constexpr ModInt operator-(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { ModInt res = a; res -= b; return res; } friend constexpr ModInt operator*(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { ModInt res = a; res *= b; return res; } friend constexpr ModInt operator/(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { ModInt res = a; res /= b; return res; } // bool operator overloading friend constexpr bool operator\u0026gt;(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value \u0026gt; b.value; } friend constexpr bool operator\u0026gt;=(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value \u0026gt;= b.value; } friend constexpr bool operator\u0026lt;(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value \u0026lt; b.value; } friend constexpr bool operator\u0026lt;=(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value \u0026lt;= b.value; } friend constexpr bool operator==(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value == b.value; } friend constexpr bool operator!=(const ModInt \u0026amp;a, const ModInt \u0026amp;b) { return a.value != b.value; } // stream operator overloading friend std::ostream\u0026amp; operator\u0026lt;\u0026lt;(std::ostream \u0026amp;os, const ModInt \u0026amp;x) { os \u0026lt;\u0026lt; x.value; return os; } friend std::istream\u0026amp; operator\u0026gt;\u0026gt;(std::istream \u0026amp;is, ModInt \u0026amp;x) { T v; if(is \u0026gt;\u0026gt; v) { x = ModInt(v); } return is; } }; constexpr i64 mod = 1\u0026#39;000\u0026#39;000\u0026#39;007LL; using Z = ModInt\u0026lt;mod\u0026gt;; template \u0026lt;typename T\u0026gt; Z mpow(Z base, T exp) { assert(exp \u0026gt;= 0); Z res = 1; while(exp \u0026gt; 0) { if(exp \u0026amp; 1) res *= base; base *= base; exp \u0026gt;\u0026gt;= 1; } return res; } 动态模数还没写 \u0026hellip;\n本文部分内容参考自 OIwiki ","date":"2025-05-25T13:59:19+08:00","permalink":"https://anfsity.com/p/multiplicative-inverse/","title":"Multiplicative Inverse"},{"content":"$Part\\ I.$ Static Variables in a Function 可以认为，声明在函数中被直接初始化的静态变量，就相当于把变量声明在全局，但是静态变量的作用域仅仅在函数内部其作用。\nStatic Member Variables in a Class 在类里面声明的静态变量分配空间时只被分配一次，所以类中所有实例化的对象中的静态变量相同，也就是静态变量和所有对象共享。也正是因为这个原因，静态变量不能使用构造函数进行初始化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;iostream\u0026gt; using namespace std; class GfG { public: // Static data member static int i; GfG(){ // Do nothing }; }; // Static member inintialization //int GfG::i = 1; int main() { // Prints value of i cout \u0026lt;\u0026lt; GfG::i; } 1 2 3 /tmp/ccPusLB5.o: In function `main\u0026#39;: Solution.cpp:(.text.startup+0xd): undefined reference to `GfG::i` collect2: error: ld returned 1 exit status 编译器在编译阶段为可能存在的 int GfG:: 打上引用，因为编译器假设 GfG::i 的定义会在其他地方提供，然而链接的阶段并没有找到为 GfG::i 分配空间的地方，所以抛出一个引用错误 undefined reference to GfG::i\nStatic Member Function in a Class 类似的，静态成员函数只能调用静态变量，被所有类共享。\nGlobal Static Variable 全局静态变量具有内部链接，也就是对于链接器来说，全局静态变量是看不见的，他只能被当前的翻译单元所访问，可以用来防止其他相同名字的其他文件变量冲突。注意定义在头文件中的静态变量作用域也被限制在当前的翻译单元，也就是对所有引用头文件的翻译单元来说，每个翻译单元都有一个静态变量的副本\nStatic 修饰的变量或者函数，导致的性质就是他的作用域会被改变，要想理解 static 关键词，应该先对程序的编译过程有一定的认识。\n参考链接 $Part\\ II.$ $Tag\\ Dispatch$\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // C++ Program to show the implementation of // tag dispatch #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; // Creating the different tags of type empty // struct struct t1{}; struct t2{}; // Defining a function for type t2 void fun(int a, t1) { cout \u0026lt;\u0026lt; \u0026#34;Calling function of tag t\u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; } // Defining the function with different // implementation for type t2 void fun(int a, t2) { cout \u0026lt;\u0026lt; \u0026#34;Function with tag t\u0026#34; \u0026lt;\u0026lt; a \u0026lt;\u0026lt; endl; } int main() { // Function calling with different tags fun(1, t1{}); fun(2, t2{}); return 0; } 标签调度，其实是利用函数重载的一种 c++ 技巧，可以用来处理这样一种情况：你要做出不同的操作对具有相似参数和返回值的同名函数。\n这是一种静态多态($static\\ polymorphism$)\n参考博客 $Part\\ III.$ $Duff's\\ Device$\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;numeric\u0026gt; void copyIntArray(std::vector\u0026lt;int\u0026gt;\u0026amp; src, std::vector\u0026lt;int\u0026gt;\u0026amp; dest, int size) { int rounds = size / 8; int i = 0; switch(size % 8) { case 0: while(rounds-- \u0026gt; 0) { dest[i] = src[i++]; case 7 : dest[i] = src[i++]; case 6 : dest[i] = src[i++]; case 5 : dest[i] = src[i++]; case 4 : dest[i] = src[i++]; case 3 : dest[i] = src[i++]; case 2 : dest[i] = src[i++]; case 1 : dest[i] = src[i++]; }; } } int main () { int size = 20; std::vector\u0026lt;int\u0026gt; src(size, 0), dest(20); std::iota(src.begin(), src.end(), 1); copyIntArray(src, dest, size); for(int i = 0; i \u0026lt; size; ++i) { std::cout \u0026lt;\u0026lt; dest[i] \u0026lt;\u0026lt; std::endl; } return 0; } 利用 switch 语句的特性进行非常奇怪的操作，这被叫做 $Duff's\\ device$， 可以用来模拟 C++ 的协程，但是在 C++20 版本已经提供了官方封装的协程。\nHow does Duff\u0026rsquo;s Device work? $Part\\ IV.$ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #pragma once #include \u0026lt;optional\u0026gt; #include \u0026lt;stdexcept\u0026gt; /* * A Ref\u0026lt;T\u0026gt; represents a \u0026#34;borrowed\u0026#34;-or-\u0026#34;owned\u0026#34; reference to an object of type T. * Whether \u0026#34;borrowed\u0026#34; or \u0026#34;owned\u0026#34;, the Ref exposes a constant reference to the inner T. * If \u0026#34;owned\u0026#34;, the inner T can also be accessed by non-const reference (and mutated). */ template\u0026lt;typename T\u0026gt; class Ref { static_assert( std::is_nothrow_move_constructible_v\u0026lt;T\u0026gt; ); // Type Trait : 模板 T 是否是可移动构造的？ // _v : 获取类型特性模板类中 :: value , 在 c++17 之前 ，通过 std::is_nothrow_move_constructible\u0026lt;T\u0026gt;::value static_assert( std::is_nothrow_move_assignable_v\u0026lt;T\u0026gt; ); public: // default constructor -\u0026gt; owned reference (default-constructed) Ref() requires std::default_initializable\u0026lt;T\u0026gt; : obj_( std::in_place ) {} // construct from rvalue reference -\u0026gt; owned reference (moved from original) Ref( T\u0026amp;\u0026amp; obj ) : obj_( std::move( obj ) ) {} // NOLINT(*-explicit-*) // move constructor: move from original (owned or borrowed) Ref( Ref\u0026amp;\u0026amp; other ) noexcept = default; // move-assignment: move from original (owned or borrowed) Ref\u0026amp; operator=( Ref\u0026amp;\u0026amp; other ) noexcept = default; // borrow from const reference: borrowed reference (points to original) static Ref borrow( const T\u0026amp; obj ) { Ref ret { uninitialized }; ret.borrowed_obj_ = \u0026amp;obj; return ret; } // duplicate Ref by producing borrowed reference to same object Ref borrow() const { Ref ret { uninitialized }; ret.borrowed_obj_ = obj_.has_value() ? \u0026amp;obj_.value() : borrowed_obj_; return ret; } #ifndef DISALLOW_REF_IMPLICIT_COPY // implicit copy via copy constructor -\u0026gt; owned reference (copied from original) Ref( const Ref\u0026amp; other ) : obj_( other.get() ) {} // implicit copy via copy-assignment -\u0026gt; owned reference (copied from original) Ref\u0026amp; operator=( const Ref\u0026amp; other ) { if ( this != \u0026amp;other ) { obj_ = other.get(); borrowed_obj_ = nullptr; } return *this; } #else // forbid implicit copies Ref( const Ref\u0026amp; other ) = delete; Ref\u0026amp; operator=( const Ref\u0026amp; other ) = delete; #endif ~Ref() = default; bool is_owned() const { return obj_.has_value(); } bool is_borrowed() const { return not is_owned(); } // accessors // const reference to object (owned or borrowed) const T\u0026amp; get() const { return obj_.has_value() ? *obj_ : *borrowed_obj_; } // mutable reference to object (owned only) T\u0026amp; get_mut() { if ( not obj_.has_value() ) { throw std::runtime_error( \u0026#34;attempt to mutate borrowed Ref\u0026#34; ); } return *obj_; } operator const T\u0026amp;() const { return get(); } // NOLINT(*-explicit-*) operator T\u0026amp;() { return get_mut(); } // NOLINT(*-explicit-*) const T* operator-\u0026gt;() const { return \u0026amp;get(); } T* operator-\u0026gt;() { return \u0026amp;get_mut(); } explicit operator std::string_view() const requires std::is_convertible_v\u0026lt;T, std::string_view\u0026gt; { return get(); } T release() { if ( obj_.has_value() ) { return std::move( *obj_ ); } #ifndef DISALLOW_REF_IMPLICIT_COPY return get(); #else throw std::runtime_error( \u0026#34;Ref::release() called on borrowed reference\u0026#34; ); #endif } private: const T* borrowed_obj_ {}; std::optional\u0026lt;T\u0026gt; obj_ {}; struct uninitialized_t {}; static constexpr uninitialized_t uninitialized {}; explicit Ref( uninitialized_t /*unused*/ ) {} }; template\u0026lt;typename T\u0026gt; static Ref\u0026lt;T\u0026gt; borrow( const T\u0026amp; obj ) { return Ref\u0026lt;T\u0026gt;::borrow( obj ); } $Q \\\u0026 A$\nstatic_assert 是做什么用的？为什么这里要检查 std::is_nothrow_move_constructible_v\u0026lt;T\u0026gt; 和 std::is_nothrow_move_assignable_v\u0026lt;T\u0026gt;？如果 T 类型不满足这些条件会怎么样？\n默认构造函数 Ref() 后面的 requires std::default_initializable\u0026lt;T\u0026gt; 是什么意思？它和普通的构造函数有什么区别？\nstd::optional\u0026lt;T\u0026gt; obj_ 和 const T* borrowed_obj_ 这两个成员变量是如何协同工作来表示 \u0026ldquo;owned\u0026rdquo; 和 \u0026ldquo;borrowed\u0026rdquo; 状态的？为什么不直接用一个指针和一个布尔标志位？\nstruct uninitialized_t {}; static constexpr uninitialized_t uninitialized {}; 和私有构造函数 explicit Ref(uninitialized_t) 这一套组合的目的是什么？为什么 borrow 函数需要这样创建一个 Ref 对象？\n#ifndef DISALLOW_REF_IMPLICIT_COPY ... #else ... #endif 这段预处理指令是用来做什么的？为什么会有允许或禁止隐式拷贝的选项？拷贝构造函数和拷贝赋值运算符在 \u0026ldquo;owned\u0026rdquo; 和 \u0026ldquo;borrowed\u0026rdquo; 状态下是如何工作的？\noperator const T\u0026amp;() const 和 operator T\u0026amp;() 这两个类型转换运算符为什么没有 explicit 关键字 (注释中提到了 NOLINT(*-explicit-*))？它们允许什么样的隐式转换，这在实际使用中会有什么好处或潜在风险？\nT release() 函数在 Ref 是 \u0026ldquo;owned\u0026rdquo; 和 \u0026ldquo;borrowed\u0026rdquo; 状态时行为有什么不同？为什么在 \u0026ldquo;borrowed\u0026rdquo; 状态下（如果 DISALLOW_REF_IMPLICIT_COPY 被定义）会抛出异常？std::move(*obj_) 的作用是什么？\n文件末尾的自由函数 template\u0026lt;typename T\u0026gt; static Ref\u0026lt;T\u0026gt; borrow(const T\u0026amp; obj) 和类内部的静态成员函数 static Ref borrow(const T\u0026amp; obj) 有什么区别？为什么会提供一个自由函数版本？这里的 static 用在自由函数模板上是什么意思？\n除了类型转换运算符，代码还重载了 operator-\u0026gt;() 和 operator-\u0026gt;() const。这两个箭头运算符的重载允许我们像使用指针一样使用 Ref 对象（例如 ref_obj-\u0026gt;member_func()），它们是如何实现这一点的？在 \u0026ldquo;owned\u0026rdquo; 和 \u0026ldquo;borrowed\u0026rdquo; 状态下，它们分别返回什么？\n以上问题由 Gemini 生成。\n","date":"2025-05-24T22:40:03+08:00","permalink":"https://anfsity.com/p/c-%E6%9D%82%E9%A1%B9/","title":"C++ 杂项"},{"content":"有感而发，也算是一学年的总结吧。\n先引用一段《上海交通大学生存手册》的话吧。\n各位同学们，在本书的开始，我不得不遗憾地告诉大家一个消息。国内绝大部分大学的本科教学，不是濒临崩溃，而是早已崩溃。在此，我无意争论是否复旦、中科大、或者清华、北大是否比我们崩溃的更少一些——这种争论是没有意义的。我只是看到了无数充满求知欲、激情、与年轻梦想的同学们，将要把自己的四年青春，充满希望与信任地交给大学来塑造。这使我心中非常不安。\n其实我很庆幸，我的运气还算可以，我的信息检索能力也还说的过去。在高考刚刚结束，我就看到了《上海交通大学生存手册》 ，在学习 CS 不久，就了解到了 csdiy ，虽然不是心仪的大学(高考发挥失常)，但起码是我自己选择的专业，是我有足够的热情，足够的兴趣去投入的专业。\n初入大学，我曾向一些优秀的学长请教选择 CS 专业的缘由，得到的答案却往往是“这个专业有潜力，能赚钱”这类现实考量。我不禁好奇，在这宝贵的四年里，究竟有多少人真正怀揣着明确的目标与梦想，他们热切追求着什么，渴望达成何种成就，又将塑造怎样的未来？而此刻，当我敲下这些文字，耳边传来的却仍旧是室友沉浸于游戏的喧嚣。我并非自诩清醒或优越，只是深感如此挥霍青春，实在可惜，也着实令人惋叹。\n从小就喜欢折腾的我，对 CS 感兴趣也许并不是一件值得惊讶的事，然而很遗憾的是，由于设备限制，我对于 CS 的了解，其实非常非常的肤浅。我不懂命令行，不懂编程语言，不懂网络。基本上，我是一个什么都不会的小白。也许，会用几个插件，知道几个软件，折腾过 windows 系统，这些简单的事情，甚至足以让我以为我对电脑非常的了解。放在现在来看，简直是羞愧难当。但不管怎么说，为了搞懂这些，我学会了搜索，习惯了查看文档，这总归算是个不错的开端。\n高考终究是阶段性的选拔手段，任何选拔手段都不能做到面面俱到从而帮助高校录取到他们最想要的学生，由于中小学的教育几乎都是为了高考选拔服务（读国外本科的同学除外），很多同学潜移默化中形成了线性的思维模式。在大学里也会有学积分的评价标准，但大学中已经不再像高考录取那样只靠成绩这一单一维度来线性地评价一个人的优秀程度，本科毕业时大家的发展与入学时的高考分数关系不大，其间四年里会有无数机遇等待着你把握，他们也会极大影响着你未来人生的走向，待毕业走到社会上之后你会发现虽然统计意义上来讲毕业院校越好他所达成的成就越高，但每个学校毕业生的出路方差却会大到你无法想象。\n然而，更令人沮丧的是，我身边愿意下功夫去‘卷’的同学已是少数，而在这少数人中，能够不被学分绩点裹挟、真正为知识本身而努力的，更是屈指可数。诚如作者所言，大学的本科教育早已崩溃，就我自身体验过的学校开设过的课程，可以说独一门 c 语言程序设计能够称得上是有些用处的。即便如此，它也远不足以成为在业界安身立命的基石。至于剩下的课程，你甚至都不知道这些课开出来的意义在于何处。\n我们学校 c 语言有正儿八经的 OJ \u0026hellip;\n“你想要做什么？”——高考之后，这个问题时常萦绕在我心头。刚入学时，我是非常迷茫的。家里的期望和进入新环境的迷茫，让我方向尽失。寒假，总归是想了明白，你是没办法 “grasp all and win all” 的，汲汲于学分绩点非我本意，而耽于安逸、虚度光阴亦非我所求。以至于我有些后悔，为什么当初没有开始自学的道路，为什么还是要按部就班的去学习学校的课程。而有些事情，直到现在还在拖累我的步伐。\n无论做什么，我们都需要给自己一个理由。每天迫于生活压力，毫无主见地忙碌着，可称得上人生一大悲哀。\n在迈进大学校门的时候，我们面临的最大问题是：为什么要上课？也许是因为问题本身太过浅显，以至于我们甚至懒于思考。但我们之中又有谁真正有效地思考过这个问题呢？\n“怕老师点名”“为了抄笔记作业”“记录考试重点”……这些话，充其量只是我们被迫上课的借口，却不能成为我们心悦诚服去上课的理由。\n真正能成为我们上课理由的，只有我们对科学文化知识的渴望。\n如果是否上课对你的考试成绩影响不大；如果我们感兴趣的知识不在学校的课程表上；如果上课学习的效果足够差，效率足够低，以至于通过自习，能够在更短的时间掌握知识；那么你还需要去上课吗？\n尽管现状如此，我仍然在网上见到有人和我观点契合，遇见许多仰慕的前辈，更有先人将走过的路，汇为经验，为我们搭建平台供学习。\n请记住，总有更加值得做的事，请把目光放的长远，不要为了 GPA 而上课，去独立思考，上课，学习，考试这些事情是否真的值得去做，日复一日的习题锻炼是否真的有必要执行。我们之所以拒绝学习那些对自己不是特别有用的知识，是因为这些知识对我们的价值太低。\n切勿指望学校亲自为你安排一条康庄大道，真正的路终究是需要自己一步一个脚印踏出来的。\n学习不一定是痛苦的，但是没有痛苦的学习，感受不到开心的学习，是没有收获的。\n最后，引用 csdiy 的一段话作为结语\n你得有足够的驱动力强迫自己静下心来，阅读几十页的 Project Handout，理解上千行的代码框架，忍受数个小时的 debug 时光。而这一切，没有学分，没有绩点，没有老师，没有同学，只有一个信念 —— 你在变强。\n","date":"2025-05-24T22:31:23+08:00","permalink":"https://anfsity.com/p/%E9%9A%8F%E7%AC%94%E5%85%B6%E4%B8%80/","title":"随笔其一"},{"content":" [!quote] A fine quotation is a diamond on the finger of a man of wit, and a pebble in the hand of a fool. — Joseph Roux\n我们要证明的等式是： $$ \\gcd(\\operatorname{lcm}(a,b), \\operatorname{lcm}(a,c)) = \\operatorname{lcm}(a, \\gcd(b,c)) $$ 基础：唯一分解定理（算术基本定理） 证明的关键在于使用唯一分解定理。该定理指出，任何大于 1 的整数都可以唯一地分解为素数的乘积（不考虑顺序）。\n形式一： 对于 $\\forall n \\in \\mathbb{Z}, n \u003e 1$，存在唯一的不同素数集合 $\\{p_1, \\dots, p_k\\}$ 和唯一的正整数指数集合 $\\{\\alpha_1, \\dots, \\alpha_k\\}$ 使得： $$ n = p_1^{\\alpha_1} p_2^{\\alpha_2} \\cdots p_k^{\\alpha_k} = \\prod_{i=1}^{k} p_i^{\\alpha_i} $$形式二（更适用于证明）： 对于 $\\forall n \\in \\mathbb{Z}, n \u003e 1$，其分解可以写成包含所有素数的形式： $$ n = \\prod_{p \\text{ prime}} p^{\\nu_p(n)} $$ 其中 $\\nu_p(n)$ 是素数 $p$ 在 $n$ 分解中的（非负）指数。对于给定的 $n$，只有有限个 $\\nu_p(n)$ 大于 0。\n使用素数指数表示 GCD 和 LCM 根据唯一分解定理，我们可以通过比较整数分解中每个素数 $p$ 的指数来计算最大公约数 (gcd) 和最小公倍数 (lcm)：\n对于任意素数 $p$： $p$ 在 $\\gcd(x, y)$ 中的指数是 $\\nu_p(\\gcd(x, y)) = \\min(\\nu_p(x), \\nu_p(y))$ $p$ 在 $\\operatorname{lcm}(x, y)$ 中的指数是 $\\nu_p(\\operatorname{lcm}(x, y)) = \\max(\\nu_p(x), \\nu_p(y))$ 例子： 设 $a = 12 = 2^2 \\cdot 3^1 \\cdot 5^0$ 设 $b = 30 = 2^1 \\cdot 3^1 \\cdot 5^1$\n计算 $\\gcd(12, 30)$:\n素数 2: 指数 $\\min(2, 1) = 1$ 素数 3: 指数 $\\min(1, 1) = 1$ 素数 5: 指数 $\\min(0, 1) = 0$ 所以 $\\gcd(12, 30) = 2^1 \\cdot 3^1 \\cdot 5^0 = 6$ 计算 $\\operatorname{lcm}(12, 30)$:\n素数 2: 指数 $\\max(2, 1) = 2$ 素数 3: 指数 $\\max(1, 1) = 1$ 素数 5: 指数 $\\max(0, 1) = 1$ 所以 $\\operatorname{lcm}(12, 30) = 2^2 \\cdot 3^1 \\cdot 5^1 = 60$ 证明过程 我们的策略是证明对于任意素数 $p$，它在等式左边 (LHS) 和右边 (RHS) 的指数都相等。根据唯一分解定理，如果所有素数的指数都对应相等，则这两个数必然相等。\n设对于任意素数 $p$，其在 $a, b, c$ 中的指数分别为 $\\alpha = \\nu_p(a)$, $\\beta = \\nu_p(b)$, $\\gamma = \\nu_p(c)$。\n1. 计算 LHS 中 $p$ 的指数： LHS = $\\gcd(\\operatorname{lcm}(a,b), \\operatorname{lcm}(a,c))$\n$p$ 在 $\\operatorname{lcm}(a,b)$ 中的指数为 $\\max(\\alpha, \\beta)$。 $p$ 在 $\\operatorname{lcm}(a,c)$ 中的指数为 $\\max(\\alpha, \\gamma)$。 根据 gcd 的指数规则， $p$ 在 LHS 中的指数为： $$ \\nu_p(\\text{LHS}) = \\min(\\max(\\alpha, \\beta), \\max(\\alpha, \\gamma)) $$ 2. 计算 RHS 中 $p$ 的指数： RHS = $\\operatorname{lcm}(a, \\gcd(b,c))$\n$p$ 在 $\\gcd(b,c)$ 中的指数为 $\\min(\\beta, \\gamma)$。 根据 lcm 的指数规则，$p$ 在 RHS 中的指数为： $$ \\nu_p(\\text{RHS}) = \\max(\\alpha, \\min(\\beta, \\gamma)) $$ 3. 证明指数相等： 我们需要证明 $\\nu_p(\\text{LHS}) = \\nu_p(\\text{RHS})$，即： $$ \\min(\\max(\\alpha, \\beta), \\max(\\alpha, \\gamma)) = \\max(\\alpha, \\min(\\beta, \\gamma)) $$ 这个等式是 $\\min$ 和 $\\max$ 运算的一个基本性质，称为分配律。这里用到的是 max 对 min 的分配律: $$ \\max(x, \\min(y, z)) = \\min(\\max(x, y), \\max(x, z)) $$ 令 $x = \\alpha$, $y = \\beta$, $z = \\gamma$，我们直接应用此分配律： $$ \\max(\\alpha, \\min(\\beta, \\gamma)) = \\min(\\max(\\alpha, \\beta), \\max(\\alpha, \\gamma)) $$ 这表明 $\\nu_p(\\text{RHS}) = \\nu_p(\\text{LHS})$。\n结论： 由于对于任意素数 $p$，它在等式两边的指数都相等，根据唯一分解定理，这两个表达式代表的整数必然相等。\n因此，原等式 $\\gcd(\\operatorname{lcm}(a,b), \\operatorname{lcm}(a,c)) = \\operatorname{lcm}(a, \\gcd(b,c))$ 成立。\n$Q.E.D.$\n算术基本定理及其证明 算术基本定理 (Fundamental Theorem of Arithmetic): 任何大于 1 的整数 $n$ 都可以被唯一地分解成有限个素数的乘积（不考虑因子的顺序）。 即：对于任意整数 $n \u003e 1$，存在唯一的不同素数集合 $\\{p_1, \\dots, p_k\\}$ 和唯一的正整数组 $\\{\\alpha_1, \\dots, \\alpha_k\\}$ 使得： $$ n = p_1^{\\alpha_1} p_2^{\\alpha_2} \\cdots p_k^{\\alpha_k} = \\prod_{i=1}^{k} p_i^{\\alpha_i} $$ 证明 证明分为两部分：存在性和唯一性。\n第一部分：存在性证明（强归纳法） 我们要证明任何整数 $n \u003e 1$ 都可以写成素数的乘积。\n基础情况: 当 $n=2$ 时，2 本身是素数，已是素数乘积。成立。 归纳假设: 假设对于所有满足 $1 \u003c k \u003c n$ 的整数 $k$，$k$ 都可以表示成素数的乘积。 归纳步骤: 考虑整数 $n$： 情况 A: 如果 $n$ 是素数，则它已是素数乘积。 情况 B: 如果 $n$ 是合数，则 $n = a \\cdot b$，其中 $1 \u003c a \u003c n$ 且 $1 \u003c b \u003c n$。根据归纳假设，$a$ 和 $b$ 都可以写成素数乘积： $$ a = p_1 p_2 \\cdots p_r $$ $$ b = q_1 q_2 \\cdots q_s $$ 其中所有 $p_i, q_j$ 都是素数。那么： $$ n = a \\cdot b = (p_1 \\cdots p_r)(q_1 \\cdots q_s) $$ 这表明 $n$ 也可以写成素数的乘积。 根据强归纳法，存在性得证。\n第二部分：唯一性证明（使用欧几里得引理和反证法） 我们需要用到一个关键引理：\n欧几里得引理 (Euclid\u0026rsquo;s Lemma): 如果素数 $p$ 整除乘积 $ab$ (记作 $p | ab$)，那么 $p$ 必须整除 $a$ 或 $p$ 必须整除 $b$ (即 $p | a$ 或 $p | b$)。 推论: 如果素数 $p$ 整除 $a_1 a_2 \\cdots a_k$，则 $p$ 至少整除其中一个 $a_i$。\n证明唯一性（反证法）：\n假设: 假设存在大于 1 的整数拥有至少两种不同的素数分解。根据良序原则（最小数原理），必然存在一个最小的这样的整数，记为 $n$。 分析 $n$: 设 $n$ 有两种不同的分解： $$ n = p_1 p_2 \\cdots p_r = q_1 q_2 \\cdots q_s $$ 其中 $p_i$ 和 $q_j$ 都是素数，并且作为多重集（考虑重复次数） $\\{p_1, \\dots, p_r\\} \\neq \\{q_1, \\dots, q_s\\}$。由于 $n$ 是最小的反例，任何小于 $n$ 的整数 $m \u003e 1$ 的素数分解是唯一的。 应用引理: 考虑 $p_1$。显然 $p_1 | n$，所以 $p_1 | (q_1 q_2 \\cdots q_s)$。根据欧几里得引理的推论，$p_1$ 必须整除某个 $q_j$。因为 $p_1$ 和 $q_j$ 都是素数，这只可能在 $p_1 = q_j$ 时发生。 约去公共因子: 不失一般性，重排 $q$ 使得 $p_1 = q_1$。等式两边同除以 $p_1$： $$ \\frac{n}{p_1} = p_2 p_3 \\cdots p_r = q_2 q_3 \\cdots q_s $$ 导出矛盾: 令 $m = n/p_1$。因为 $n$ 是合数（否则分解唯一），所以 $1 \u003c m \u003c n$。我们得到了整数 $m$ 的两种分解 $p_2 \\cdots p_r$ 和 $q_2 \\cdots q_s$。由于 $\\{p_1, \\dots, p_r\\}$ 和 $\\{q_1, \\dots, q_s\\}$ 不同，去掉相同的 $p_1=q_1$ 后，$\\{p_2, \\dots, p_r\\}$ 和 $\\{q_2, \\dots, q_s\\}$ 也必定不同。 这表明 $m$ 是一个比 $n$ 更小的、拥有不同素数分解的整数。这与 $n$ 是具有此性质的最小整数的假设相矛盾！ 结论: 初始假设错误。因此，任何整数 $n \u003e 1$ 的素数分解是唯一的（在不考虑顺序时）。 $Q.E.D.$\n","date":"2025-04-21T22:35:59+08:00","permalink":"https://anfsity.com/p/gcd-and-lcm/","title":"GCD and LCM"},{"content":"老爷子的线性代数课层层递进，深入浅出，引人入胜，只能说不愧是享誉盛名的教授，盛名之下无虚士。\n老爷子用的教材是他自己写的书：\n《Introduction To Linear Algebra Fifth Edition》\n教材和答案在 MIT Open Source 的官网上都有。\n老爷子的一个网站：\nwebsite 2023年5月15日，Gilbert Strang 上完了他在 18.06 的最后一课，以88岁高龄结束了在其 MIT 61年的教学及科研生涯。 但他的线性代数课已经并且还将继续影响一代代青年学子，让我们向老先生致以最崇高的敬意。\n― zhongyinmin@pku.edu.cn 老爷子在油管上的评论：\nPosted on behalf of Gil Strang:\nThis is my chance to thank everyone for such generous messages about my last lecture at MIT. I am almost tempted to have second thoughts about retirement \u0026hellip;\u0026hellip;\nbut it is the right time. Teaching has been a wonderful life.\nAnd I am so grateful to everyone who likes linear algebra and sees its importance.\nSo many universities ( and even high schools ) now appreciate how beautiful it is and how valuable it is. That movement will continue because it is right.\nI thank you for your good thoughts. I appreciate them more than I can say.\nVery best wishes in all your work\nGil\nLast Class 配合3b1b的视频一起食用味道更佳：\nYutube Bilibili 1 2 Any student who has taken this course will always remember the professor. We extend our highest respect to the professor. 1 2 3 4 A teacher affects eternity; he can never tell where his influence stops. -Henry Adams 教师影响永恒；他永远无法知道他的影响在哪里停止。 -亨利·亚当斯 MIT Linear Algebra Open Source ","date":"2024-10-17T22:12:58+08:00","permalink":"https://anfsity.com/p/linear-algebra/","title":"Linear Algebra"}]