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_ptr
s. 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.