Hello
I might have a problem that wasn’t mentioned here (if I’ve overlooked it, then I’m sorry) which looks more severe than how visible it is.
Now, when reading the code, I can locally decide if each bit of code moves or makes a clone. This is moving:
let a = b;
This is making a clone:
let a = b.clone();
This distinction doesn’t hold true for Copy types. But that’s fine, because for a type to be copy, it must:
- Have no custom code inside its cloning
- Have no destructor
So while I can’t decide if I’ve moved or copied, that doesn’t matter because there’s no way to observe the difference between the two. So I can actually think I’ve done both and be happy about that.
This however is not true with Rc that can copy itself implicitly. There’s observable difference between moving and copying Rc. Let’s say I have this code:
let a = Rc::new(42);
let b = a;
println!("{}", Rc::ref_count(&b));
Right now, this code is legal and prints 1 ‒ because I’ve made a move. So this code still must print 1 after introducing the change. Therefore, let b = a; is a move.
Except that the implicit copying would very much like this code to compile:
let a = Rc::new(42);
let b = a;
println!("{}", Rc::ref_count(&b));
// One million lines of code goes here
println!("{}", a);
But for that to compile, the let b = a; must have been a copy. But then, the first println must have printed 2. So, to decide if the first line prints 1 or 2, I have to read to the very end of the function and then I know what operation it has done at the beginning. While the compiler is probably capable of tracking this, I very much have problems with travelling in time while reading the code.
To add a yet bit more scary code:
let a = Rc::new(42);
let b = a;
let x = Rc::try_unwrap(b).unwrap();
// One million lines of code...
#[cfg(feature = "extra_logging")]
debug!("Value is {}", a);
Turning on the extra_logging feature makes it suddenly to start panic in code that is nowhere close to any code the extra_logging feature adds and looks completely harmless. Happy debugging.
And another thing to consider. While Rc's +1 might be cheap, the distinction of cloning and borrowing of Arc is significant. I’ve worked on a C++ codebase that heavily used shared_ptrs. We were passing them by value (eg. making copies). It turned out we were needlessly spending about 5% of our run time in the shared_ptr destructors and by passing them by reference instead that dropped to about 2%. So while the +1 is cheap, there still should be a way for the programmer to explicitly force the compiler to either move or copy depending on his choice. And it would be quite weird for Rc to be able to auto-clone and Arc not.