☰
Rust placement-new 语法之争:RFC 1228 的 `PLACE_EXPR <- VALUE_EXPR` 左箭头方案解读
2026/10/15 6:41:50 网站建设 项目流程
  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

本文深度解读 Rust RFC 仓库中的 RFC 1228:placement-left-arrow。该 RFC 提出用左箭头运算符PLACE_EXPR <- VALUE_EXPR作为 placement-new(放置式构造)的统一语法,以取代当时基于in关键字的in PLACE { BLOCK }形式。读完本文,你将完整掌握该方案的设计动机、三段式执行语义、脱糖(desugaring)原理、四种优先级候选方案,以及它最终被撤回的历史背景与后续影响。

背景:placement-new 与「中间拷贝」问题

placement-new 的概念源自 C++:不是先构造一个值再拷贝/移动到容器里,而是把值表达式直接「放置」到预先准备好的内存位置(place)上,从而避免不必要的中间拷贝。在 Rust 语境下,典型场景包括:

  • 向Vec尾部追加一个大型结构体时,希望直接在该位置初始化元素;
  • 向竞技场分配器(arena)申请内存时,希望把值直接构造进分配好的槽位,并返回对该槽位的引用或句柄;
  • 构造Rc<T>/Arc<T>时希望直接把值写入其内嵌存储,避免先构造T再拷贝。

Rust 社区对这一能力的语法形式经历了多轮演进。最早的形式是box (PLACE_EXPR) VALUE_EXPR,随后在 RFC 809 的讨论中转向基于in关键字的写法,最终落地为in PLACE_EXPR { BLOCK_EXPR }。而 RFC 1228 正是对这一「第二版」语法的又一次重大修正提案。

RFC 1228 提案核心:PLACE_EXPR <- VALUE_EXPR

RFC 1228 的 Summary 非常直白:与其继续在in关键字上寻找巧妙语法,不如直接采用

PLACE_EXPR <- VALUE_EXPR

这个形式之所以可行,是因为<-这个 token 早已因历史巧合(historical accident)被 Rust 保留(reserved),而这一次巧合「罕见地站在了我们这边」——一个既有的保留 token 可以直接被征用为运算符,无需为语法让路。

三段式执行语义

RFC 1228 明确指出,PLACE_EXPR <- VALUE_EXPR的静态与动态语义与box (PLACE_EXPR) VALUE_EXPR完全等价,仍然是一种按以下三步工作的表达式形式:

  1. 将PLACE_EXPR求值为一个 place(放置位置);
  2. 将VALUE_EXPR直接求值并写入这个已构造好的 place;
  3. 返回「定稿后的 place 值」(finalized place value)。

(该行为背后的底层协议详见 RFC 809 的文档说明。)

实战示例:竞技场分配

RFC 1228 给出了最典型的应用场景——向竞技场分配器申请槽位。竞技场(arena)需要同时接收「竞技场本身」和一个「值表达式」,并返回被分配条目的引用或句柄,因此返回值不能是():

let ref_1 = arena <- value_expression; let ref_2 = arena <- value_expression;

对照同一逻辑在 RFC 809 语法下的写法:

let ref_1 = in arena { value_expression }; let ref_2 = in arena { value_expression };

两者的差异一眼可见:左箭头版本更接近普通赋值语句的形态,而in arena { ... }看起来像某种控制流结构。

动机:为什么「左箭头」优于in语法

RFC 1228 的动机部分用一句话概括:

语法a <- b简短、可以无歧义地解析,并且已经与「赋值」形成了强烈的语义关联。

