en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

What makes Rust easier than C++

When people talk about learning the Rust programming language they fear its steep learning curve. But let’s do the comparison right: As a systems programming language with many high-level features its most obvious competitor is C++, which is also hard to learn.

While Rust is pedantic in forcing the programmer to write safe code, there are other areas where it is much easier to handle than C++. As a young language Rust brings simplicity and many convenient features.

Special member functions

The C++ special member functions enable custom implementations of basic operations such as creating, destroying, copying, moving and assigning objects. If you are unlucky, you must implement all of them yourself (see the well-known rule of five). This results in a lot of boilerplate code to be written (and hopefully also unit-tested!).

C++ example:

class Foo {
public:
    // Constructor
    Foo(int input) { /* ... */ }
    // Destructor
    ~Foo() { /* ... */ }

    // Copy constructor
    Foo(const Foo& other) { /* ... */ }
    // Move constructor    
    Foo(Foo&& other) noexcept { /* ... */ }
    // Copy assignment operator    
    Foo& operator=(const Foo& rhs) { /* ... */ }
    // Move assignment operator    
    Foo& operator=(Foo&& rhs) noexcept { /* ... */ }

private:
    char* value1;
    int value2;
};

Rust example:

pub struct Foo {
    value1: Box<[u8]>,
    value2: i32
}

impl Foo {
    // Rust equivalent of a constructor
    pub fn new(input: i32) -> Foo { /* ... */ }
}

// Rust equivalent of a destructor
impl Drop for Foo {
    fn drop(&mut self) { /* ... */ }
}

Surprisingly, the Rust language itself doesn’t have any special member functions at all. Copying and moving objects is done just like a «memcpy», without custom behavior.

Usually that’s not a limitation:

As a bonus, you don’t have to deal with C++ language features such as rvalue references and std::move in Rust.

You can also forget about other language quirks like prevention of self-assignment or the copy and swap idiom.

Data classes: Common operators and methods

Some classes are just plain data structures with little abstraction. Users of these classes expect that operators such as equality or other comparison operators are available. So again a lot of boilerplate code must be written and tested.

C++ example:

class Person {
public:
    // ...

    friend bool operator==(const Person& lhs, const Person& rhs) {
        return lhs.first_name == rhs.first_name &&
            lhs.last_name == rhs.last_name;
    }

    friend bool operator<(const Person& lhs, const Person& rhs) {
        if (lhs.first_name < rhs.first_name) {
            return true;
        } else if (rhs.first_name < lhs.first_name) {
            return false;
        }
        if (lhs.last_name < rhs.last_name) {
            return true;
        } else if (rhs.last_name < lhs.last_name) {
            return false;
        }
        return false;
    }

private:
    std::string first_name;
    std::string last_name;
};

Rust example:

#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Person {
    first_name: String,
    last_name: String
}

In Rust the operators above don’t have to be implemented manually. The magic happens in the derive macros that can generate such code for standard use cases (by applying the operator individually on each struct member).

Here a list of a few useful derive macros from the Rust standard library:

Derive macro Result

PartialEq

Eq

Generates code for the operators:

  • a == b
  • a != b

PartialOrd

Ord

Generates code for the operators:

  • a < b
  • a > b
  • a <= b
  • a >= b
Clone Generates code for a deep copy
Debug Generates code to print the object content as text in a debug output
Default Generates code for a «default constructor»
Hash Generates code for calculating a hash from the object content

Virtual and static members

The declaration of a C++ class contains a mix of all kinds of members. There are fields and methods, some of them are static and some methods are virtual. But Rust separates different member categories into distinct code blocks:

This separation results in a clean struct definition that simply shows how an object looks like in memory.

C++ example:

class Printable {
public:
    // Pure virtual method
    virtual void print() = 0;
};

class Person : public Printable {
public:
    // Constructor
    Person(std::string name) { /* ... */ }

    // Instance method
    uint32_t get_age() { /* ... */ }

    // Overridden virtual method
    void print() override { /* ... */ }

