Implicit clone()

I have a struct that is not Copy, but the value never changes once an instance is created. My actual example is a bit more complicated, but my problem is illustrated by

pub struct Foo { s: String }

As I pass instances around, I find lots of places where I need to explicitly clone them. It's only 7 extra characters, but I find it ugly. (Worse, I can never remember until the compiler reminds me, but that's my problem.) I've tried using references, but then I need a lot of lifetime specifications, which are even uglier. I can declare the struct to have a static lifetime, but I'm worried about that becoming a memory leak.

When comparing two instances, I only care if their values are the same, not whether they refer to the same instance. In other words, I don't care if an instance has an identity any more than I would want the integer 42 to have one. (I've heard the term self-less for such things, as opposed to self-ish for things where you care about instance identity.)

If I had a way to tell the compiler about these restrictions, I wonder if it could implicitly clone them so I don't have to.

Would Cow do the trick?

I used to feel similarly, but over time I just grew numb to seeing clone. Or rather, I see it, but don't have much issue with deciding where it needs to be called, because these days I'm always thinking about ownership, anyways.

I suspect you will have a difficult time convincing people of the merits of having implicit clones. I for one tend to feel that the value added by the lack of implicit clones in the places where it does matter is worth the pain and trouble it causes in code where the distinction does not particularly matter. So my advice is, quite honestly, to learn to live with it. (sorry!)

Sometimes, I find that things are nicer when most values available to me are references. Why? Because this removes one factor you need to think about when changing code, which is "is this usage the last use?":

Compare:

#[derive(Clone)] struct S{};
#[derive(Clone)] struct Esses(S, S);

fn takes_ref(s: &S);
fn takes_own(s: S);

// --------------------
// The world of mostly owned values

fn do_with_own(s: S) {
    // some code will look like this
    takes_ref(&s);
    takes_ref(&s);

    // most code will look like this
    // watch out for that last use when refactoring!
    takes_own(s.clone());
    takes_own(s.clone());
    let esses = Esses(s.clone(), s);
}

// --------------------
// The world of mostly references

fn do_with_ref(s: &S) -> Esses {
    // this is what most code will be like
    takes_ref(s);
    takes_ref(s);

    // Some places will look like this.
    // Object construction in particular will require cloning.
    // But you need no longer worry about the last use of `s`,
    // since every such use requires a clone anyways.
    takes_own(s.clone());
    takes_own(s.clone());

    // That said, objects returned by functions will often unavoidably be owned.
    // Can't win 'em all.
    let esses = Esses(s.clone(), s.clone());
    do_with_refs(&esses); // esses is owned
}

fn do_with_refs(esses: &Esses) {
    // When I need to access data members, I frequently begin a method
    // by doing this, creating a lot of references (as opposed to using esses.0)
    let &Esses(ref a, ref b) = esses;

    // use a and b, which are &S
}

'static itself brings you no closer to a memory leak. The same conditions for creating one remain: you would have to ::std::mem::forget something or create an Rc cycle. That said, it seems doubtful to me that using 'static will bring you much closer to your goal, because you will likely have great difficulty creating data that is both Copy + 'static at runtime.

I, too, have learned to pass references around as much as possible. That way I only clone things when I really have to. The key factor about instances of these structs is that they have some special properties that the compiler could take advantage of if I only had a way of telling the compiler.

This is probably going to be a bad idea. You're executing a potentially expensive operation happen without anything explicitly indicating that a clone() is happening. This implicit behaviour would be like the default C++ copy constructor, and adding an implicit clone would, in my opinion, be a big step backwards for Rust (move semantics are awesome).

If it never changes could you put it in an Arc/Rc? You still end up calling clone(), but at least it's only copying a pointer around instead of the entire struct and its body.

3 Likes

Maybe you need a string interner to make them cheaply copyable?

2 Likes

It's not the performance of cloning, and it's not really the ugliness of seeing .clone() all over the place, even though that was my rationalization. The real issue for me is the extra builds I have to do because I can never remember to explicitly clone when I know very well that I have to :frowning_face:

My more serious point is that there are optimizations the compiler could use for things with special properties, but there's no way to let the compiler know. Other languages have pragmas, but maybe Rust's derive can fill the bill.

Have you tried using an IDE with RLS integration? Hopefully it is responsive enough to support a reasonable workflow? In any case, this is certainly the kind of issue that such tools are meant to help with.

(I've been switching back and forth between IntelliJ (which does cargo check on save) and VSCode (which has RLS integration). Both have been responsive enough for me to feel comfortable working on my latest project; but my codebase is heavily split up into microcrates so I don't know how well it scales)

I'm using IntelliJ for editing, because it highlights syntax errors as I type. Unfortunately, it doesn't do ownership checking until build. I'm using VSCode for debugging, because breakpoints are not available for IntelliJ (yet, he said hopefully). I could just use VSCode, but I can't get VSCode to highlight syntax errors. It's probably due to the "could not start client Rust Language Server" error I get on startup, but I haven't found a fix.

There is an option to run cargo check in the background. In CLion it is hiding under Preferences > Languages and Frameworks > Rust

(it isn't indexed by the search glass for some reason...)

1 Like

Amazing. It takes a few seconds, but it works. Thanks. You just reduced the number of builds I'll end up doing by a factor of 2.:smiley: In my version of IntelliJ it's called "Use cargo check to analyze code."

Automatic deep copies can have surprising effects: Redirecting to Google Groups

7 Likes