So why even have such a thing in a language designed for concurrent programming from the ground up?
Arc should be called Rc, and that's it.
Just because a language is designed for concurrent programming, it shouldn't make it impossible to achieve full single-threaded performance, as long as you're not compromising safety.
if you have only 1 thread, you don't atomic, and thus not using atomic reference counts is fine
but if you have more than 1 thread, you can't use a non-atomic refcount, so you can't use Rc but must use Arc.
"but that's such a simple change, just change the decl with a 1 char addition! Pluse, Rust won't let you do bad stuff if you've forgotten to change the type".
I guess I'm just old. Old enough that I've already implemented all the data structures and methods I need in C++, including safely passing around shared_ptr<T>.
So the "diagnostic against misuse" you're concerned about is a necessary part of the compiler anyway.
Indeed, although Rc has this line:
impl<T: ?Sized, A: Allocator> !Send for Rc<T, A> {}
(which means roughly "You can't send this type to another thread")It also has these lines:
// Note that this negative impl isn't strictly necessary for correctness,
// as `Rc` transitively contains a `Cell`, which is itself `!Sync`.The point of C++ is performance. If you don't need performance, why not just use Java or Python, why use Rust?
As a counterpoint, I also don't need to use `Rc` or `Arc` in Rust, and I can get by without reference counting. Why use C++?
It doesn't (see e.g. slice::split_at_mut). It however doesn't allow you to do that and mutate the vector at the same time.
If you don’t care about safety guarantees and abstractions like shared_ptr, you might just as well use C instead of C++.
That I'm using reference counting on the error path of my parser (so there's something wrong with the input and the task is not going to complete) is very unlikely to become a bottleneck.
You may have a point in that I navigated codebases that were plagued with shared ptrs everywhere in the past, and that general style of programming is not going to yield good performance. But you shouldn't deal in absolutes.
If I hired someone to paint my wall and they were saying "I'm not going to use any protection against splatters on the ground because I am that good and don't need training wheels", I would find that behavior very unprofessional and wouldn't want that person anywhere close to my wall.
I'm writing professional software I'll take any help from tooling that is available without compromising other aspects like performance. It also helps that Rust is much more productive than C++ overall.
About the self-own, my teams over the years lauded my low bug rate, be it in C++ or in Rust. I have a knack for correctness, hence why I prefer languages that make strong guarantees about it by construction to languages where I need to remember and regurgitate thousands of rules at every corner
Only if there's a lot of programmers that don't need them.
There's roughly zero of those programmers. And you're not one of them.
It's possible to implement in C++... so it's not "too dangerous" for C++. It's dangerous for people who don't have knowledge of what they're doing in C++; same as in any programming language.
https://stackoverflow.com/a/15140227/1614219
Which summarizes a discussion by the C++ standards committee to reject the C++ version of Rc, and one of the main arguments is the risk of Rc code being accidentally included in threaded code.
I would point out that this code can include: code you wrote years ago that you forgot includes Rc, code in libraries that was modified internally to use Rc and the authors forgot to mention it, code written by colleagues who aren't familiar with the pitfalls, etc. That's why this isn't a trivial problem to solve.
It's too dangerous to parachute off the Eiffel tower. That doesn't mean it's impossible, periodically somebody does it.
https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/htm...
i.e. a single-threaded non-atomic shared_ptr
Rust fans can dislike on the "C++ has no central library system like crates" all they want, but there's not many things you actually need when programming that don't exist for C++, even if you don't like them not coming in a little box that looks like other little boxes.
The fact that it wasn't included in the standard library for this reason is an argument for this. The fact that even `shared_ptr` has thread-safety footguns, one of which made it to the famous C++ talk, “Curiously Recurring C++ Bugs at Facebook”[1], is another. By the way, every single of the bugs from that talk is impossible in safe Rust.
The fearless concurrency is real. It reliably prevents data races, use-after-free, and moving of thread-specific data to another thread. It works across arbitrarily large and complex call graphs, including 3rd party dependencies and dynamic callbacks. Plus immutability is strongly enforced, and global mutable state without synchronization is not allowed.
It doesn't prevent deadlocks, but compared to data corruption heisenbugs, these are pretty easy — attach a debugger and you can see exactly what deadlocked where.
That's the java model again. I don't want fearless concurrency, I want intentionally designed threads.
For me, single-threaded intra-task concurrency using async/await turned out to be safer and also easier to work with than either of the above mentioned concurrency models. Just a single loop with a top level select - everything is sequential and easy to reason about, also no need for any synchronization like locks or shared atomic pointers.
First, there is criticism that assigning to a shared_ptr is not synchronized so it would be bad to share a single shared_ptr object between threads. True, but that is no different than literally every other non-atomic object in C++. It's not surprising in any way.
Second, there is criticism that assigning to the object pointed at by the shared_ptr is not synchronized between threads. This is odd because that's not actually different than a single thread where there are two shared_ptrs pointing to the same object. That is, even with single threading you have a problem you must be careful about.
So, is he wrong about the Rust part?
Unsynchronized access to the pointed to object will typically cause a specific kind of race condition called a data race, which is undefined behaviour. As it requires threads, it cannot happen in a single-threaded context.
This is to support an atomic lock-free shared_ptr. You can then use this as a building block for building lock-free data structures.
The target of this optimization is low-latency code. Rendezvous will not work for that.
> With GCC when your program doesn't use multiple threads shared_ptr doesn't use atomic ops for the refcount. This is done by updating the reference counts via wrapper functions that detect whether the program is multithreaded (on GNU/Linux this is done by checking a special variable in Glibc that says if the program is single-threaded[1]) and dispatch to atomic or non-atomic operations accordingly.
> I realised many years ago that because GCC's shared_ptr<T> is implemented in terms of a __shared_ptr<T, _LockPolicy> base class, it's possible to use the base class with the single-threaded locking policy even in multithreaded code, by explicitly using __shared_ptr<T, __gnu_cxx::_S_single>. You can use an alias template like this to define a shared pointer type that is not thread-safe, but is slightly faster[2]:
You can definitely implement a non-atomic non-threadsafe shared pointer in C++, my point in the article is that actually using it is very error prone. This is supported by the type being excluded from the standard library with one of the reasons being the risk of bugs.
[1]: https://www.boost.org/doc/libs/1_65_0/libs/smart_ptr/doc/htm...
It is very hard to understand which thread will call the destructor (which is by definition a non-thread-safe operation), and whether a lambda is currently holding a reference to the object, or its members. Different runs result different threads calling the destructor, which is very painful to predict and debug.
I think that rust suffers from the same issue, but maybe it is less relevant as it is a lot harder to cause thread safety issues there.
yes, but at this point, since the reference count is reaching 0, there is supposed to be only that one thread accessing the object being destroyed, so the destruction not being thread-safe should not be a problem.
If otherwise, it means there was a prior memory error where a reference to the pointed-to object escaped the shared_ptr. From there the code is busted anyway. By the way it cannot happen in Rust.
> Different runs result different threads calling the destructor
What adverse effects can happen there? I can think of performance impact, if a busy thread terminates the object, or if there is a pattern of always offloading termination to the same thread (or both of these situations happening at once). I can think of potential deadlocks, if a thread holding a lock must take the same lock to destroy the object (unlikely to happen in Rust where the Arc object would typically contain the object wrapped in its mutex and the mutex wouldn't be reused for locking other parts of the code). There isn't much else I can think of, what do you have in mind?
> whether a lambda is currently holding a reference to the object, or its members
This cannot happen in Rust. If a lambda is holding a reference to the object, then it either has (a clone of) the Arc, or is a scoped lambda to a borrow of an Arc.
And if you only have a small number of refcounted references in your program, the small performance difference between atomic and non-atomic refcounting doesn't matter either.
Same problem with Box and unique_ptr btw, a handful is ok, but once that number grows into the thousands all over the codebase it's hard to do any meaningful optimization (or even figure out how much performance you're actually losing to cache misses because it's a death-by-a-thousand-cuts scenario).