3
votes

I have a problem that I don't know exactly how to model into Rust, in regard with ownership, lifetime and all that.

I have a large struct:

struct LargeStruct {
    x: f32,
    n: i32,
    // lot of other fields.
}

The struct being large, I want to avoid copies, and therefore I carry around pointers to it.

I have a foo function that takes a pointer to LargeStruct and a f32 value. If this value is larger than the field x, I want to create a new object with x set to this value, and returns it. If it is not larger, then I want to return the original pointer itself.

I naively implemented it like this:

fn foo(object: &LargeStruct, y: f32) -> &LargeStruct {
    if object.x < y {
        LargeStruct {
            x: y,
            n: object.n,
            // ...
        }
    }
    else {
        object
    }
}

But it does not work: the two branches of the if do no return the same type. In the first case, I actually return a LargeStruct, and in the second I return a &LargeStruct. If I modify the object construction to take its pointer:

    &LargeStruct {

then it doesn't work either: the lifetime of the object constructed is too short, so I can not return that from the function.

If I try to build the object on the heap:

    ~LargeStruct {

I have now another compilation error:

if and else have incompatible types: expected ~LargeStruct but found &LargeStruct (expected &-ptr but found ~-ptr)

I tried to specify a lifetime in the function signature:

fn foo<'a>(object: &'a LargeStruct, y: f32) -> &'a LargeStruct {

But it does not help: I don't know how to build a new LargeStruct with the same lifetime.

I am calling this function like this:

fn main() {
    let object = LargeStruct{ 
        x: 1.0, 
        n: 2,
        // ...
    };

    let result = foo(&object, 2.0);
}
2

2 Answers

2
votes

The intent behind your approach is to only return a modified copy under certain conditions if I understand it right. In Rust this can be modeled with a function that returns an Option<~LargeStruct> type (perhaps even with Option<LargeStruct> but I'm not sure if the compiler can efficiently move large objects in this case).

fn foo(object: &LargeStruct, y: f32) -> Option<~LargeStruct> {
    if object.x < y {
        return Some(~LargeStruct {
            x: y,
            //n: object.n,
            // ...
        })
    }
    None
}

As for why your approach didn't work: Rust doesn't let you return a reference to an object that will be freed once the function returns. A lifetime is a way to say that an object must live at least as long as the references to it.

2
votes

The answer to the question as asked

It is not possible to design something in this way with it completely transparent; a reference must always have a lifetime not in excess of the scope of its owner; it is thus impossible to return a reference to an object whose scope does not exceed the function.

This can be solved in a way with an enum specifying the return type as either a reference to a LargeStruct or a LargeStruct, but it's clumsy:

pub struct LargeStruct {
    a: int,
}

pub enum LargeStructOrRef<'a> {
    LargeStructRef(&'a LargeStruct),
    LargeStruct(LargeStruct),
}

fn foo<'a>(object: &'a LargeStruct, y: f32) -> LargeStructOrRef<'a> {
    if object.x < y {
        LargeStruct(LargeStruct {
            x: y,
            n: object.n,
            // ...
        })
    } else {
        LargeStructRef(object)
    }
}

You'd then need to do pattern matching between LargeStruct and LargeStructRef—it can't be made transparent to the caller.

Alternative designs

You can probably design this in a different way which will resolve these difficulties. For example, you might make foo take a &mut LargeStruct and modify the struct rather than creating a new one if object.x < y. (This is most likely to be the design you actually want, I think.)