【问题标题】:How do I implement the Add trait for a reference to a struct?如何实现 Add trait 以引用结构?
【发布时间】:2015-03-16 07:23:11
【问题描述】:

我创建了两个元素 Vector 结构,我想重载 + 运算符。

我让我的所有函数和方法都采用引用而不是值,并且我希望 + 运算符以相同的方式工作。

impl Add for Vector {
    fn add(&self, other: &Vector) -> Vector {
        Vector {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

根据我尝试的变体,我可能会遇到终身问题或类型不匹配。具体来说,&self 参数似乎没有被视为正确的类型。

我在implAdd 上看到了带有模板参数的示例,但它们只会导致不同的错误。

我找到了How can an operator be overloaded for different RHS types and return values?,但即使我在顶部放了use std::ops::Mul;,答案中的代码也不起作用。

我正在使用 rustc 1.0.0-nightly (ed530d7a3 2015-01-16 22:41:16 +0000)

我不会接受“你只有两个字段,为什么要使用参考”作为答案;如果我想要一个 100 个元素的结构怎么办?我会接受一个答案,证明即使有一个大的结构,我也应该按值传递,如果是这样的话(但我不认为是这样。)我有兴趣了解结构大小的一个好的经验法则并通过值与结构传递,但这不是当前的问题。

【问题讨论】:

  • “如果我想要一个 100 个元素的结构怎么办” - Rust 使用 RVO 等优化,它会在适当的时候自动使用引用和更好的选择。
  • @Shepmaster:RVO 只会影响返回值,我按值返回。您能否指出任何说明大型结构的特征应该按值实现的文档?
  • 我所知道的最好的文档是book chapter on returning pointers。但是,我created an example of adding a large struct 并检查了生成的 LLVM(略微清理):(%struct.Big* sret, %struct.Big*, %struct.Big*)。我不声称自己是 LLVM 专家,但看起来它会自动通过引用获取和返回。
  • 文档也提到了返回值,我同意它不应该是一个参考。事实上,文档曾经说过,除非需要,否则不应将指针用于输入参数,但实际上已将其删除。此外,我将您的示例更改为按引用传递,发现它删除了两个分配(%arg7 = alloca %struct.Big, align 8%arg8 = alloca %struct.Big, align 8),因此至少对于大型结构来说,引用更好。
  • 我应该指出,我对 LLVM 的了解比任何人都少,所以我的解释可能都是湿的。使用引用进行运算符重载的另一个明显缺点是,如果您碰巧没有引用,let c = (&a) + (&b); 会很烦人。

标签: reference rust traits lifetime


【解决方案1】:

您需要在&Vector 而不是Vector 上实现Add

impl<'a, 'b> Add<&'b Vector> for &'a Vector {
    type Output = Vector;

    fn add(self, other: &'b Vector) -> Vector {
        Vector {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

在其定义中,Add::add 始终按值获取 self。但是引用是与其他任何1 一样的类型,因此它们也可以实现特征。在引用类型上实现 trait 时,self 的类型是引用;引用是按值传递的。通常,在 Rust 中按值传递意味着转移所有权,但是当按值传递引用时,它们只是被复制(或者如果它是可变引用,则被重新借用/移动),并且不会转移所指对象的所有权(因为引用首先不拥有它的所指对象)。考虑到这一切,Add::add(和许多其他运算符)按值获取 self 是有意义的:如果您需要获得操作数的所有权,您可以直接在结构/枚举上实现 Add,如果您不要,你可以在引用上实现Add

这里,self&amp;'a Vector 类型,因为这是我们正在实现 Add 的类型。

请注意,我还为 RHS 类型参数指定了不同的生命周期,以强调两个输入参数的生命周期是不相关的。


1 实际上,引用类型的特殊之处在于,您可以为在您的 crate 中定义的类型的引用实现特征(即,如果您被允许为 T 实现特征,那么您re 也允许为&amp;T 实现它。 &amp;mut TBox&lt;T&gt; 具有相同的行为,但对于 U&lt;T&gt; 通常情况并非如此,其中 U 未在同一个 crate 中定义。

【讨论】:

  • "Add::add 总是按值获取 self。这里,self 的类型是 &'a Vector,因为这是我们正在实现 Add 的类型。"这是关键信息,自我的类型会根据特征是否用于参考而改变。谢谢!
  • 哇。很惊讶这是正确的答案,但事实确实如此。这一切都让人感觉很反直觉。你可以用两种不同的方式来定义 Add ,这取决于它是否是一个参考。
  • 要反弹@Squirrel 的评论,我们是否应该从另一个操作中实现一个操作以避免冗余?是按引用添加复制值并应用按值添加,还是按值添加对移动的值应用按引用添加?
  • 是的,绝对!代码重用总是好的! :) 通常,如果值的克隆/复制成本低廉,您会希望按值实现成为主要实现,否则,您会希望按引用实现。
  • 你实际上甚至需要实现所有组合,如果一个是 ref,另一个不是,反之亦然。在 rust 中,它是通过宏完成的...stackoverflow.com/questions/38811387/…
【解决方案2】:

如果要支持所有场景,则必须支持所有组合:

  • &T op U
  • 顶部&U
  • &T op &U
  • T op U

在 rust 中,this was done through an internal macro.

幸运的是,有一个 rust crate impl_ops,它还提供了一个宏来为我们编写该样板:该 crate 提供了 impl_op_ex! 宏,它可以生成所有组合。

这是他们的示例:

#[macro_use] extern crate impl_ops;
use std::ops;

impl_op_ex!(+ |a: &DonkeyKong, b: &DonkeyKong| -> i32 { a.bananas + b.bananas });

fn main() {
    let total_bananas = &DonkeyKong::new(2) + &DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = &DonkeyKong::new(2) + DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = DonkeyKong::new(2) + &DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
    let total_bananas = DonkeyKong::new(2) + DonkeyKong::new(4);
    assert_eq!(6, total_bananas);
}

更好的是,他们有一个impl_op_ex_commutative!,如果您的运算符恰好是可交换的,它还会生成参数反转的运算符。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-21
    • 1970-01-01
    • 2020-01-25
    • 2023-03-06
    • 2020-03-24
    相关资源
    最近更新 更多