1

I've just started with Rust but can't quite grasp lifetimes so I could resolve following issue by myself:

This test project is about simulating a bit to allow tracing it through various bitwise operations, e.g. let newbit = oldbit1 ^ oldbit2 and looking at newbit I can tell afterwards it came out of an XOR operation with oldbit1 and oldbit2 as operands.

#[derive(Copy,Clone)]
pub enum TraceOperation {
        AND,
        OR,
        XOR,
        NOT,
}

#[derive(Copy,Clone)]
pub struct TraceBit<'a> {
        source_a: Option<&'a TraceBit<'a>>,
        source_b: Option<&'a TraceBit<'a>>,
        source_op: Option<TraceOperation>,
        value: bool,
}

This compiles, but I don't fully understand why the lifetime parameters are needed that way. I assume that the compiler cannot expect that the members source_a and source_b live as long as the struct itself as this may not hold true, so explicit lifetimes are required.

  • is this assumption correct?

Further I don't fully understand why I have to re-specify the lifetime parameter for the reference type, i.e. why I have to write source_a: Option<&'a TraceBit<'a>> as opposed to source_a: Option<&'a TraceBit>.

  • What is the second lifetime used for? How do I read that line out loud? I have: "source_a is a variable of type Option that may have Some reference (that is valid at least as long as the struct itself and as long as member source_b) to an instance of TraceBit"

My final issue is that I cannot make it to work using an overloaded operator:

use std::ops::BitXor;
impl<'a> BitXor for TraceBit<'a> {
        type Output = Self;
        fn bitxor(self, rhs: Self) -> Self {
                let valA: usize = if self.value { 1 } else { 0 };
                let valB: usize = if rhs.value { 1 } else { 0 };
                let val = if valA ^ valB != 0 { true } else { false };
                TraceBit { source_a: Some(&self), source_b: Some(&rhs), source_op: Some(TraceOperation::XOR), value: val }
        }
}

This is basically pure guessing based on BitXor documentation. So what I try to do, in a very explicit manner, is to perform an xor operation on the two input variables and create a new TraceBit as output with the inputs stored in it as reference.

error[E0597]: `self` does not live long enough
  --> libbittrace/src/lib.rs:37:30
   |
37 |   TraceBit { source_a: Some(&self), source_b: Some(&rhs), source_op: Some(TraceOperation::XOR), value: val }
   |                              ^^^^ does not live long enough
38 |  }
   |  - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the impl at 31:1...
  --> libbittrace/src/lib.rs:31:1
   |
31 | / impl<'a> BitXor for TraceBit<'a> {
32 | |  type Output = Self;
33 | |  fn bitxor(self, rhs: Self) -> Self {
34 | |   let valA: usize = if self.value { 1 } else { 0 };
...  |
40 | |
41 | | }
   | |_^

error[E0597]: `rhs` does not live long enough
  --> libbittrace/src/lib.rs:37:53
   |
37 |   TraceBit { source_a: Some(&self), source_b: Some(&rhs), source_op: Some(TraceOperation::XOR), value: val }
   |                                                     ^^^ does not live long enough
38 |  }
   |  - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the impl at 31:1...
  --> libbittrace/src/lib.rs:31:1
   |
31 | / impl<'a> BitXor for TraceBit<'a> {
32 | |  type Output = Self;
33 | |  fn bitxor(self, rhs: Self) -> Self {
34 | |   let valA: usize = if self.value { 1 } else { 0 };
...  |
40 | |
41 | | }
   | |_^

error: aborting due to 2 previous errors
  • Seems like nothing lives longer than the xor operation itself, but how can I resolve this?

I've tried various workarounds/changes to the code but to no avail and in any way I rather like to understand the issue than guessing a correct solution....

grasbueschel
  • 879
  • 2
  • 8
  • 24

2 Answers2

0

Tree-like structures must use the Box pointer type (Option<Box<TraceBit>>). In general, in structs you should prefer owned types.

Rust references aren't mere pointers. They are borrows (compile-time read/write locks) of data that must exist as owned somewhere else.

So if you have an owned version of TraceBit:

pub struct TraceBit {
    source_a: Option<Box<TraceBit>>,
}

then reference to it is of type: &'a TraceBit, but references to a type don't change how the type looks internally, so the type of source_a is still Box<TraceBit>. You can keep getting the &'a TraceBit references recursively step by step:

trace_bit = trace_bit.source_a.as_ref().unwrap();

but there's no construct in Rust where taking a reference to the root of a tree suddenly changes the whole tree into a tree of references, so the type you are creating can't exist, and that's why you can't get type annotations right.

Kornel
  • 97,764
  • 37
  • 219
  • 309
-1

Maybe instead of passing references around, you should use a contained and cloneable name type.

use std::rc::Rc;

#[derive(Debug)]
pub enum TraceOperation {
    AND,
    OR,
    XOR,
    NOT,
}

#[derive(Debug)]
pub enum BitName<T> {
    Name(Rc<T>),
    Combination(Rc<(TraceOperation, BitName<T>, BitName<T>)>),
}

impl<T> Clone for BitName<T> {
    fn clone(&self) -> Self {
        match self {
            &BitName::Name(ref x) => BitName::Name(Rc::clone(x)),
            &BitName::Combination(ref x) => BitName::Combination(Rc::clone(x)),

        }
    }
}

impl<T> From<T> for BitName<T> {
    fn from(x:T) -> Self {
        BitName::Name(Rc::new(x))
    }
}

impl<T> BitName<T> {
    pub fn combine(op : TraceOperation, a : &Self, b :&Self) -> Self {
        BitName::Combination(Rc::new((op, (*a).clone(), (*b).clone())))
    }
}

fn main() {
    let x : BitName<String> = BitName::from(String::from("x"));
    let y : BitName<String> = BitName::from(String::from("y"));
    let xandy = BitName::combine(TraceOperation::AND, &x, &y);
    println!("{:?}", xandy);
}
NovaDenizen
  • 5,089
  • 14
  • 28