Recently I’ve been reading Effective Modern C++ by Scott Meyers. It’s a great book that contains tons of practical advice, as well as horror stories to astound your friends and confuse your enemies. Since Rust shares many core ideas with modern C++, I thought I’d describe how some of the C++ advice translates to Rust, or doesn’t.
This is not a general-purpose Rust / C++ comparison. Honestly, it might not make a lot of sense if you haven’t read the book I’m referencing. There are a number of C++ features missing in Rust, for example integer template arguments and advanced template metaprogramming. I’ll say no more about those because they aren’t new to modern C++.
I may have a clear bias here because I think Rust is a better language for most new development. However, I massively respect the effort the C++ designers have put into modernizing the language, and I think it’s still the best choice for many tasks.
There’s a common theme that I’ll avoid repeating: most of the C++ pitfalls that result in undefined behavior will produce compiler or occasionally runtime errors in Rust.
Chapters 1 & 2: Deducing Types / auto
This is what Rust and many other languages call “type inference”. C++ has always had it for calls to function templates, but it became much more powerful in C++11 with the auto
keyword.
Rust’s type inference seems to be a lot simpler. I think the biggest reason is that Rust treats references as just another type, rather than the weird quasi-transparent things that they are in C++. Also, Rust doesn’t require the auto
keyword — whenever you want type inference, you just don’t write the type. Rust also lacks std::initializer_list
, which simplifies the rules further.
The main disadvantage in Rust is that there’s no support to infer return types for fn
functions, only for lambdas. Mostly I think it’s good style to write out those types anyway; GHC Haskell warns when you don’t. But it does mean that returning a closure without boxing is impossible, and returning a complex iterator chain without boxing is extremely painful. Rust is starting to improve the situation with -> impl Trait
.
Rust lacks decltype
and this is certainly a limitation. Some of the uses of decltype
are covered by trait associated types. For example,
template<typename Container, typename Index>
auto get(Container& c, Index i)
-> decltype(c[i])
{ … }
becomes
fn get(c: &Container, i: Index) -> &Output
where Container: ops::Index
{ … }
The advice to see inferred types by intentionally producing a type error applies equally well in Rust.
Chapter 3: Moving to Modern C++
Initializing values in Rust is much simpler. Constructors are just static methods named by convention, and they take arguments in the ordinary way. For good or for ill, there’s no std::initializer_list
.
nullptr
is not an issue in Rust. &T
and &mut T
can’t be null, and you can make null raw pointers with ptr::null()
or ptr::null_mut()
. There are no implicit conversions between pointers and integral types.
Regarding aliases vs. typedefs, Rust also supports two syntaxes:
use foo::Bar as Baz;
type Baz = foo::Bar;
type
is a lot more common, and it supports type parameters.
Rust enums
are always strongly typed. They are scoped unless you explicitly use MyEnum::*;
. A C-like enum (one with no data fields) can be cast to an integral type.
f() = delete;
has no equivalent in Rust, because Rust doesn’t implicitly define functions for you in the first place.
Similar to the C++ override
keyword, Rust requires a default
keyword to enable trait specialization. Unlike in C++, it’s mandatory.
As in C++, Rust methods can be declared to take self
either by reference or by move. Unlike in C++, you can’t easily overload the same method to allow either.
Rust supports const iterators smoothly. It’s up to the iterator whether it yields T
, &T
, or &mut T
(or even something else entirely).
The IntoIterator
trait takes the place of functions like std::begin
that produce an iterator from any collection.
Rust has no equivalent to noexcept
. Any function can panic, unless panics are disabled globally. This is pretty unfortunate when writing unsafe
code to implement data types that have to be exception-safe. However, recoverable errors in Rust use Result
, which is part of the function’s type.
Rust supports a limited form of compile-time evaluation, but it’s not yet nearly as powerful as C++14 constexpr
. This is set to improve with the introduction of miri.
In Rust you mostly don’t have to worry about “making const
member functions thread safe”. If something is shared between threads, the compiler will ensure it’s free of thread-related undefined behavior. (This to me is one of the coolest features of Rust!) However, you might run into higher-level issues such as deadlocks that Rust’s type system can’t prevent.
There are no special member functions in Rust, e.g. copy constructors. If you want your type to be Clone
or Copy
, you have to opt-in with a derive
or a manual impl
.
Chapter 4: Smart Pointers
Smart pointers are very important in Rust, as in modern C++. Much of the advice in this chapter applies directly to Rust.
std::unique_ptr
corresponds directly to Rust’s Box
type. However, Box
doesn’t support custom deallocation code. If you need that, you have to either make it part of impl Drop
on the underlying type, or write your own smart pointer. Box
also does not support custom allocators.
std::shared_ptr
corresponds to Rust’s Arc
type.