Skip to main content

Rust vs modern C++ 2/3

· 4 min read
Oliver With
Senior Software Engineer

Nach dem Ternary-Operator, den es gar nicht braucht, und verschiedenen Ansätzen zu Slices geht die Diskussion weiter über Optionals und Vergleichsoperatoren.


Companion Blog für Entscheider: Modernes C++ und Rust: Wenn Theorie auf Praxis trifft

Optionale Werte: Wenn nichts auch was ist

Marco: "Schau mal. Wir haben jetzt auch std::optional!"

Sarah: "Welcome to the party, pal! Ist im Prinzip das Gleiche wie unser Option<T>."

Marco: "Siehst du, C++ bleibt relevant!"

Das Problem: Einen Wert im Array finden

Klassisches Szenario: Wir suchen das erste Element, das eine Bedingung erfüllt, falls es eins gibt.

C++: std::optional

#include <optional>
#include <vector>
#include <iostream>

std::optional<int> find_first_even(const std::vector<int>& numbers) {
for (const auto& n : numbers) {
if (n % 2 == 0) {
return n;
}
}
return std::nullopt;
}

int main() {
std::vector<int> numbers = {1, 3, 7, 8, 9};

if (auto result = find_first_even(numbers); result.has_value()) {
std::cout << "Found: " << result.value() << "\n";
} else {
std::cout << "No even number found\n";
}
}

Marco: "Sauber. Die Absenz ist explizit mit std::nullopt. Funktional geht auch:"

#include <algorithm>

std::optional<int> find_first_even_functional(const std::vector<int>& numbers) {
auto it = std::ranges::find_if(numbers, [](int n) { return n % 2 == 0; });
if (it != numbers.end()) {
return *it;
}
return std::nullopt;
}

Marco: "Aber ich muss aus dem Iterator selbst den Rückgabewert bauen.

Rust: Option<T>

fn find_first_even(numbers: &[i32]) -> Option<i32> {
for &n in numbers {
if n % 2 == 0 {
return Some(n);
}
}
None
}

fn main() {
let numbers = vec![1, 3, 7, 8, 9];
let result = find_first_even(&numbers);

match result {
Some(n) => println!("Found: {}", n),
None => println!("No even number found"),
}
}

Sarah: "Gleiche Idee. Funktional geht natürlich auch:"

fn find_first_even_functional(numbers: &[i32]) -> Option<i32> {
numbers.iter().find(|&&n| n % 2 == 0).copied()
}

Sarah: "Kein if-check nötig denn find() gibt direkt Option zurück. Das |&&n| ist übrigens keine doppelte Referenzierung sondern ein pattern match auf eine doppelte Referenzierung. Danach kann das n direkt verwendet werden."

Sarah: "Im Prinzip haben wir beide das gleiche Konzept: Einen Typen, um die Absenz eines Wertes zu signalisieren."

Marco: "Genau! std::optional und Option<T> machen im Grunde das Gleiche."

Sarah: "Ein kleiner Unterschied ist: Bei uns ist Option<T> der der normale Weg. In C++ ist es der moderne Weg."

// C++: Klassische Wege, um "kein Wert" zu signalisieren
User* find_user_old(int id); // nullptr = nicht gefunden
auto it = vec.find(...); // vec.end() = nicht gefunden
int get_value(); // -1 = nicht gefunden (Sentinel)

// C++: Moderner Weg (seit C++17)
std::optional<User> find_user(int id); // std::nullopt = nicht gefunden
// Rust: Der einzige Weg
fn find_user(id: u32) -> Option<User> { /* ... */ }
// Keine nullptr, keine Sentinel-Werte, keine "end()" Iteratoren

Marco: "Stimmt... wir haben std::optional zu einer Sprache hinzugefügt, die schon viele andere Wege hatte. Ihr habt es von Anfang an als den Weg designed."

Sarah: "Genau. Das macht Code vorhersagbarer. Wenn ich eine Funktion sehe, die Option<T> zurückgibt, weiß ich sofort: Der Wert kann fehlen. Keine Überraschungen mit nullptr oder magischen -1 Werten."

Umgang mit Optionals: Pattern Matching

Sarah: "Aber schau dir an, wie unterschiedlich wir mit den Werten umgehen:"

// Rust: Pattern Matching extrahiert den Wert direkt
match find_first_even(&numbers) {
Some(n) => println!("Das erste gerade Zahl ist {}", n),
None => println!("Keine gefunden"),
}

// Oder mit if let
if let Some(n) = find_first_even(&numbers) {
println!("Gefunden: {}", n);
}

