C++ has three ways to pass parameters to a function: by value, by lvalue reference, and by rvalue reference. Of these, passing by value creates ownership in the sense that the called function receives its own copy, and passing by rvalue reference indicates that the value may be consumed, i.e. will no longer be used by the caller. Passing by lvalue reference means that the object is temporarily borrowed from the caller.
However, these tend to be “by convention” and cannot always be checked by the compiler. And you can accidentally turn a lvalue reference into an rvalue reference using
std::move(). Concretely, there are three problems:
A reference can outlive the object it references. Rust's lifetime system prevents this.
There can be more than one mutable/non-const reference active at any time. Rust's borrow checker prevents this.
You cannot opt out of references. You cannot see at a call site whether that function creates a reference to your object, without knowing the signature of the called function. Therefore you cannot reliably prevent references, neither by deleting any special methods of your classes nor by auditing the call site for compliance with some “no references” style guide.
The lifetime problem is about basic memory safety. It is of course illegal to use a reference when the referenced object has expired. But it is very easy to forget about the lifetime when you store a reference within an object, in particular when that object outlives the current scope. The C++ type system cannot account for this because it doesn't model object lifetimes at all.
std::weak_ptr smart pointer does encode ownership semantics similar to a plain reference, but requires that the referenced object is managed via a
shared_ptr, i.e. is reference-counted. This is not a zero-cost abstraction.
While C++ has a const system, this doesn't track whether an object can be modified, but tracks whether an object can be modified through that particular reference. That does not provide sufficient guarantees for “fearless concurrency”. In contrast, Rust guarantees that if there is an active mutable reference that is the only reference (“I am the only one who can change this object”) and if there are non-mutable references then all references to the object are non-mutable (“while I can read from the object, no one can change it”).
In C++ you might be tempted to guard access to an object through a smart pointer with a mutex. But as discussed above once we have a reference it can escape its expected lifetime. Therefore such a smart pointer cannot guarantee that it is the single point of access to its managed object. Such a scheme may actually work in practice because most programmers don't want to sabotage themselves, but from a type-system view point this is still completely unsound.
The general problem with smart pointers is that they are libraries on top of the core language. The set of core language features enables these smart pointers, e.g.
std::unique_ptr needs move-constructors. But they cannot fix deficiencies within the core language. The abilities to implicitly create references when calling a function and to have dangling references together mean that the core C++ language is unsound. The inability to limit mutable references to a single one means that C++ cannot guarantee safety against race conditions with any kind of concurrency.
Of course in many respects C++ and Rust are more alike than they are disalike, in particular regarding their concepts of statically determined object lifetimes. But while it is possible to write correct C++ programs (provided none of the programmers makes any mistakes), Rust guarantees correctness regarding the discussed properties.