-
Notifications
You must be signed in to change notification settings - Fork 0
Rust for CXX programmers
Pointers in C++ are vulnerable to bugs involving dangling or null pointers. In Rust, pointers are non-nullable and the compiler enforces the invariant that pointers are never dangling.
C++ | Rust |
---|---|
& |
&mut |
const& |
&const |
n/a |
& (immutable) |
std::unique_ptr |
owned pointer: ~
|
std::shared_ptr |
managed pointer: @ (with cycles collected) |
Borrowed pointers (&
) have the same representation as C pointers or C++
references at runtime, but to enforce safety at compile-time they are more
restricted and have a concept of lifetimes. See the borrowed pointer tutorial for details.
For example, a container can return a reference to an object inside it, and the lifetime will ensure that the container outlives the reference and is not modified in a way that would make it invalid. This concept also extends to any iterator that wraps borrowed pointers, and makes them impossible to invalidate.
Owned pointers are almost identical to std::unique_ptr
in C++, and point to
memory on a shared heap which allows them to be moved between tasks.
Managed pointers are similar to std::shared_ptr
, but have full garbage
collection semantics and point to a task-local heap. By virtue of being on a
local heap, they can use a per-task garbage collector.
If managed pointers are avoided, garbage collection is not used (the compiler
can enforce this with the managed-heap-memory
lint check). Garbage collection
will be avoided in the standard library except for concepts like persistent
(copy-on-write, with shared substructures) containers that require them.
C++ | Rust |
---|---|
"foo" (const char * ) |
"foo" (&static/str ) |
std::string("foo") |
~"foo" (~str ) |
std::make_shared<std::string>("foo")
(std::shared_ptr<std::string> ) |
@"foo" (@str , no indirection) |
C++ | Rust |
---|---|
std::array<int, 3> {{1, 2, 3}} |
[1, 2, 3] ([int * 3] ) |
std::vector<int> {1, 2, 3} |
~[1, 2, 3] (~[int] ) |
std::make_shared<std::vector<int>>(std::initializer_list<int>{1, 2, 3})
(std::shared_ptr<std::vector<int>> ) |
@[1, 2, 3] (@[int] , no indirection) |
Rust's enum
is a sum type like boost::variant
(tagged union). The match
expression is used to pattern match out of it, replacing the usage of a
switch
with a C++ enum
, or a visitor with boost::variant
. A match
expression is required to cover all possible cases, although a default fallback
can be used.
C++ | Rust |
---|---|
enum |
enum |
boost::variant |
enum , Either (an enum ) for 2 types |
boost::optional |
Option (an enum ) |
Option
and Either
are very common patterns, so they're predefined along
with utility methods.
A Rust struct
is similar to struct
/class
in C++, and uses a memory layout
compatible with C structs. Members are public by default but can be marked with
priv
. There is no concept of a friend
function or type, because priv
applies only at a module level.
Rust has no special syntax or semantics for constructors, and just uses static methods that return the type.
Custom destructors for non-memory resources can be provided by implementing the
Drop
trait.
Rust has no concept of a copy constructor and only shallow types are implicitly
copyable. Assignment or passing by-value will only do an implicit copy or a
move. Other types can implement the Clone
trait, which provides a clone
method.
Rust will implicitly provide the ability to move any type, along with a swap implementation using moves. The compiler enforces the invariant that no value can be read after it was moved from.
Rust does not allow sharing mutable data between threads, so it isn't vulnerable to races for in-memory data. Instead of sharing data, message passing is used instead - either by copying data, or by moving owned pointers between tasks.
There are several scheduling models available, which allows for either mapping tasks N:N with OS threads, or using N:M scheduling like Go and Erlang for lightweight concurrency.
Rust does not include C++-style exceptions that can be caught, only uncatchable
unwinding (fail
) that can be dealt with at task boundaries. The lack of
catch
means that exception safety is much less of an issue, since calling a
method that fails will prevent that object from ever being used again.
Generally, errors are handled by using a enum
of the result type and the
error type, and the result
module implements related functionality.
There is also a condition system, which allows a failure to be handled at the
site of the failure, or otherwise just resorts to fail
if it cannot be
handled.
In addition to the prevention of null or dangling pointers and data races, the Rust compiler also enforces that data be initialized before being read. Variables can still be declared and then initialized later, but there can never be a code path where they could be read first. All possible code paths in a function or expression must also return a correctly typed value, or fail.
This also means indexing a string/vector will do bounds checking like
vector<T>::at()
.
C++ | Rust |
---|---|
size_t foo() const |
fn foo(&self) -> uint |
size_t foo() |
fn foo(&mut self) -> uint |
static size_t foo() |
static fn foo() -> uint |
n/a |
fn foo(self) , fn foo(~self) , fn foo(@self)
|
Methods in Rust are similar to those in C++, but Rust uses explicit self, so
referring to a member/method from within a method is done with self.member
.
It's also possible to take self
by-value, which allows for moving out of
self
since the object will be consumed.
For an example, the PriorityQueue
struct in the standard library has a
to_sorted_vec
method that takes self by-value, making
PriorityQueue::from_vec(xs).to_sorted_vec()
an in-place heap sort.
The ~self
and @self
forms are available for objects allocated as ~
or @
respectively.
Once the transition to explicit self is complete, the static
keyword will no
longer be needed to mark static methods.
In Rust, functions and structs can be given generic type annotations (like templated functions and structs), but to actually call a type's methods and use operators, the type annotations must be explicitly bounded by the traits required. Annotating the type bounds for functions is only necessary at the module-level, and not for nested closures where it is inferred.
Despite the different model in the type system, traits are compiled to code similar to what templates would produce.
Rust includes macros (not textual ones) and syntax extensions which can be used
for many of the other use cases of templates and constexpr
. Syntax extensions
can also be used in place of user-defined literals.