如何在Gmake宏函数中实现延迟简单变量单次展开
结论
这个封装需求完全可以实现,核心是处理好转义层级,保证和原始手写逻辑一致的求值顺序即可,不需要特殊的冷门技巧。
实现原理
原始的延迟单次展开逻辑本质是两层求值:
- 第一次给变量赋值时用递归赋值(
=),完全不展开右侧内容,此时不会执行耗时的计算逻辑 - 变量第一次被引用时,先通过
eval把当前变量覆盖为简单赋值(:=)类型,同时在这一步执行实际计算、把结果存入变量,最后返回计算完成的变量值。后续再引用变量时,因为已经是简单变量,直接返回存储的结果,不会重复计算。
封装时只要严格保留这个两层求值的结构,就不会破坏原有逻辑。
可直接使用的宏实现
# 惰性单次赋值宏 # 参数1:需要定义的变量名 # 参数2:变量第一次被引用时才执行的计算逻辑,逻辑内的$需要转义为$$ LAZY_ONCE = $(eval $1 = $$(eval $1 := $2)$$($1))
使用方式
不需要再手写变量名 = $(eval ...)的模板,直接调用宏即可完成定义,效果和手写原始逻辑完全一致:
# 批量定义多个惰性变量,无冗余代码 $(call LAZY_ONCE, OUTPUT, $$(shell ./gen_heavy_output.sh)) $(call LAZY_ONCE, VERSION, $$(shell git describe --tags --dirty)) $(call LAZY_ONCE, DEP_HASH, $$(shell find src -type f | xargs sha256sum | sha256sum)) # 只有执行build目标、实际用到上述变量时,才会执行对应的耗时shell命令 build: @echo "当前版本: $(VERSION)" @echo "依赖哈希: $(DEP_HASH)" @./build.sh -o $(OUTPUT)
关于省略变量名传参的调用形式
你伪代码里写的X = $(call LAZY_ONCE, foo)这类省略变量名传参的调用形式,在原生GNU Make中无法稳定实现:因为变量展开阶段没有内置机制可以获取「当前正在被展开/赋值的变量名」,如果硬要实现只能依赖扫描.VARIABLES变量找最后新增的变量这类歪招,非常容易被其他变量定义操作破坏,完全不建议在生产环境使用。
上面给出的传变量名的实现稳定兼容所有GNU Make 3.81及以上版本,求值顺序和原始手写逻辑完全一致,不会出现提前计算或者无限递归的问题。
常见踩坑点
- 计算逻辑里的
$必须转义为$$,和你直接写在eval里的规则一致,否则会出现提前展开的问题 - 不要在变量第一次被引用前,手动用
eval修改该变量的定义,否则会破坏惰性覆盖逻辑
内容的提问来源于stack exchange,提问作者Underhill
相关产品推荐
相关产品推荐