围绕此结论,RFC 正文还整理了大量来自 RFC 809 讨论期的社区反馈,用以说明in语法没能解决的那些反对意见:

  • kennytm 的反对(关于in (place) value形式):

    虽然in (place) value在语法上无歧义,但单独作为一条语句时看起来非常不自然,主要是因为动词没有出现在正确的位置;而且单独使用in通常让人联想到迭代(for x in y)和成员测试(elem in set)。

  • petrochenkov 的反对(强调语法要尽量短小便捷):

    正如 C++11 的经验所证明的,一旦这种能力可用,它就会成为容器插入元素的默认方式,因为它绝不比「普通插入」更差,而且常常更好。所以它真的应该拥有尽可能短小便捷的语法。

  • p1start 的反对(针对in <place> { <stmts> }的块状形式):

    我不喜欢in <place> { <stmts> },仅仅因为块(block)这个要求暗示它是一种控制流结构,或者暗示块内的所有语句都会以某种方式在给定的<place>中执行。这将是我们第一个本质上只是一个运算符、却不得不带一个块操作数的语法构造。

RFC 1228 的作者(pnkfelix)认为,PLACE_EXPR <- VALUE_EXPR语法能够回应上述全部顾虑:它是真正的二元运算符形态、没有块、没有in关键字带来的迭代/成员测试联想,且天然满足「占位符在左、值在右」的从左到右求值顺序。

详细设计:解析、脱糖与 feature gate

RFC 1228 的 Detailed design 部分对实现路径给出了具体规划。

语法与结合性

  • 扩展解析器以解析EXPR <- EXPR;
  • 左箭头运算符右结合(right-associative);
  • 优先级高于赋值与复合赋值(binop-assignment),但低于其他二元运算符(这是 2016-04-22 由 [RFC 1319] 修订后的结论,详见下文「优先级设计」一节)。

脱糖方式

EXPR <- EXPR会被解析成某种 AST 形式,并按照与in EXPR { BLOCK }或box (EXPR) EXPR相同的方式脱糖(该脱糖实现由 rust-lang/rust 的 PR 27215 落地)。

feature gate 的处理

该解析形式可以单独设置 feature gate;但由于 placement-in语法刚随 PR 27215 落地、本身仍处于placement_in_syntaxfeature gate 之下,也可以选择直接把这一改动并入既有的placement_in_syntaxgate——尽管这有点反直觉,因为新语法中已经不再出现in关键字。

值得注意的是,RFC 1228 提到该特性已经有一个原型实现(作者在place-left-syntax分支上完成了 prototype),说明解析与脱糖路径的可行性在当时已得到验证。

移除旧语法:让<-成为「唯一正道」

在足够的快照(snapshot)和时间窗口之后,计划移除以下两种旧语法:

  • box (PLACE_EXPR) VALUE_EXPR
  • in PLACE_EXPR { VALUE_BLOCK }

即PLACE_EXPR <- VALUE_EXPR将成为表达 placement-new 的唯一方式("one true way")。同时需要注意:

  • box VALUE_EXPR(无 place 参数的重载盒语法)将继续保留;
  • 作为顺带收益,(box ())这个表达式将变得无歧义、从而可以合法化——RFC 作者调侃说「毕竟那些『单元的盒子』的语法真的很值得优化」。

文档层面的配套动作

RFC 1228 还建议:作为流程的一部分,应修订 RFC 809 的正文,让其改用a <- b语法——因为很多人即使 RFC 已过时,仍会把它当作参考来源;如果做不到,至少要在 RFC 809 中加上指向本 RFC 的前向引用。

语义深挖:placement 协议与脱糖示例

要真正理解PLACE_EXPR <- VALUE_EXPR在底层做什么,需要回到 RFC 809 的 Appendix A。它用宏演示了 placement 与重载box的脱糖过程,并给出了完整的协议 trait 设计。

in PLACE { BLOCK }的脱糖

let p = PLACE; let mut place = Placer::make_place(p); let raw_place = Place::pointer(&mut place); let value = { BLOCK }; unsafe { std::ptr::write(raw_place, value); InPlace::finalize(place) }

重载box EXPR的脱糖