Marco: "Bei uns muss man das manuell prüfen:"

// C++: Manuelles Auspacken
if (auto result = find_first_even(numbers); result.has_value()) {
std::cout << "Found: " << result.value() << "\n";
} else {
std::cout << "No even number found\n";
}

Sarah: "Pattern Matching und Algebraic Data Types ist etwas vom Nützlichsten das Rust bietet. Für mich sind das Features, die ich ständig verwende und für die es keine Entsprechung in C++ gibt. Dieses beiden Dinge prägen weite Teile der meisten Rust-Code-Bases."

Marco: "Pattern Matching ist erst für C++26 oder später geplant. Es gibt Proposals, aber noch nichts finales"

Fazit

Beide Sprachen haben mit std::optional (C++17) und Option<T> ein Konzept, um die Absenz eines Wertes explizit zu signalisieren. Das ist ein großer Fortschritt gegenüber impliziten Null-Werten oder Sentinel-Werten.

Pattern Matching: Rust hat es eingebaut, C++ bekommt es vielleicht in C++26.

Für neuen Code: Beide Sprachen empfehlen optional/Option. C++ hat hier eine einigermassen brauchbare, moderne Lösung gefunden.

Code Samples zu Options

Vergleichsoperatoren: Das Spaceship landet

Sarah: "Wenn wir schon Programmiersprachen vergleichen, lass uns mal über Vergleichsoperatoren reden."

Marco: "Seit C++20 haben wir den Spaceship-Operator! <=> macht alles automatisch."

Sarah: "Was wohl automatisch bei C++ bedeutet? Zeig mal."

C++: Der Spaceship-Operator mit = default

#include <compare>
#include <string>

struct Person {
int age;
std::string name;

// Compiler generiert automatisch lexikographische Vergleiche
auto operator<=>(const Person&) const = default;
};

int main() {
Person alice{25, "Alice"};
Person bob{23, "Bob"};

if (bob > alice) { /* ... */ }
}

Marco: "Siehst du? Mit = default generiert der Compiler alles automatisch!"

Rust: Derive Macros

#[derive(PartialOrd, PartialEq)]
struct Person {
age: u32,
name: String,
}

fn main() {
let alice = Person { age: 25, name: "Alice".into() };
let bob = Person { age: 23, name: "Bob".into() };

if bob > alice { /* ... */ }
}

Sarah: "Praktisch das Gleiche. Hier hat C++ wirklich eine praktische und Lösung gefunden."

Marco: "Endlich mal ein Lob! Aber wenn der lexikographische Vergleich nicht ausreicht und ich was Spezielles brauche?"

Wenn man mehr Kontrolle braucht

C++: Manuelle Implementation

struct Person {
int age;
std::string name;

std::strong_ordering operator<=>(const Person& other) const {
if (auto age_cmp = age <=> other.age; age_cmp != 0) {
return age_cmp;
}
return name <=> other.name;
}
};

Rust: Trait manuell implementieren

use std::cmp::Ordering;

impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match self.age.partial_cmp(&other.age) {
Some(Ordering::Equal) => self.name.partial_cmp(&other.name),
ord => ord,
}
}
}

impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.age == other.age && self.name == other.name
}
}

Marco: "Okay, bei Custom-Logic sind wir etwa gleichauf."

Sarah: "Stimmt. Hier habt ihr wirklich nachgelegt."

Marco: "Was ich verschwiegen habe... Wenn ich den <=>-Operator manuell implementiere, muss ich auch den == operator implementieren, damit == und != Vergleiche gemacht werden können.

Sarah: "Da sind wir wieder bei C++ kann alles ... nur manchmal ein bisschen kompliziert"

Fazit

Mit = default hat C++20 hier tatsächlich eine DX-freundliche Lösung gefunden! Der Spaceship-Operator ist eine echte Verbesserung. Rust ist mit #[derive] vergleichbar kompakt. Hier hat C++ gezeigt, dass moderne Features durchaus elegant sein können. Hier ist nicht sowas wie ein obskures Lambda-Konstrukt nötig.

Wichtig: In beiden Sprachen müssen die zugrundeliegenden Member-Typen (int, String, etc.) bereits vergleichbar sein.

Code Samples zu Vergleichsoperatoren

Image Credits

  • Rust logo © The Rust Foundation, used under CC BY 4.0. Modified and combined with other elements.
  • C++ logo by Jeremy Kratz. Modified and combined with other elements.

Cover image derived from the above and shared under CC BY 4.0.