元编程让程序参与生成或变换其他程序。编译期元编程把部分工作放到构建阶段,但不同机制操作的对象不同:有的处理预处理 token,有的处理语法数据,有的计算常量,还有的修改字节码。
理解时先问三个问题:元程序拿到的是语法还是已检查的值,它在什么阶段执行,产物是代码、数据还是额外行为?
先区分输入与产物:生成语法、求值、织入行为
Token / 语法数据
宏展开
新语法 → 后续检查
常量表达式
编译期求值
值 / 静态数据
程序 + 匹配规则
编织
包含额外行为的程序
变换在构建阶段发生,不代表生成的行为或数据没有运行时成本。
为什么不用普通函数就够了
普通函数适合抽象运行时计算,通常不需要知道调用方字段的名字,也不能随意生成新声明。当需求是“为结构体的每个字段生成序列化代码”或“根据类型参数构造新类型”,就需要超出普通函数调用的能力。
但元编程会增加展开、诊断和构建复杂度。若普通函数、泛型或组合能清楚表达需求,它们通常更容易调试。选择依据应是需要操作什么信息,而非哪个机制看起来更强。
C 宏:预处理 token 展开
C 预处理器按预处理 token 规则替换,不是无规则的字符替换,也不理解后续完整的表达式类型。它支持条件编译、字符串化和 token 拼接等用途。
下面的 x、y、z 语句位于函数体内;这是宏展开的局部示例。
#define PICK_MAX ( a , b ) ( ( a) > ( b) ? ( a) : ( b) )
int x = 3 ;
int y = 2 ;
int z = PICK_MAX ( x+ + , y ) ;
括号解决部分运算符优先级问题,却不解决重复求值:比较使用一次 x++,选中的分支再执行一次 x++。如果参数包含函数调用,也可能重复产生副作用。对于固定类型的比较,普通内联函数能提供一次参数求值和类型检查。更多展开边界见 GCC 宏说明 。
用同一个调用点比较“展开两次”与“实参求值一次”
下面保存为 evaluate-once.c。两个测试分别从 x=3 开始,宏得到 x=5、结果 4;函数得到 x=4、结果 3。这个例子没有在同一个未排序表达式里多次修改 x,避免用未定义行为来解释重复求值。
#include < stdio.h>
#define PICK_MAX ( a , b ) ( ( a) > ( b) ? ( a) : ( b) )
static inline int max_int ( int a , int b ) { return a > b ? a : b; }
int main ( void ) {
int x = 3 , y = 2 ;
int by_macro = PICK_MAX ( x+ + , y ) ;
if ( x != 5 || by_macro != 4 ) return 1 ;
x = 3 ;
int by_function = max_int ( x+ + , y ) ;
if ( x != 4 || by_function != 3 ) return 1 ;
printf ( " macro=%d function=%d \n " , by_macro, by_function) ;
return 0 ;
}
执行 cc -Wall -Wextra evaluate-once.c -o evaluate-once,输出 macro=4 function=3。用 cc -E 查看展开也能看到 x++ 出现在哪两个分支位置。内联优化可以消除函数调用开销,但函数实参仍按语言语义求值,不会因此变成宏式重复求值。GCC 副作用重复
一次求值不等于多个参数之间有固定求值顺序。若第二个参数也读写 x,仍须遵守 C 的求值顺序规则;需要明确先后关系时先用独立语句计算临时值,再调用函数。用函数替代这个固定类型的宏,解决的是参数复制问题,不是自动为所有表达式排序。
括号都写对了,为什么 x 仍会多增加一次?
观察点不只放在返回值,还放在每一次读取 x++ 时 x 的旧值与新值。初始 x>y 时,比较读取一次、选中的分支再读取一次;x≤y 时第二个 x++ 分支不执行。函数版本始终先得到一个实参值,然后复用它。这个对照保持 C 条件运算符的顺序边界,没有借未定义行为制造差异。
正在呈现知识画面 重新加载
括号都写对了,为什么 x 仍会多增加一次? 宏复制的是未求值表达式,比较之后被选中的 x++ 还会执行;函数先把实参求成值,再在函数体复用那个值。分支未选中时也不会无条件执行第二次。
{"id":"macro-evaluation-count","title":"括号都写对了,为什么 x 仍会多增加一次?","summary":"宏复制的是未求值表达式,比较之后被选中的 x++ 还会执行;函数先把实参求成值,再在函数体复用那个值。分支未选中时也不会无条件执行第二次。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"x\" data-i18n=\"x\"></label><output for=\"x\" id=\"x-value\"></output><input id=\"x\" type=\"range\" min=\"0\" max=\"5\" value=\"3\" step=\"1\"></div><div class=\"control\"><label for=\"y\" data-i18n=\"y\"></label><output for=\"y\" id=\"y-value\"></output><input id=\"y\" type=\"range\" min=\"0\" max=\"5\" value=\"2\" step=\"1\"></div><div id=\"expansion\" class=\"source\"></div><div id=\"evaluations\" class=\"table\"></div><div class=\"metrics\"><div><span data-i18n=\"macro\"></span><strong id=\"macro\"></strong></div><div><span data-i18n=\"function\"></span><strong id=\"function\"></strong></div></div><p id=\"count\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#expansion{min-height:5.4em}.metrics strong{font-size:18px}#evaluations .tr{grid-template-columns:1fr 1fr}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const x=+$('#x').value,y=+$('#y').value,branch=x>y,macroResult=branch?x+1:y,macroX=x+(branch?2:1),fn=Math.max(x,y);set('x-value',x);set('y-value',y);$('#expansion').innerHTML='((<mark>x++</mark>) > (y) ? ('+(branch?'<mark>x++</mark>':'x++')+') : (y))\\ncondition: '+x+' > '+y+' = '+branch;table('evaluations',[['compare','read '+x+'; x→'+(x+1)],['selected branch',branch?'read '+(x+1)+'; x→'+(x+2):'read y='+y]]);set('macro',macroResult+' / '+macroX);set('function',fn+' / '+(x+1));set('count',t('count')+': '+(branch?2:1)+' / 1');$('#macro').dataset.values=[macroResult,macroX].join(',');}$('#x').oninput=draw;$('#y').oninput=draw;draw();","audio":false,"strings":{"count":"x++ 求值次数(宏 / 函数)","function":"函数:结果 / 最终 x","heading":"括号都写对了,为什么 x 仍会多增加一次?","intro":"比较 PICK_MAX(x++, y) 与 max_int(x++, y)。改变初始 x、y,观察宏选择哪个分支以及真实求值次数。","macro":"宏:结果 / 最终 x","reset":"重置","scope":"固定 C 条件运算语义,x/y取0..5,第二参数没有副作用。模型没有未排序的重复修改,不声明普通函数参数间存在固定求值顺序。","scopeLabel":"模型范围与约定","x":"初始 x","y":"固定 y"}}
Lisp 宏:输入是未求值的代码形式
Lisp 的代码形式可以作为列表等数据操作。宏接收未按普通函数参数规则求值的形式,产生新的代码形式,随后由语言处理该展开结果。
( defmacro unless-demo ( condition &body body)
`( if ( not ,condition) ( progn ,@body) ) )
这里 condition 和 body 被放入生成的 if 形式;它们表达的运行时动作并不是在宏展开时就执行。准引用、反引用和拼接帮助构造代码,但宏作者仍要考虑变量捕获、求值次数、展开环境与错误位置。代码是数据不意味着作用域和语义问题消失。
Rust 过程宏:TokenStream 不等于带类型 AST
Rust 过程宏在编译时消费并产生 TokenStream。宏可以借助 syn 等库解析为语法结构,但这不意味着获得 rustc 完成类型推导后的内部 AST,也不能普遍查询某个表达式推导出的类型。
过程宏主要有 function-like、derive 和 attribute 三类,展开的插入或替换规则不同。例如 derive 生成附加条目,常用来生成 impl;attribute 可以替换所标注的条目。接口与规则见 Rust Reference 。
假设为 struct Record<T> { value: T } 生成序列化实现,宏必须保留泛型参数与 where 约束,生成合法路径和边界,然后由后续编译阶段检查实现是否类型正确。“宏知道字段语法”与“宏证明字段类型满足全部约束”不同。
卫生性:展开后名字指向谁
卫生性解决生成名字与调用点名字的意外捕获。Rust macro_rules! 有其混合位置卫生规则,过程宏则不能概括为“天然完全卫生”。过程宏作者需要处理名称冲突和路径解析,例如使用适当的绝对路径、保留 span,以及避免生成容易冲突的辅助名字。
卫生也不自动保证求值一次、借用正确或错误信息良好。宏生成代码的这些性质仍要单独设计。
生成的 tmp,会不会抓走调用方的 tmp?
相同拼写的两个 tmp 可能来自不同的声明环境。下图保留调用方声明 C 与宏生成声明 M,分别追踪插入表达式和辅助引用。关闭卫生策略时,仅在调用方也叫 tmp 的情况下出现捕获;改名能避开这个例子的碰撞,却不是保证所有宏调用安全的通用方案。卫生规则应该维护绑定来源,而不只是寻找一个看起来冷门的变量名。
正在呈现知识画面 重新加载
生成的 tmp,会不会抓走调用方的 tmp? 卫生性保留插入表达式原有的声明身份,同时让生成的辅助引用指向生成声明。仅凭展开后文本做最近名字查找,可能把调用方变量误绑到辅助变量。
{"id":"macro-hygiene-binding","title":"生成的 tmp,会不会抓走调用方的 tmp?","summary":"卫生性保留插入表达式原有的声明身份,同时让生成的辅助引用指向生成声明。仅凭展开后文本做最近名字查找,可能把调用方变量误绑到辅助变量。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"name\"><button data-mode=\"tmp\" data-i18n=\"same\"></button><button data-mode=\"value\" data-i18n=\"different\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"hygiene\" checked><span data-i18n=\"hygiene\"></span></label><p id=\"expansion\" class=\"source\"></p><svg id=\"bindings\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div><div><span data-i18n=\"binding\"></span><strong id=\"binding\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#expansion{min-height:7.2em}.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let name='tmp';function draw(){const good=$('#hygiene').checked||name!=='tmp';set('expansion','let '+name+' = 10; // C\\nwith_temp('+name+')\\n⇒ { let tmp = 1; // M\\n '+name+' + tmp; }');graph('bindings',[{name:'C',x:80,y:40,cls:good?'on':'',sub:name+'=10'},{name:'M',x:280,y:40,cls:'gold',sub:'tmp=1'},{name:'expr',x:80,y:150},{name:'tmp',x:280,y:150}],[[2,good?0:1,true],[3,1,true]]);set('result',good?11:2);set('binding',good?'C':'M');pressed(name);}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{name=b.dataset.mode;draw()});$('#hygiene').onchange=draw;draw();","audio":false,"strings":{"binding":"expr 的绑定","different":"value:不同名","heading":"生成的 tmp,会不会抓走调用方的 tmp?","hygiene":"保留插入表达式的绑定身份","intro":"教材宏 with_temp(expr) 生成 let tmp=1; expr+tmp。切换调用方名字与卫生绑定,观察两个相同拼写分别指向谁。","name":"调用方名字","reset":"重置","result":"表达式结果(期望11)","same":"tmp:发生同名","scope":"词法作用域教材语言,调用方变量值10、辅助变量值1。比较声明ID策略,不冒充 Rust、Lisp 等语言各自完整的宏卫生规则。","scopeLabel":"模型范围与约定"}}
编译期求值:计算一个值
Rust 的 const fn 可以在满足条件的常量上下文中执行;在普通调用位置也可以运行时执行,不能仅凭函数声明里的 const 就断言每次调用都在编译期完成。
const fn square_table ( ) -> [ u32 ; 4 ] {
let mut out = [ 0 ; 4 ] ;
let mut i = 0 ;
while i < 4 {
out[ i] = ( i * i) as u32 ;
i + = 1 ;
}
out
}
const SQUARES : [ u32 ; 4 ] = square_table ( ) ;
fn main ( ) {
assert_eq! ( SQUARES , [ 0 , 1 , 4 , 9 ] ) ;
}
常量上下文要求得到合法常量结果,语言会限制可执行的操作。数组越界等在必须求值时成为编译错误;允许的语法和引用行为按语言与版本规定。Rust 常量求值
C++ constexpr、consteval 与 Zig comptime 的具体能力不同,不应统一描述成同一种解释器。也不要把 const 求值与构建脚本、过程宏的权限混为一谈:后两者可能访问构建环境和文件系统。
预计算把计算时间转移到构建阶段,并可能把结果放入产物。它还可能增加二进制体积、编译内存或缓存压力;“编译期执行”不等于所有成本归零。
同一个 const fn,调用上下文决定错误在哪一阶段暴露
下面把合法 payload 上限定义为 1024,固定头部占 8 字节。常量 PACKET_SIZE 的初始化必须在编译期求值;main 中来自 args 的长度则属于普通运行时输入。
const fn packet_size ( payload : usize ) -> usize {
assert! ( payload < = 1024 , " payload too large" ) ;
payload + 8
}
const PACKET_SIZE : usize = packet_size ( 16 ) ;
fn main ( ) {
assert_eq! ( PACKET_SIZE , 24 ) ;
let runtime_input = std:: env:: args( ) .count ( ) ;
assert_eq! ( packet_size ( runtime_input) , runtime_input + 8 ) ;
}
直接编译并无额外参数运行会通过。如果把常量初始化改成 packet_size(1025),编译器必须拒绝这个常量;若仅在 main 普通路径传入 1025,程序会在执行到断言时 panic。const fn 的函数体并没有变成两种逻辑,变化的是当前上下文是否要求合法常量结果。常量上下文
使用位置 阶段义务 不能据此推断
const 初始化 必须能在常量求值规则下完成 数据没有产物空间成本
普通运行时调用 按普通执行语义产生值或失败 编译器完全不会做常量折叠
构建脚本读取文件 构建时运行独立程序 文件内容自动成为已声明的依赖
过程宏生成表达式 产生待编译语法 表达式已经完成类型检查
同一个 const fn,错误会在哪个阶段出现?
阶段义务与优化机会应分开。const 初始化要求编译阶段完成合法求值;普通路径则按是否真正执行决定运行时结果。下面保留同一个 packet_size 函数体,只改变调用上下文和输入,便于观察错误从哪里产生。普通调用即使被优化器预计算,也不能因此无条件改成常量上下文的诊断规则。
正在呈现知识画面 重新加载
同一个 const fn,错误会在哪个阶段出现? const 初始化必须得到合法常量;普通调用不因函数名带 const 就成为必须编译期求值。普通路径没执行时,也不会凭空发生该调用的运行时 panic。
{"id":"constant-evaluation-context","title":"同一个 const fn,错误会在哪个阶段出现?","summary":"const 初始化必须得到合法常量;普通调用不因函数名带 const 就成为必须编译期求值。普通路径没执行时,也不会凭空发生该调用的运行时 panic。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"context\"><button data-mode=\"const\" data-i18n=\"constant\"></button><button data-mode=\"runtime\" data-i18n=\"runtime\"></button></div><div class=\"control\"><label for=\"n\" data-i18n=\"n\"></label><output for=\"n\" id=\"n-value\"></output><input id=\"n\" type=\"range\" min=\"0\" max=\"1050\" value=\"1025\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"execute\" checked><span data-i18n=\"execute\"></span></label><p id=\"source\" class=\"source\"></p><div id=\"phases\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"compile\"></span><strong id=\"compile\"></strong></div><div><span data-i18n=\"run\"></span><strong id=\"run\"></strong></div><div><span data-i18n=\"value\"></span><strong id=\"value\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#source{min-height:12.6em}.metrics strong{font-size:17px;min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let mode='const';function draw(){const n=+$('#n').value,valid=n<=1024,constant=mode==='const',executed=$('#execute').checked;$('#execute').disabled=constant;set('n-value',n);set('source','const fn packet_size(n) {\\n assert(n <= 1024); n+8\\n}\\n'+(constant?'const SIZE = packet_size('+n+');':'if execute { packet_size(input_n); }'));const compileOK=!constant||valid,run=constant?(valid?t('embedded'):'—'):!executed?t('notrun'):valid?'return':'panic';set('compile',t(compileOK?'ok':'fail'));set('run',run);set('value',valid&&(constant||executed)?n+8:'—');cells('phases',[{text:'source',cls:'on'},{text:constant?'const eval':'compile',cls:compileOK?'on':'gold'},{text:'runtime',cls:constant?'empty':executed?'on':'empty'}]);$('#compile').dataset.valid=String(compileOK);pressed(mode);}$('#n').oninput=draw;$('#execute').onchange=draw;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"compile":"编译阶段","constant":"const 初始化","context":"调用上下文","embedded":"已嵌入常量","execute":"运行到这个调用","fail":"常量求值错误","heading":"同一个 const fn,错误会在哪个阶段出现?","intro":"packet_size(n) 先检查 n≤1024,再返回 n+8。选择调用上下文与输入,区分编译期义务和普通运行时调用。","n":"payload 长度 n","notrun":"未执行","ok":"编译通过","reset":"重置","run":"运行阶段","runtime":"普通运行时调用","scope":"模拟正文 Rust const fn 的阶段义务,payload0..1050。不是在浏览器运行 rustc;优化器可能折叠普通调用,但不能改变这里定义的可观察语义。","scopeLabel":"模型范围与约定","value":"得到的值"}}
模板与泛型:参数也可以决定程序结构
泛型的类型参数参与检查;某些语言还支持值参数、类型级计算或按参数实例化代码。单态化可以让不同实例获得专用实现,但会增加实例数量与编译成本。类型擦除与字典传递则采用不同的运行时表示和调用方式。
这些与宏可以组合:宏先生成带泛型的实现,编译器再检查并按策略生成代码。把所有泛型都叫宏,或把所有元编程都叫 AST 替换,会掩盖阶段差异。
AOP 编织:选择程序中的连接点并加入行为
AspectJ 通过 pointcut 描述连接点集合,通过 advice 指定要执行的行为。call 与 execution 匹配的对象不同,不能把方法执行连接点一律画成调用点前插入 AST。
AspectJ 支持源代码编译以及对已有类文件的编织等路径,产物可为织入行为的字节码;也存在加载时编织。它不局限于“编译器只修改 Java AST”。AspectJ 编译器与编织器
提前织入可以避免某些代理边界,但 advice 里的日志、计时和检查仍在运行时执行,动态连接点条件也可能有成本。自调用是否被拦截取决于机制、匹配规则与编织范围,不能用“覆盖所有调用、零运行时开销”作通用结论。
生成结果能否重现,取决于所有影响输出的输入
显式输入
schema + 生成器版本 + 选项
确定性生成
固定字段顺序与命名规则
生成代码
后续解析、类型检查、编译
隐式输入:当前目录、环境变量、时间或无序遍历
若会改变输出,就要纳入构建模型,或从生成逻辑中消除
生成成功和产物正确是两步;编译通过后还要验证生成接口的实际语义。
构建缓存必须能看见生成器的真实依赖
例如由 schema 生成 Record 的序列化实现,字段顺序来自 schema,命名规则来自生成器版本,输出模式来自配置开关。只把生成器源文件列为依赖,修改 schema 后就可能沿用旧输出;只把 schema 列为依赖,升级命名规则也可能不重建。
相反,若生成器在每次运行时无条件加入当前时间,即使所有业务输入不变,生成结果仍会变化,使缓存与代码审查充满噪声。应先确定时间是否属于产物语义;若只是日志,留在构建日志中即可。
变化 合理期望 需要核对
新增字段 对应生成声明变化 输入依赖是否使生成器重跑
升级生成器规则 相同 schema 按新规则生成 工具版本是否进入缓存键
无业务输入变化 生成内容保持稳定 顺序、随机数与隐式环境
生成出非法类型组合 编译诊断能指向领域输入 源位置或生成清单是否保留
编译期计算也有规模边界:N 个泛型实例每个生成 B 字节,代码空间可能随实例数增加,但链接器折叠、优化和共享策略会改变最终大小。应查看产物与构建资源,不能只根据源码少写了多少行就认定编译期元编程一定节省总成本。
工程评审速查
需求 可考虑的机制 要检查
少量重复计算 函数、泛型 是否已经足够表达
按字段生成实现 derive / 过程宏 泛型、路径、错误 span、生成代码可检查性
预生成常量表 常量求值 允许操作、编译成本、产物大小
按类型和值生成专用结构 模板或类型级机制 实例膨胀、约束诊断
横切行为 编织或代理 连接点语义、覆盖范围、运行时副作用
验证不仅要看正常展开,还要看错误输入、同名变量、带副作用的参数、泛型边界,以及修改后增量构建是否失效正确。生成结果应有可追溯的位置和稳定顺序,避免构建环境的隐式输入造成难以复现的产物。
参考
Keywords: metaprogramming, macro, TokenStream, hygiene, const fn, constexpr, templates, AspectJ, weaving