let mut place = BoxPlace::make_place(); let raw_place = Place::pointer(&mut place); let value = $value; unsafe { ::std::ptr::write(raw_place, value); Boxed::finalize(place) }

两者唯一的差别在于finalize的重载依据:placement 形式依据place 参数(其类型源自<place-expr>)选择InPlace::finalize;而box形式依据期望的返回类型选择Boxed::finalize。这决定了同一个表达式在两种语境下的类型推导方向不同。

协议 trait 一览

RFC 809 附录 A 中演示的协议由以下 trait 组成(在正式实现中它们属于core::ops,且刻意保持不稳定状态,以便语言设计者在 1.0 之后仍可修订底层协议):

/// 表示对未初始化存储的指针 pub trait Place<Data: ?Sized> { fn pointer(&mut self) -> *mut Data; } /// `in PLACE { BLOCK }` 的入口:由 `PLACE` 的值构造出一个中间 place pub trait Placer<Data: ?Sized> { type Place: InPlace<Data>; fn make_place(self) -> Self::Place; } /// `in PLACE { BLOCK }` 的定稿接口 pub trait InPlace<Data: ?Sized>: Place<Data> { type Owner; unsafe fn finalize(self) -> Self::Owner; } /// `box EXPR` 的核心 trait pub trait Boxed { type Data; type Place; unsafe fn finalize(filled: Self::Place) -> Self; } /// `box EXPR` 的 place 特化 pub trait BoxPlace<Data: ?Sized>: Place<Data> { fn make_place() -> Self; }

几个关键语义点:

  • Place可以理解为「假想的&uninit引用」的替身——它代表指向未初始化存储的指针,初始化时应使用ptr::write而不是直接赋值;
  • in PLACE { BLOCK }的整体类型由PLACE的类型推导:若PLACE类型为P,则整个表达式类型为P::Place::Owner;
  • 当in PLACE { BLOCK }只用于对既有数据结构做副作用(如Vec::emplace_back)时,Owner可以是单元类型(),不携带任何信息;
  • 协议必须正确应对 panic:如果求值VALUE_EXPR时 panic,Place实现的析构函数必须清理所有中间状态(如释放盒存储、弹出栈顶元素等)。

Vec::emplace_back的示例实现要点

RFC 809 附录 A 给出了一个完整的Vec::emplace_back演示实现,其核心思路是:

  • VecPlacer持有&'a mut Vec<T>,通过Placer::make_place生成VecPlace;
  • VecPlace::pointer先记录当前长度、push一个mem::zeroed()占位,再返回新槽位的裸指针供ptr::write写入真实值;
  • InPlace::finalize返回()并mem::forget(self),把清理责任转移掉;
  • 同时为VecPlace实现Drop:在写入过程中发生 panic 时pop掉占位元素,保证Vec内部状态一致。

这套协议正是PLACE_EXPR <- VALUE_EXPR在底层要对接的同一套机制——左箭头语法只是它的「表面语法」(surface syntax)之一。

优先级设计:四种候选方案

RFC 1228 花了较大篇幅讨论<-运算符的优先级,给出了从「低于赋值」到「高于一切运算符」的完整谱系。这是该 RFC 最具设计深度的部分,四种方案与各自的解析结果如下。

方案 1:低于赋值

假设()永远不会成为Placer,那么x = y <- z这类语句将因类型检查不通过而迫使程序员写出x = (y <- z),从而回应「x = y <- z含义不清」的常见抱怨。但代价是let x =与x =在解析上产生不一致:let x = (y <- z)成立,而(x = z) <- y也成立。

方案 2:与赋值、复合赋值同级

x = y <- z = a <- b = c = d <- e <- f

解析为:

x = (y <- (z = (a <- (b = (c = (d <- (e <- f)))))))

这是当时编译器中最容易实现的方案。

方案 3:高于赋值/复合赋值、低于其他二元运算符(本 RFC 采用)

这允许含等号与<-的各种表达式被合理且一致地解析。例如:

