[Idea] Improving the ergonomics when using Rc

Hello everyone! When using Rc, I found myself often required to call clone() explicitly and this hurts ergonomics a lot.

use std::rc::Rc;

pub trait Widget {}

#[derive(Clone, Copy)]
pub struct Foo(u32);
impl Widget for Foo {}

#[derive(Clone, Copy)]
pub struct Bar(f32);
impl Widget for Bar {}

pub struct Row {
    children: Vec<Rc<Widget>>,
}

fn main() {
    let foo = Rc::new(Foo(1));
    let bar = Rc::new(Bar(2.5));
    let row1 = Row {
        children: vec![foo.clone(), bar.clone()],
    };
    let row2 = Row {
        children: vec![bar, foo],
    };

    fn take_copyable(_v: Vec<Foo>) {}
    let f = Foo(0);
    // no need to call clone if T: Copy
    take_copyable(vec![f, f, f, f]);
}

Most of the cases explicitly calling clone is preferred to indicate there are something expensive operations. However, some type’s clone is lightweight and ergonomics is preferred. Therefore, similar to primitive type implement Copy, should we introduce ImplicitClone trait for ergonomics when using some type, e.g. Rc?

trait ImplicitClone: Clone {
    fn implicit_clone(&self) -> Self;
}

impl<T> ImplicitClone for Rc<T> {
    fn implicit_clone(&self) -> Self {
        self.clone()
    }
}

fn main() {
    let foo = Rc::new(Foo(1));
    let bar = Rc::new(Bar(2.5));
    let row1 = Row {
        // Rc<Foo>, Rc<Boo> implement ImplicitClone, no need to call clone explicitly
        children: vec![foo, bar],
    };
    let row2 = Row {
        children: vec![bar, foo],
    };
}
2 Likes

Personally, I find it more ergonomic to be able to tell where a reference count operation is happening. (Reading convenience >>> writing convenience.)

So, no, I don't think that the cloning of Rc should be implicit. The "doesn't involve runtime operations other than plain memcpy" property of Copy is just good enough for drawing the line between implicit and explicit.

10 Likes

Understand making Rc implement ImplicitClone may has concerns. The point I want to make is should compiler offer a tool for implicit cloning. Currently it is not possible.

1 Like

It is currently possible, using the Copy trait, which I argue is the right thing to do, because it’s trivial, therefore there’s no loss of information if it’s implicit. Any other cloning that is non-trivial should be explicit.

1 Like

Can we implement Copy for Rc<SomeTrait> now?

No, as far as I know Rc can’t be Copy because its cloning involves more than copying the underlying memory.

I think introduce ImplicitClone would help the ergonomics when using some types especially their clone is consider light weight. If compiler provide such tool, the choice is up to its users.

And Copy is trivial in terms of operations (a memcpy) but not necessary light weight, if a large type compose of all copyable fields.

Due to caching, whether that copy is "lightweight" or not depends to a great extent on how soon after copying the copied data is accessed. On all but the simplest iOT processors, memory/cache access statistics of modern computers often dominate computational considerations.

1 Like

This is why Copy is not derived automatically on PODs. The rule here is already that you should not opt in if a memcpy is expensive, but only derive Clone instead.

2 Likes

"ergonomics" is not about how little code you have to type, it's also about reading and understanding the code afterward. Explicit .clone() is a major advantage of Rust over many other languages; it's easy to tell when you're paying a cost.

16 Likes

Moving Rcs is important for avoiding reference count operations; if it was implicitly cloned [edit: on every use] like Copy does, that would become impossible.

11 Likes
  • @josh Rc::clone which do +1 to its counter can consider trivial and lightweight enough. Require explicitly calling .clone() actually make identify “real” expensive cloning difficult. Which in fact also hurts “reading ergonomics”

  • @skade To prevent abuse using ImplicitClone, Jut like Copy which only implemented for primitive type and opt in derive Copy explicitly on PODs. we can make ImplicitClone implemented for primitive smart pointer types and require opt-in derive explicitly on PODs also.

  • If implement ImplicitClone on existing smart pointer types (e.g. Rc) is a concern. We can introduce implicit version of it, e.g. std::rc::implicit::Rc which is wrap Rc and implement ImplicitClone.

  • @scottmcm To avoid reference count operation when moving Rc with implicit clone is possible but require some work on compiler. If not possible, will doing one more reference count cause big problem?

// compiler needs to know only two implicit clone is required.
let v = vec![rc.clone(), rc.clone(), rc]
// not use rc afterwards
3 Likes

It's common advice and practice in C++ -- where things do copy implicitly -- to std::move one's shared_ptr<T>s or take shared_ptr<T> const & if one might not need the copy to avoid the extra cost. Is it a huge cost? Probably not. But empirically it's something that people are concerned about. See for example the answers in

(I've personally had to give this code review feedback in C++, so appreciate the "pit of success" Rust offers with move-by-default.)

Edit: two additional references:

4 Likes

This would be even more confusing, because you would see Rc's and not even know if they were implicitly cloned or not.

I also don't see any guidelines about what types should not be ImplicitClone if it exists, which seems to imply that pretty much everything would end up with an ImplicitClone impl, so long as someone didn't want to type .clone().

4 Likes

std::shared_ptr is like Arc in rust which require atomic reference count operation. We should be care which primitive smart pointer implement ImplicitClone. And I think the compiler is smart enough to avoid unnecessary cloning call.

1 Like

When use normal Rc, there will be compiler error if forget to call clone. If we call clone explicitly then there should have no difference for both. My personal preference is we implement ImplicitClone on existing Rc.

We can make ImplicitClone like Copy, which type implementing it is controlled in std. Other user type can only derive ImplicitClone if all fields are implement ImplicitClone.

“trivial” here means "involves zero code other than a register/memory copy.

Note that Copy intentionally provides no means of running user code for the copy.

That also ensures that generic code that doesn’t call clone isn’t surprised.

Incrementing a reference count is precisely the sort of thing that requires an explicit operation.

5 Likes

What you are arguing for here is that humans don't need to understand the code because the compiler will understand it for them. That's not a strong foundation for ergonomics discussion.

2 Likes

I don't know why implicit memory copy in Copy is fine, but counter increment is not. To me both are some kind of memory operation and mostly lightweight. I also won't be surprised if the compiler help me increase reference counter of Rc object implicitly when it required to do so.

When using Rc most user will know there need to call clone, and the compiler knows too. It is not just saving few keystrokes to type .clone(), if there are too many lightweight Rc::clone all over the place. It make spot the true heavy cloning difficult. Therefore, why don't make Rc implement ImplicitClone?

2 Likes

I think an important point here is that there shouldn't be too many Rc::clones. After all, most of the times you'll just pass around a &T deref'd from the Rc<T>.

8 Likes