【问题标题】:How does println! interact with multiple levels of indirection?println!与多个间接级别交互?
【发布时间】:2020-10-03 15:14:31
【问题描述】:

我有以下程序:

fn main() {
    let x = 0;

    println!("Example 1: {:p}", &x);
    println!("Example 1: {:p}", &x);

    println!("Example 2: {:p}", &&x);
    println!("Example 2: {:p}", &&x);
}

这是一个示例输出:

Example 1: 0x7ffcb4e72144
Example 1: 0x7ffcb4e72144
Example 2: 0x7ffcb4e72238
Example 2: 0x7ffcb4e72290

"Example 1" 的输出始终相同,而 "Example 2" 的输出始终不同。

我读过Does println! borrow or own the variable?,我从给定的答案中了解到println! 默默地引用了一个引用。换句话说,这听起来像println! 增加了额外的间接级别。

我曾预计"Example 1" 的输出也会有所不同。看到println! 默默地采取了另一个级别的间接,"Example 1" 实际上正在与&&x 合作,而"Example 2" 正在与&&&x 合作。这似乎与我链接的答案一致,特别是:"If you write println!("{}", &x), you are then dealing with two levels of references"

我认为值&&xholds 将为"Example 1" 打印,而值&&&xholds 将为"Example 2" 打印。 &&x&&&x 都持有一个临时的 &x,所以我认为 "Example 1" 也会打印不同的地址。

我哪里错了?为什么"Example 1" 没有打印不同的地址?

【问题讨论】:

  • 为什么x的地址要改变? &&xx 的地址,当你临时创建这个变量时,两个文字&&x 值相等是未指定的。 println 只为非复制类型借用,所有引用都可以复制,所以它们不会被 println 借用。
  • @SCappella 我不这么认为,因为该问题的答案涉及Display 的行为,它适用于{} 而不是{:p}
  • @Stargateur 我的理解是println!不管是不是非拷贝类型都会借用。例如,我链接的问题的答案在底部:If you write println!("{}", &x), you are then dealing with two levels of references。听起来甚至对x 的引用也应用了额外的间接级别。
  • @MarioIshac 好吧,反正你让 println 打印 &x 的值,所以即使宏带一个 &&x,它也会显示你问的所以 &x,如果有两个也没关系间接级别(我很确定最终 asm 会摆脱它)。您需要了解的是&x&&x 非常不同。

标签: pointers rust


【解决方案1】:

让我们从一个棘手的问题开始:这是否编译?

fn main() {
    println!("{:p}", 1i32);
}

我们要求打印i32 作为内存地址。这有意义吗?

当然不,Rust 理所当然地拒绝了这个程序。

error[E0277]: the trait bound `i32: std::fmt::Pointer` is not satisfied
 --> src/main.rs:2:22
  |
2 |     println!("{:p}", 1i32);
  |                      ^^^^ the trait `std::fmt::Pointer` is not implemented for `i32`
  |
  = note: required by `std::fmt::Pointer::fmt`
  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

但是我们知道宏隐式地借用了参数,所以1i32 变成了&1i32。并且引用实现Pointer。那么有什么关系呢?

首先,它有助于理解为什么宏借用它的参数。你有没有注意到all the formatting traits 看起来几乎一模一样?它们都定义了一个方法,名为fmt,它接受两个参数,&self 和一个&mut Formatter,并返回Result<(), fmt::Error>

与此处相关的是&self。为了调用fmt,我们只需要对该值的引用,因为格式化一个值不需要该值的所有权。现在,格式化参数的实现比这更复杂,但最终,对于参数x,程序最终会调用std::fmt::Pointer::fmt(&x, formatter)(对于:p)。但是,要成功编译此调用,x 的类型必须实现 Pointer不是&x 的类型。如果x1i32,那么x的类型是i32,而i32没有实现Pointer

结论是:p 格式最终会打印出程序中以文本形式编写的表达式所表示的指针的值。该表达式的借用存在,因此宏不获取参数的所有权(这对于 :p 仍然有用,例如,如果您想打印 Box<T>)。


现在我们可以继续解释您的程序的行为。 x 是一个局部变量。局部变量通常1有一个稳定的地址2。在您的Example 1 调用中,表达式&x 允许我们观察该地址。 &x 的两次出现都将给出相同的结果,因为 x 在调用之间没有移动。打印的是x 的地址(即保存值0 的地址)。

不过,&&x 这个表达式有点奇怪。取地址两次究竟是什么意思?子表达式&x 产生一个临时值,因为结果未分配给变量。然后,我们询问该临时值的地址。 Rust is kind enough to let us do that,但这意味着我们必须将临时值存储在内存中的某个位置,以便它具有 some 地址。在这里,临时值存储在某个隐藏的局部变量中。

事实证明,在调试版本中,编译器为&&x 的两次出现中的每个&x 子表达式创建一个单独的隐藏变量。这就是为什么我们可以观察到Example 2 行的两个不同内存地址。但是,in release builds,代码被优化,只创建了一个隐藏变量(因为在我们需要第二个的时候,我们不再需要第一个,所以我们可以重用它的内存位置),所以两个Example 2 行实际上打印相同的内存地址!


1 我说通常是因为在某些情况下优化器可能会决定在内存中移动一个局部变量。我不知道是否有任何优化器在实践中确实做到了这一点。

2 一些局部变量可能根本没有“地址”!如果从未观察到该变量的地址,则优化器可能会决定将局部变量保留在寄存器中。在许多处理器架构上,寄存器不能通过指针来寻址,因为可以说它们位于不同的“地址空间”中。当然,在这里,我们正在观察地址,因此我们可以非常确信该变量实际上存在于堆栈中。

【讨论】:

  • 这是一个非常棒的答案!由于std::fmt::Pointer::fmt 引用了Pointer 的实现者,听起来它会在内部进行一些取消引用以消除借用而不是拥有引入的额外间接级别(因此它可以打印实现者的十六进制格式值Pointer 而不是上一级的参考)。这是一种正确的思考方式吗?
猜你喜欢
  • 1970-01-01
  • 2021-09-27
  • 2014-03-01
  • 1970-01-01
  • 2022-01-19
  • 2017-05-25
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多