RUST 所有权移动问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
caobug
V2EX    Rust

RUST 所有权移动问题

  •  
  •   caobug 2023-10-01 03:46:06 +08:00 2768 次点击
    是一个创建于 746 天前的主题,其中的信息可能已经有所发展或是发生改变。

    刚随手写了一个示例,尝试探测编译器检测机制。以下代码在 JAVA/C/C++等众多语言中没有任何问题,即使按照 RUST 的所有权原则,也应该正常。但实际上编译失败了。

    struct Dog { name: String, } impl Dog { fn release(self) { println!("{}", self.name) } } struct Person { dog: Dog, name: String, } fn main() { let mut person = Person { dog: Dog { name: String::from("Hamel"), }, name: String::from("Zoe"), }; person.dog.release(); for _ in 0..1 { person.dog = Dog { name: String::from("Hogge"), }; } println!("{} got a new dog: {}", person.name, person.dog.name); } 

    以上代码我先通过 Person 关联了 Dog ,随后释放 Person 中的 Dog ,到此 Dog 不再有效,若后续直接调用会编译失败。

    我跟着创建一个百分比会执行的 for 循环重新赋值,按理说指针已发生了变更,后续在循环外使用的 Dog 是新建的对象不会有问题。

    但事上上编译失败。Rust 的检测器似乎没有认真评估 for 中的条件,认为 loop 至少执行一次,而 for 却不确定。当我把 for 改成 loop ,它确实正常了。

    loop { person.dog = Dog { name: String::from("Hogge"), }; break } 

    我接着在 loop 中加一个百分比会执行的条件判断,最终仍然失败了。

    loop { if true { person.dog = Dog { name: String::from("Hogge"), }; } break } 

    如果说写 C/C++满脑袋都是寄存器、堆、栈、指针...,那么 RUST 必然是所有权检查器。

    24 条回复    2023-10-11 15:37:51 +08:00
    dianqk
        1
    dianqk  
       2023-10-01 06:06:54 +08:00 via Android
    (似乎是 bug ,感觉可以提个 issue
    binhb
        2
    binhb  
       2023-10-01 06:48:40 +08:00
    因为是静态所有权检查,条件不确定,可能不被执行
    qdwang
        3
    qdwang  
       2023-10-01 09:02:57 +08:00 via iPad
    rust 编译过程中,除非是特殊指定在编译器运算的代码(比如 const fn ),其他代码都是不会运算的,只能根据一些特殊情况做一些处理。比如 loop 内非条件语句下认定为会执行到。for 里不一定必然执行。还有例如有些循环情况算必然执行多次,这样就不可以用 FnOnce 这样。
    Leviathann
        4
    Leviathann  
       2023-10-01 11:36:15 +08:00
    所有权检查实际上是一种证明
    你要做的是向编译器提供证据
    rrfeng
        5
    rrfeng  
       2023-10-01 12:06:06 +08:00 via Android
    这跟不定长数组越界访问一样啊,编译器不会检查条件的。
    lance6716
        6
    lance6716  
       2023-10-01 19:28:31 +08:00 via Android
    只是有个度不想继续细化了,毕竟停机问题
    Kaiv2
        7
    Kaiv2  
       2023-10-02 09:48:45 +08:00
    loop {
    if true {
    person.dog = Dog {
    name: String::from("Hogge"),
    };
    break;
    }
    }
    编译成功
    caobug
        8
    caobug  
    OP
       2023-10-02 13:26:26 +08:00
    @Kaiv2 RUST 编译检查默认会检查完整的分支,即 if 和 else
    swordcoming9527
        9
    swordcoming9527  
       2023-10-03 20:00:01 +08:00
    硬要解决这个问题只能等待以后编译器在各种 edge case 优化的更好,但实践中可能没啥意义。
    这个问题,是 People 里面的 dog 可能被 Move ,dog 被 Move 时,People 其实是处于一种“不可用”的状态,在 dog 被填充回来之前,对 People 的其他操作都是不允许的。如果在真实的实践中,只需要提前设计好 dog 被 Move 的状态,典型的就是 dog: Option<Dog>。
    caobug
        10
    caobug  
    OP
       2023-10-03 23:49:03 +08:00
    @swordcoming9527 确实如此。不过编译器应该要检查 in 0..1 才对,甚至编译后应该直接展开这种硬编码。
    buxiuxi
        11
    buxiuxi  
       2023-10-04 13:16:35 +08:00
    为啥这里 release 函数会释放 dog?
    owtotwo
        12
    owtotwo  
       2023-10-08 15:00:36 +08:00
    直接简化问题,此问题本质上,与下面代码无法编译通过的原因是一致的:


    **即在编译期 Borrow Checker 是不进行具体值计算的。**

    如上面的 if 分支,在生命周期检查期间,并不知道 1 == 0 总为 false 且永不执行此代码块。故而在使用 name 时,无法得知所有权是否已被转移。
    甚至将 1 == 0 直接换成 false ,此期间 Borrow Checker 依旧不知道 false 总不执行代码块,即认为是**有可能**调用 drop(name)触发所有权转移的。
    owtotwo
        13
    owtotwo  
       2023-10-08 15:30:23 +08:00
    回到原问题,person.dog 的各种赋值写法:


    0. 对于 Code 0 ,即上一条回复提到的本质问题,此处尽管写着 if true ,然而 Borrow Checker 并不默认此代码总是执行,而是认为可能执行也可能不执行。(当然,当 build release 编译优化阶段时,if true 就会被优化掉了,而所有权检查阶段并不会,或许是太耗时了)

    1. 对于 Code 1 ,与 Code 0 本质一样,Borrow Checker 会认为此 for 语句可能不会执行,所以“Rust 的检测器似乎没有认真评估 for 中的条件”确实是对的,并不会在此阶段评估。

    2. 对于 Code 2 ,因为有 else 分支且调用 unreachable!(),所以理论上后续使用 person 时必然已经经过 if 部分的赋值语句(因为 else 部分会 panic ,即不会执行后面代码)
    owtotwo
        14
    owtotwo  
       2023-10-08 15:40:44 +08:00
    续上

    3. 对于 Code 3 ,即楼主的第一次修改尝试,实际上等价于将 loop 和 break 去掉(因为此控制流总是执行),所以必然会执行赋值语句,故而编译通过(Ok)。

    4. 对于 Code 4 ,即楼主的第二次修改尝试,依然可以将 loop 和 break 去掉,此时情况等价于 Code 0 ,所以一样是编译不通过(Error)。

    5. 对于 Code 5 ,即 @Kaiv2 提到的编译成功的写法,因为 break 进去 if 里了,情况就不一样了。此时控制流的逻辑只有两种情况:情况一,若不进入 if 语句里,则无限循环,那么就不会执行后面的使用 person 及 person.dog 的代码了,就没问题;情况二,进入 if 语句,则成功执行赋值,且最后必定 break 出去,执行后面的代码,依然没问题。所以这种不涉及对具体求值有依赖的控制流是能编译通过的。
    owtotwo
        15
    owtotwo  
       2023-10-08 15:52:55 +08:00   2
    综上

    6. 楼主的代码符合 Rust 的所有权规则吗?符合的,因为问题并不在所有权转移上,而是在编译期所有权检查时是否会进行具体求值的判断上。

    7. @DianQK 所以**目前**而言不是 bug ,或许以后编译器更聪明效率更高了就支持此优化了。

    8. @binhb @qdwang @rrfeng 的说法是对的。

    9. 为啥这里 release 函数会释放 dog ?因为 fn release(self)的参数是 self ,跟 std::mem::drop()一样,调用时会获取其所有权,并在此函数结束后 drop 掉。
    owtotwo
        16
    owtotwo  
       2023-10-08 15:54:39 +08:00
    FIX: @buxiuxi 第 9 项忘 at 了
    kerwincsc
        18
    kerwincsc  
       2023-10-09 13:14:23 +08:00
    @buxiuxi 因为 release 方法签名是 self
    PTLin
        19
    PTLin  
       2023-10-09 13:49:02 +08:00
    ```
    loop {
    if true {
    person.dog = Dog {
    name: String::from("Hogge"),
    };
    break;
    }
    }
    ```
    你这把 break 写在 if 里面不就可以了吗,这种 edge case 也算是满脑子都是的东西?
    owtotwo
        20
    owtotwo  
       2023-10-10 15:19:35 +08:00
    @DianQK #17
    或许有些区别
    简单点概况,NLL(Non-Lexical Lifetimes)的迭代进化(即下一代的 Polonius)应该依然并不能解决此问题。

    因为楼主这问题本质上是 rustc 编译流程的限制。如图: https://blog.rust-lang.org/images/2016-04-MIR/flow.svg
    根据**目前**的编译流程,Borrow Checking 发生在 MIR 阶段,此刻的 CFG(Control-Flow Graph)仅将`if true {}`识别为`if some_cond {}`。
    故而`if true { <code> }`无法等价于`loop { <code>; break }`或`{ <code> }`,因为它们的 CFG 是不一致的。(参考引用 数据流分析中的 CFG )
    最后将类似`if false {}`这样的死代码消除的优化行为,是在 LLVM 的 codegen 优化阶段进行的,所以此前 borrowck 并不认识"true"或"false"。

    在 Rust 2018(Rust 1.31)引入 NLL 后,生命周期的推断精度更高了,而下一代的 Polonius 会支持更复杂的控制流(Control Flow)。但是如上所述的原由,依然不能在此阶段进行条件求值,所以问题依在。

    * NLL: https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html#non-lexical-lifetimes
    * 编译流程中的 MIR: https://blog.rust-lang.org/2016/04/19/MIR.html
    * 数据流分析中的 CFG: https://github.com/rust-lang/rustc-dev-guide/blob/master/src/appendix/background.md#what-is-a-dataflow-analysis
    * rustc 概览(包含各编译阶段): https://rustc-dev-guide.rust-lang.org/overview.html


    我希望能由浅入深解释问题,但无法太深。前面的回答并无涉及到更多的编译器部分的具体内容,是因太冗长容易导致阅读阻力大,很少人愿意认真看(完)。(但即使现在这长度,似乎大家也习惯 tl;dr 了)

    希望我有解释清楚。 : )
    dianqk
        21
    dianqk  
       2023-10-10 22:29:39 +08:00 via Android
    @owtotwo #20
    除了 RFC 和具体代码我还没看到,剩下的我应该基本了解。如果先做一些常量传播,再做借用检查就可以了(但是常量传播可能也需要先有一个合格的借用检查?)
    caobug
        22
    caobug  
    OP
       2023-10-11 05:08:53 +08:00
    @owtotwo 非常感谢,你解释的非常清楚远超 RUST 指南,恳请抽空出些 RUST 文章。
    owtotwo
        23
    owtotwo  
       2023-10-11 15:07:30 +08:00   1
    @DianQK #21
    嗯呐 这个确实就是关键所在

    若我没记错的话 似乎 rustc 的编译流程会有两次的常量折叠/传播 一次是在前端的 MIR 中 另一次是在后端如 LLVM 中
    (好像是前端优化一次能降低给后端的 IR 代码复杂度)

    MIR 中支持常量传播应该是比较早前的事了(或许有相关公告) 似乎是支持控制流的(代码可能在 mir 部分的 const_prop.rs ?文件名应该长得差不多)
    但并不知道是否支持“消掉 if const_expr”的行为
    (我不知道这种分支优化的术语应该是什么 死代码消除 Dead Code Elim ?或者是叫 Sparse Cond Const Prop ?中文可能是 稀疏条件常量传播 之类的 或许也不准确)

    但比较尴尬的是 常量传播是在 MIR 的优化阶段进行的 而 borrowck 是在 mir-opt 之前进行的(如果我没记错的话)

    所以正如老哥你所说的 常量传播时应该已经有借用检查了
    (以及我感觉理论上应该确实是能在借用检查前算 const 的 就是不知道最终会不会增加 MIR 部分编译的总耗时)

    编译流程层面的改动影响对 rustc 而言还是挺大的(如 Polonius 也只是 borrowck 部分的平替) 所以短期内可能不会有相应优化了(个人感觉 不知道目前有没有人提对应的 RFC )


    以上的话并不严谨 我也暂时没能去进行校验 或许会有些错漏或过时(记忆有点旧了)

    有条件的朋友或许可以补充下相关链接~
    owtotwo
    nbsp;   24
    owtotwo  
       2023-10-11 15:37:51 +08:00   1
    @caobug #22
    有讲清楚了就好 能对别人有帮助还是很开心的 : )

    Rust 文章的话 不太好写 写起来时很难兼顾到不同熟练度的 Rust 小伙伴(主要还是我自己能力有限)

    举个例子
    就像一开始其实我想**直接**用 `while true {}`和`loop {}`为什么不一样 来解释这问题的(前者迭代 0+次 后者 1+次)
    但是这样会引入题目中没提到的 while 语句 以及它们另外的差异( loop-break <value>能返回值而 while 恒为`()`之类的) 最后还得再迁移到 for 语句来解释

    这样就有可能将问题复杂化了 而如果跳过中间例子直接说“const_expr 在 borrowck 阶段不求值” 不熟悉的小伙伴有可能一下子转不过来

    且问题涉及 if 语句 所以最后决定用`if false {}`作例子来渐进地解释 比较好理解

    Rust 文章同理 讲一个点 从多浅讲到多深 我就有点犯难了
    有的知识点实在涉及太多 比如 Pin 感觉没十几页纸实在讲不清楚 一想就头都大了 0.o
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5488 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 06:05 PVG 14:05 LAX 23:05 JFK 02:05
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86