x = y <- z += a <- b <- c

解析为:

x = ((y <- z) += (a <- (b <- c)))

方案 4:高于一切运算符

适合想强制使用者大部分时间把<-两侧都括进括号的场景:

x <- y <- z * a

解析为:

(x <- (y <- z)) * a

权衡与备选方案

Drawback:意图不如关键字清晰

RFC 1228 唯一明确的 drawback 来自 nikomataskis 在 RFC 809 讨论中的评论:

其意图不如使用一个专用关键字来得清晰。

但 RFC 作者指出,该评论原本是针对「重载=运算符」这一假设情形提出的;而<-本身可以视为一种足够「专用」(devoted)的独立语法,足以平息上述顾虑。

备选方案:in与<-并用

如果希望强调这不是普通的赋值,可以把in与<-组合起来,得到:

let ref_1 = in arena <- value_expression; let ref_2 = in arena <- value_expression;

此外,RFC 809 的 Alternatives 章节还讨论过其他表面语法,例如in (<place-expr>) <expr>、box (in <place-expr>) <expr>等,均可作为本方案的横向参照。

未决问题与变更记录

优先级悬而未决

RFC 1228 的 Unresolved questions 只留下一个问题:<-运算符的优先级到底取多少?具体来说,让<-与=同级可能更合理(相关论点见 huon 在 RFC 1319 讨论中的评论)。最终答案大概率取决于a <- b的结果是否会被高频地组合使用、以何种方式组合,因此作者决定暂缓决策,等社区中出现更多实际用法后再定夺。

变更记录

  • 2016-04-22:由 rust-lang/rfcs#1319 修订,调整了<-的优先级(即上文采用的「高于赋值、低于其他二元运算符」方案)。

结局与历史意义:一份被撤回的 RFC

需要特别说明的是,RFC 1228 与 RFC 809 目前在仓库中都标注着同一状态:曾被批准(approved),但后来被撤回(withdrawn),细节见各自头部引用的跟踪 issue 总结评论。这意味着:

  • 左箭头语法最终没有成为 Rust 的稳定语法——可以推断,当时相关的 placement 语法线整体没有走向稳定;
  • 但该 RFC 的设计价值依然成立:它是理解「放置式构造需要什么样的表面语法」的关键历史文献;
  • 后续的语法类 RFC 还把它当作反面/正面参考。例如 RFC 1331:grammar-is-canonical 在动机部分明确点名#1228(连同#1219、#1192)是「近期合并的、以非正式方式描述语法变更」的 RFC 之一,正是这类经验催生了「以src/grammar形式化文法作为规范」的后续改革。

从今天的 Rust 回望,in PLACE { BLOCK }与PLACE_EXPR <- VALUE_EXPR都没有进入稳定语言,Vec尾插避免中间拷贝的诉求最终通过vec![...]的宏内联等手段部分满足,而 placement 语法本身则长期停留在 RFC 历史的档案中。RFC 1228 的「三段式语义 + 优先级谱系 + 协议脱糖」讨论框架,依然是任何想在 Rust 中设计二元运算符式内存构造语法的后来者最值得研读的起点。

延伸阅读

  • RFC 1228 原文:placement-left-arrow
  • RFC 809 原文:box-and-in-for-stdlib(placement 协议与脱糖示例、附录 A/B)
  • RFC 1331:grammar-is-canonical(对语法类 RFC 的形式化文法要求,点名引用本 RFC)
  • RFC 过程说明(了解「active / withdrawn」等 RFC 生命周期的含义)
  • 文档

【免费下载链接】rfcs

RFCs for changes to Rust

项目地址:https://gitcode.com/gh_mirrors/rf/rfcs
点击查看免费下载

相关推荐

上一篇:PlayIntegrityFix终极实战指南:3步解决Google Play设备认证问题
下一篇:3步掌握Sniffles2:长读测序结构变异检测的终极解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询