• Noser.com
facebook
linkedin
twitter
youtube
  • NOSERmobile
    • Android
    • HTML 5
    • Hybrid Apps
    • iOS
    • Windows Phone
  • NOSERembedded
    • Medizintechnik
  • NOSERprojektmanagement
  • NOSERtesting
  • NOSERlifecycle
    • .NET Allgemein
    • Application Lifecylce Management
    • Apps
    • Architektur
    • ASP.NET
    • Azure
    • Cleancode
    • Cloud
    • Silverlight
    • Visual Studio / Team Foundation Server
    • Windows 8
    • Windows Presentation Foundation
  • NOSERinnovation
    • Big Data
    • Cloud
    • IoT
    • Operations Research
    • Augmented Reality
    • RFID, NFC, Bluetooth LE

What makes Rust easier than C++

04. Februar 2022
Willi Fluehmann
1
C++, Programming, Rust

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:

  • There are no constructors in Rust. The programmer provides a regular static method named “new” that does the same thing.
  • Rust prefers moving over copying: Passing values to a function or returning them is considered a move operation by default. An expensive copy implementation (including duplication of all contained objects) is less common and therefore not mandatory. It can still be provided via Clone trait.
  • Rust completely ignores the original object after a move operation. No destructor will be called in contrast to C++. Often a custom move implementation is required in C++ to reset some pointers to null (preventing any double-free issues).

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:

  • Fields (data definition) and methods (operating on the data) are in separate blocks.
  • There are no static fields. They are replaced by static variables on module scope.
  • Methods implementing a particular trait (interface) are in a separate block. They can be regarded as “virtual” (associated with a vtable). Other methods are always non-virtual because Rust doesn’t support inheritance.

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++:

  • Adding a library only requires a single line in the Cargo.toml file
  • No separation into header/source file, no include guards, no forward declarations
  • No unexpected side effects of preprocessor defines or macros somewhere in the included headers
  • Panics and result types instead of exceptions: No need to think about exception safety everywhere
  • Defensive programming already enforced by default (const, smart pointers, strong type safety, etc.), no extra keywords or encapsulation required
  • More effective type inference (looking backward and forward)
  • More self-explaining compiler errors
  • etc.

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.

Ubuntu Core oder Azure Sphere? – Plattformen für das IoT

16. November 2020
Willi Fluehmann
0
Azure, Cloud, Embedded, IoT, linux

Grössere IoT-Geräte verwenden meist Linux als Grundlage, da es bereits viel bietet (Boards, Kernel, Treiber, usw.). Trotzdem ist es ein weiter Weg, damit ein Produkt zu bauen. Denn die Geräte sind im Kern letztlich immer noch ein Embedded-System, an welches die üblichen Anforderungen gestellt werden (z.B. selbständig, zuverlässig und wartungsarm). Gleichzeitig kommt man an automatischen Software-Updates kaum mehr vorbei. Fertige Plattformen wie Ubuntu Core oder Azure Sphere können einem dabei viel Arbeit abnehmen. Während sich der Entwickler auf die Applikation konzentriert, kümmert sich der Hersteller um die Wartung der Plattform und das Stopfen von Sicherheitslücken. Nachfolgend werden die Vor- und Nachteile einer solchen Plattform gegenüber einem Eigenbau aufgezeigt.

READ MORE

12

Tag Cloud

.NET android Angular AngularJs Arduino ASP.Net automated testing Azure Big Data C# C++ Cloud continuous integration Elm Embedded Führung gRPC Internet of Things IoT Java Javascript M2M OWASP Projektmanagement protobuf Python Raspberry Pi Reactive Programming REST Scrum Security Softwarequalität SPA Testen testing Testmanagement Teststrategie UX Visual Studio WebAPI windows WPF Xamarin Xamarin.Android Xamarin.Forms

Archive

Current Posts

  • Akzente setzen mit der Android Splash Screen API unter .NET MAUI
  • Do You have Your Personal Space?
  • Automated provisioning with ARM Templates
  • Asynchrone Beobachtungen und Versprechungen in Angular
  • Simplify Your Automated Tests With Fluent Syntax

Last Comments

  • Hans Reinsch bei Der Safety-Plan: Die wichtigsten Antworten mit Checkliste
  • George H. Barbehenn bei Modeling Optocouplers with Spice
  • Noser Blog Touch-Actions in Xamarin.Forms - Noser Blog bei Mach mehr aus Animationen in Xamarin.Forms mit SkiaSharp
  • Noser Blog Focus on the Secure Storage service of Trusted Firmware (TFM) - Noser Blog bei First run of the Trusted Firmware (TFM) application
  • Noser Blog First run of the Trusted Firmware (TFM) application - Noser Blog bei Focus on the Secure Storage service of Trusted Firmware (TFM)

Popular Posts

Xamarin.Android Code Obfuscation

6 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 1 - Aufbruch

5 Comments

ManuScripts: Wenn jemand eine Reise tut... Funktionale Programmierung mit Elm - Teil 2 - Kein Picknick

4 Comments

Contact us

  1. Name *
    * Please enter your name
  2. Email *
    * Please enter a valid email address
  3. Message *
    * Please enter message
© 2013 NOSER ENGINEERING AG. All rights reserved. Datenschutz | Cookie-Richtlinie