    // Static method
    static Person& get_oldest_from(std::span<Person> persons) { /* ... */ }

private:
    // Instance fields
    Date birthday;
    std::string name;
    uint32_t id;

    // Static field
    static uin32_t next_person_id = 0;
};

Rust example:

trait Printable {
    // Rust equivalent of pure virtual methods
    fn print(&self);
}

// Clean struct definition (only instance fields)
pub struct Person {
    birthday: Date<Utc>,
    name: String,
    id: u32,
}

// Static fields are not part of the struct
static mut next_person_id: u32 = 0;

impl Person {
    // Rust equivalent of static methods (without "self" argument)
    pub fn new(name: String) -> Person { /* ... */ }
    pub fn get_oldest_from(persons: &[Person]) -> &Person { /* ... */ }

    // Rust equivalent of instance methods (with "self" argument)
    pub fn get_age(&self) -> u32 { /* ... */ }
}

impl Printable for Person {
    // Rust equivalent of overridden virtual methods
    // (trait implementation)
    fn print(&self) { /* ... */ }
}

 

Tuples and enums

In statically typed languages like C++ and Rust it is essential to compose new types from basic ones. Rust has introduced a lot of syntactic sugar to simplify working with compound types such as tuples or enums (similar to variants in C++).

C++ example:

// Tuple
auto coordinate = std::make_tuple(200, 100);

// Access tuple members (since C++17)
auto [x, y] = coordinate;
std::cout << std::format("x is {}, y is {}", x, y) << std::endl;
// Before C++17:
//     auto x = std::get<0>(coordinate);
//     auto y = std::get<1>(coordinate);

// Variant (supported since C++17)
struct CreateCommand {};
struct MoveCommand { int32_t x; int32_t y; };
struct WriteCommand { std::string text; };
using Command = std::variant<
        CreateCommand,
        MoveCommand,
        WriteCommand>;

// Assign variant value
auto cmd1 = Command(CreateCommand{});
auto cmd2 = Command(WriteCommand{"Hello"});

// Evaluate variant value
struct Visitor {
    void operator()(const CreateCommand&) { }
    void operator()(const MoveCommand& c) {
        std::cout << std::format("Move by {}, {}", c.x, c.y) << std::endl;
    }
    void operator()(const WriteCommand& c) {
        std::cout << std::format("Text: {}", c.text) << std::endl;
    }
};
std::visit(Visitor(), cmd2);

Rust example:

// Tuple
let coordinate = (200, 100);

// Access tuple members
let (x, y) = coordinate;
println!("x is {x}, y is {y}");

// Enum
enum Command {
    Create,
    Move(i32, i32),
    Write(String),
}

// Assign enum value
let cmd1 = Command::Create;
let cmd2 = Command::Write("Hello".to_string());

// Evaluate enum value
match cmd2 {
    Command::Create => {},
    Command::Move(x, y) =>
        println!("Move by {x}, {y}"),
    Command::Write(text) =>
        println!("Text: {}", text),
}

 

Other areas

There are even more areas where the Rust language is more concise and less complicated than C++:

Conclusion

C++ has a long history and must remain compatible with a large existing code base. Therefore, it has many legacy features and inconsistencies, making it complicated. The existence of books such as the «Effective C++» series indicates that a programmer must learn many language quirks and possible workarounds.

Rust on the other hand is young and focuses more on commonly used language features instead of special cases. That’s why it takes much more time for a beginner to become an expert in C++ than in Rust. Nevertheless also advanced programmers will appreciate Rust’s approach of realizing safety features with moderate language complexity.

Kommentare

Eine Antwort zu “What makes Rust easier than C++”

  1. Urs sagt:

    Really nice write-up!

    Small addition: C++20 supports default comparisons too. E.g:

    #include
    struct Point {
    int x;
    int y;
    auto operator(const Point&) const = default;
    };
    // compiler generates all six two-way comparison operators

    I think modern C++, especially C++20, becomes more and more
    similar to Rust. But, as you already concluded, suffers from
    a long evolution. The fact that we use terms like «modern C++»
    says a lot.

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht