From C++ to Rust
I recently started learning Rust. As a long-time C++ programmer, I wanted to understand how C++ concepts worked in Rust. This is a comparison of various concepts and implementation details of C++ and Rust.
I rate each aspect of the language as:
Indicator | Concept | Implementation |
---|---|---|
✅ | Highly Similar | Minor Differences |
❓ | Somewhat Similar | Substantial Differences |
❌ | Vaguely Similar | Major Differences |
This is not an exhaustive comparison.
- Variables
- Procedural Programming
- Object Orient Programming
- Overriding and Overloading
- Meta-Programming: Generics, Templates, and Macros
- The End
Variables
❌ Naming Conventions
Rust specifies snake_case
for variable and function names. It specifies UpperCamelCase
for custom types and UPPER_CASE
for constant values. The Rust compiler issues warnings if the case conventions are not followed. While most projects follow similar conventions, C++ does not specify case conventions.
✅ Primitive Types
C++ | Rust | Size (C++/Rust) |
---|---|---|
size_t |
isize |
cpu specific |
ssize_t |
usize |
cpu specific |
uint8_t |
u8 |
8-bit |
int8_t |
i8 |
8-bit |
uint16_t |
u16 |
16-bit |
int16_t |
i16 |
16-bit |
uint32_t |
u32 |
32-bit |
int32_t |
i32 |
32-bit |
uint64_t |
u64 |
64-bit |
int64_t |
i64 |
64-bit |
uint128_t |
u128 |
128-bit |
int128_t |
i128 |
128-bit |
const char * |
&str |
varies |
float |
f32 |
32-bit |
double |
f64 |
64-bit |
bool |
bool |
varies/1 Byte |
char |
char |
8-bit/32-bit |
✅ Literals
C++ | Rust | Type |
---|---|---|
5'000 |
5_000 |
Decimal |
0xff |
0xff |
Hexadecimal |
O777 |
0o777 |
Octal |
0b1111'0000 |
0b1111_0000 |
Binary |
'A' |
'A' |
Byte |
1.0f |
1.0_f32 |
single-precision floating point |
1.0 |
1.0 |
double-precision floating point |
With Rust you can suffix a literal with an underscore-primitive type:
//rust
let x = 5_u64;
❌ User-Defined Literals
//c++
auto operator"" _milliseconds(unsigned long long int value) -> long long int {
return value;
}
const auto value = 5_milliseconds;
In Rust, you can have user-defined literals only within macros. Doing so is non-trivial but the call site could look something like this:
//rust
//can do this
let duration = chrono!(5milliseconds);
//can't do this
let other_duration = 5milliseconds;
❓ Compound Data Types
❌ Enums
//c++
enum class Color {
red, green, blue
};
//rust
enum Color {
Red, Green, Blue
}
An enum
in Rust can also be used similarly to std::variant
.
using Option = std::variant<bool, int>;
const auto option = Option{5};
const auto nothing = Option{false};
//rust
enum Option {
None,
Some(i32)
}
//usage
let option = Option::Some(5);
let nothing = Option::None;
Rust
std::Option
is used like C++’sstd::optional
. Some aspects of Ruststd::Option
are built into the language to streamline error handling using the?
operator.
❓ Arrays
//c++
char x[5]; //may be uninitialized
//access
const auto z = x[0] + x[4]; //first and last
const auto ub = x[5]; //undefined behavior
//rust
let x: [i32; 5] = [1,2,3,4,5]; //must be initialized before 1st use
let y = [3; 5]; //y is [3,3,3,3,3];
//access
let z = x[0] + x[4];
let ub = x[5]; //won't compile
//if a dynamic out-of-range value is used, rust will panic at runtime
❓ Tuples and Structs
In Rust, tuples are a native type. In C++, they are part of std::tuple
.
//c++
auto get_tuple() -> std::tuple<int, int>{
return std::tuple<int, int>{1, -1};
}
//rust
fn get_tuple() -> (i32, i32) {
(1, -1)
}
✅ Constness and Mutability
C++ variables are mutable by default while Rust variables are constant by default.
//c++
const int x = 5;
int y = 10;
//rust
let x: i32 = 5;
let mut y: i32 = 10;
In Rust, variables are dropped after the last use. This means a variable can be re-declared in the same scope.
//rust
//this compiles and is valid rust
let x = 10;
let mut x = x; //x is now mutable
✅ auto
vs. inference
The auto
keyword in C++ was introduced to enable backward compatible type inference. Rust comes with built-in type inference.
//c++
const int w = 5;
const auto x = 5;
int y = 10;
auto z = 10;
//rust
let w: i32 = 5;
let x = 5;
let mut y: i32 = 10;
let mut z = 10;
✅ Casting
C++ includes many implicit type-casts. In Rust, type-casting is always explicit.
//c++
const double x = 1; //implicit conversion from int to double
const auto y = static_cast<double>(1); //C++11
There are many ways to cast in C++. I can’t cover them all here.
//rust
let x: f64 = 5; //won't compile - no implicit conversion
let y = 5 as f64; //cast using as
❌ Copy and Move Semantics
Copy and move semantics between C++ and Rust concerning are very different. To summarize:
- C++ copies variables by default. If a
class
/struct
has implemented move semantics, the value is sometimes moved. - Rust moves (transfers ownership) by default. Custom types can implement
Copy
(implicit) andClone
(explicit) copy semantics.
Rust primitive types implement Copy
and Clone
such that primitives are copied by default (just like in C++).
//c++
const auto x = 5;
const auto y = x; //y holds a copy of 5
printf("x%d\n", x);
printf("y%d\n", y);
let x = 5;
let y = x; //y holds a copy of 5
//for these we assume `impl fmt::Display for X`
println!("x{}", x);
println!("y{}", y);
See Also References vs Borrowing.
❌ Copying and Moving Compound Types
For non-primitives, C++’s default mode is to copy variables whereas Rust’s default is to move (or transfer ownership). For example:
//c++
struct X {
int value;
};
auto x = X{5};
auto y = std::move(x); //y holds a copy of x (default move is to copy)
printf("x%d\n", x); //C++ specifies a valid but unspecified state for x
printf("y%d\n", y);
struct X(i32);
let x = X(5);
let y = x; //y now owns '5' and x is now invalid
//for these we assume `impl fmt::Display for X`
println!("x{}", x); // This is a compiler error because x is moved
println!("y{}", y);
In Rust, you can override the default behavior by implementing Clone
or Copy
for a custom type. The mechanics are similar to custom move/copy assign/construct operators in C++.
struct X {
int value;
X(const X&a){
//magic here
}
X& operator=(const X&a){
//magic here
return *this;
}
X(const X&&a){
//magic here
}
X& operator=(const X&&a){
//magic here
return *this;
}
};
struct X(i32);
impl Copy for X {
fn copy(self: &Self) -> Self {
//magic here
}
}
self: &Self
can be written:
&self
: readingmut self
: mutatingself
: consuming
Or you can use the default Copy
:
#[derive(Copy)]
struct X(i32);
let x = X(5);
let y = x; //y gets a copy of X
//for these we assume `impl fmt::Display for X`
println!("x{}", x); // This compiles now
println!("y{}", y);
❌ References vs Borrowing
C++ references and Rust borrows are very different but use similar syntax.
//c++
auto x = 5;
auto & y = x; //y refers to x
In C++, y
is a pointer to x
. If x
changes, y
will reflect that change. If x
goes out of scope, y
will be a dangling reference (undefined behavior).
//rust
let mut x = 5;
let y = &x; //y borrows x's value
x = 10; //won't compile, because x is "borrowed"
println!("y{}", y);
x = 20; //will compile, borrow ends after last use
In Rust, &
is a borrow. The borrower takes ownership of the underlying data and limits what the original variable can do.
The above is a simplification of Rust ownership. Read more in the Rust book
Procedural Programming
Most procedural programming concepts in Rust are similar to C++. A few concepts differ substantially such as Rust’s match
keyword.
❓ Flow Control
❓ If/Else
//c++
auto is_true = true;
if( is_true ){
//do this
} else {
//do that
}
//rust
let is_true = true;
if is_true { //parentheses are optional but discouraged
//do this
} else {
//do that
}
else if
works the same way in C++ and Rust.
✅ Ternary Operator
//c++
const auto x = true;
const auto y = x ? 5 : 10;
//rust
let x = true;
let y = if x {5} else {10};
In Rust, if
is an expression that evaluates to a value. Notice there are no ;
’s in the branches. Both branches must resolve to the same type (just like C++ ternary).
❌ switch vs match
C++ switch
and Rust match
are similar conceptually but have distinct implementations.
//c++
auto x = 5;
auto y = 0;
switch(x){
case 0:
y = 0;
break;
case 1:
y = 1;
break;
default:
y = 2;
break;
}
//rust
let x = 5;
let mut y = 0;
match x {
0 => y = 0;
1 => y = 1;
_ => y = 2; //_ is a catch-all value
};
In Rust, the match
statement can pattern match enums in a way that is unparalleled in C++.
let x = Some(5); //is a std::Option
match x {
Some(value) => println!("Has value {}", value),
None => println!("doesn't have a value")
};
❓ Loops
❓ For Loops
//c++
for(int i=0; i < 10; ++i){
//do something (10x)
}
const auto list = {0,1,2,3,4};
for(const auto & value: list){
//do something with each `value`
}
//list is still valid
//rust
for i in 0..10 { //use _ if i is not used
//do something (10x)
}
let list = [0,1,2,3,4];
for value in &list { //borrow list
//do something with each `value`
}
//borrow is returned
for value in list { //consume list
//do something with each `value`
}
//at this point list has been consumed and cannot be used
❓ While Loops
//c++
while(true){
//forever
}
auto dont_stop = true;
while(dont_stop){
dont_stop = false;
}
//rust
//this is a warning in rust - use a `loop`
while true {
//forever
}
let mut dont_stop = true;
while dont_stop == true {
dont_stop = false;
}
❌ Loops
Rust has a loop
keyword that loops forever without an explicit break
s. Rust also supports named break
’s with nested loops.
//rust
loop {
//forever loop
}
✅ Functions
//c++
int function(int x){
return x;
}
//with trailing return type (looks more like Rust)
auto function(int x) -> int {
return x;
}
//callsite
function(5);
//rust
fn function(x: i32) -> i32 {
return x;
}
//rust lines without a semicolon are an implicit return
fn function(x: i32) -> i32 {
x // same as return x;
}
//callsite
function(5);
❌ Default Arguments
//c++
auto do_something(int x = 2) -> int {
return x*2;
}
//callsite
do_something(10); //or
do_something();
Rust does not support default arguments.
There are techniques using
Option
orimpl Default
that achieve similar behavior.
❓ Lambas
C++ lambdas are called closures in Rust.
//c++
const auto x = 5;
//captured variables `&x` are explicit
auto lambda = [&x](int y){
return x + y;
};
//callsite
const auto sum = lamdba(5);
//rust
let x = 5;
//captured values, `x` in this case, are automatically
//borrowed until closure's last use
let closure = |y: i32| -> i32 { x + y };
//callsite
let sum = closure(5);
❓ Immediately called Lambda
In C++, I like to declare and immediately call a lambda to do complex logic in an isolated scope and return a const
value from it.
//c++
const auto list = [&](){
auto result = std::vector<std::string>{};
for(int i=0; i < 10; i++){
result.push_back(std::string{"hello"});
}
return result;
}();
In Rust, a scope is an expression. So there is no need for a closure to do the same thing.
//rust
let list = {
let mut result: Vec<String> = Vec::new();
for _ in 0..10 {
result.push(String::from("hello"));
}
result
};
Object Orient Programming
❌ Classes and Structs
The primary difference between a C++
class
and astruct
is theclass
members areprivate
by default andstruct
members arepublic
. When comparing to Rust, we will just consider the C++struct
.
//c++
struct X {
int width;
int height;
}; //trailing ;
//or a tuple
using T = std::tuple<int, int>;
//rust
struct X {
width: i32,
height: i64, //this trailing comma is optional
} //no trailing ;
//struct can be a tuple
struct T(i32, i64)
❓ Member Functions/Methods
//c++
struct Foo {
int value;
void set_value(int num){
value = num;
}
};
auto foo = Foo{10};
foo.set_value(5);
//rust
struct Foo {
value: i32
}
//implementation is always separate from the data
//with no forward declarations required
impl Foo {
fn set_value(&mut self, num: i32){
self.value = num;
}
}
let mut foo = Foo{value: 10};
foo.set_value(5);
❌ Constructors
C++ gives a ton of flexibility in how to construct objects. Rust has exactly one way to construct an object. However, both languages support factory functions for combining logic with instantiation.
//c++
struct X {
int width; //could assign defaults
int height;
X(int w, int h){ //could do an init-list here
//do some magic
}
//or with a factory function
static X create_x(int w, int h){
//do some magic
return X(w,h);
}
};
//rust
struct X {
width: i32,
height: i64, //this trailing comma is optional
} //no trailing ;
impl X {
fn create(w: i32, h: i64) -> Self {
//do some magic
//this is the ONLY way to officially construct X
return X{width: w, height: h};
}
}
//callsite
let x0 = X{width:10, height:20};
let x1 = X::create(5,10);
❓ Destructors
❓ C++ destructors have a lot more to worry about than Rust because of the way inheritance and copy/move semantics are implemented in C++.
//c++
struct X {
int width; //could assign defaults
int height;
~X(){
//called when the instance goes out of scope
//take care to properly handle copy/move semantics and inheritance
}
};
//rust
struct X {
width: i32,
height: i32
}
impl Drop for X {
fn drop(&self){
//called when the instance is dropped
//only called once per value
//not called on borrow or ownership transfer
}
}
❓ Access Modifiers
//c++
struct Foo {
public: //this is the default for struct
int public_value;
protected:
int protected_value;
private:
int private_value;
}
//rust
struct Foo {
pub public_value: i32,
//no equivalent for protected
private_value: i32
}
In Rust, everything in the same module of the same crate can access private values. In C++, only the members of the same type can access private values.
❌ Inheritance
struct Foo {
int foo;
int get_foo() const {
return foo;
}
};
struct Bar : public Foo {
int bar;
int get_bar() const {
return bar;
}
};
auto bar = Bar{};
bar.get_foo();
bar.get_bar();
Rust uses “Traits” to define interfaces. A struct
can implement multiple traits. “Traits” allow
- constants
- functions
- types
They do not allow data members. To do approximately the above in Rust:
//rust
trait Foo {
fn get_foo() -> i32;
}
trait Bar {
fn get_bar() -> i32;
}
struct FooBar {
foo: i32,
bar: i32
}
impl Foo for FooBar {
fn get_foo(&self) -> i32 {
return self.foo;
}
}
impl Bar for FooBar {
fn get_bar(&self) -> i32 {
return self.bar;
}
}
let foobar = FooBar{foo: 5, bar: 10};
let _foo = foobar.get_foo();
let _bar = foobar.get_bar();
❓ Virtual Functions
//c++
struct Foo {
virtual int pure_vitual() = 0;
virtual int virtual_with_default(){
return 0;
}
}
struct Bar: public Foo {
int pure_virtual() override (){
return 1;
}
int virtual_with_default() override (){
return 1;
}
}
//rust
trait Foo {
fn pure_virtual() -> i32;
fn virtual_with_default() -> i32 {
return 0;
}
}
struct Bar;
impl Foo for Bar {
fn pure_virtual() -> i32 {
return 1;
}
//omit to use Foo's default implementation
fn virtual_with_default() -> i32 {
return 1;
}
}
Overriding and Overloading
C++ uses polymorphism more liberally than Rust.
❌ Function Overloading
//c++
struct Foo {
int bar();
int bar() const; //unique by const
int bar(int value); //unique by parameter
int rbar() &&; //unique by rvalue
int rbar() &; //unique by lvalue
};
//rust
impl Foo {
//functions can't be overloaded (need unique names)
fn bar(){}
fn bar_const(){}
fn bar_int(value: i32){}
}
✅ Operator Overloading
//c++
struct X {
int width; //could assign defaults
int height;
X operator+(const X & rhs) const {
return X{width+rhs.width, height+rhs.height};
}
};
//callsite
const auto x = X{5,5} + X{10,10};
//rust
struct X {
width: i32,
height: i32
}
impl std::ops::Add for X {
fn add(&self, rhs: &X) -> Self {
return X{ width: self.width + rhs.width,
height: self.height + rhs.height};
}
}
//callsite
let x = X{width:5, height:5} + X{width:10, height:10};
❓ Function Overriding
//c++
struct Foo {
void do_something(){
printf("foo something\n");
}
}
struct Bar: public Foo {
//overrides Foo's default implementation
void do_something(){
printf("bar something\n");
}
}
//rust
trait Foo {
fn do_something(&self){
println!("foo something");
}
}
struct Bar;
impl Foo for Bar {
//overrides Foo's default implementation
fn do_something(&self){
println!("bar something");
}
}
Meta-Programming: Generics, Templates, and Macros
❓ Generics
//c++
//generic struct
template<typename Type> struct Foo {
Type member_of_type;
};
struct Bar {
//generic function
template<typename Type> auto do_something(Type a) -> Type {
return a;
}
};
//rust
//generic struct
struct<Type> Foo {
member_of_type: Type
}
//generic function
fn do_something<Type>(Type a) -> Type {
return a;
}
❓ C++20 Concepts vs Rust Bounds
//c++
template <typename Type>
requires std::integral<Type> //ensure T matches %d formatting
auto printer(Type t) -> void {
printf("%d", t);
}
//rust
//T must implement the Display trait
fn printer<Type: Display>(t: Type) {
println!("{}", t);
}
❌ Meta-Programming and Macros
It is hard to draw parallels between meta-programming (code that generates code) in C++ and Rust. Rust macros are used for meta-programming. They are used more like C++ template consteval
/constexpr
meta-programming than C++ preprocessor macros. While C++ pre-processor macros are discouraged, Rust macros are an integral part of the language.
Rust macros use rust code (compiled and executed at compile-time) that parses macro tokens and generate rust code which is then compiled and executed at run-time.
//rust
fn main(){
println!("{} {}", "hello", "world");
}
In rust, the println!()
macro has rust code that parses everything inside the ()
and then generates the rust implementation to achieve the desired operation. The code that parses and generates is also fully-fledged rust code that is compiled and executed at compile time.
The End
After spending a couple of months investigating Rust, I found many things about the language to be delightful compared to C++.
- Better defaults: const vs mutable, implicit vs explicit conversions, copy vs move
- More cohesive ecosystem: Rust has a built-in build system, formatter, language server, documentation generator, unit testing, and package manager
- Simpler, more expressive syntax.
Rust has benefitted from several decades of lessons learned from C++. Herb Sutter recently introduced cppfront as a way to make C++ 10 times simpler and safer. Rust might have beaten him to it.
You can learn more about Rust here:
- Rust Book
- Rust by Example
- Comprehensive Rust in 4 Days (by Google)