Hello World & Build Systems
Hello, World
fn main() {
println!("Hello, World!");
} #include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
} C++ uses
#include to pull in headers rather than a module system; std::cout and the << streaming operator replace Rust's println! macro. The std:: namespace prefix is required unless you add using namespace std; — most style guides recommend keeping it explicit.Build systems
// Cargo (built into Rust):
// cargo new myproject — create new project
// cargo build — debug build → target/debug/
// cargo build --release — optimized build → target/release/
// cargo run — build + run
// cargo test — run tests
// cargo add serde — add a dependency
// cargo fmt — format code
// cargo clippy — lint // CMake (the de facto standard):
// cmake -S . -B build — configure project
// cmake --build build — compile
// cmake --build build --target install
//
// Meson (popular modern alternative):
// meson setup build
// meson compile -C build
//
// Single-file with g++ directly:
// g++ -std=c++23 -Wall -O2 main.cpp -o myprogram
// ./myprogram
//
// Package managers: Conan, vcpkg (no single official standard) Rust ships Cargo as a first-party, integrated tool for building, testing, formatting, linting, and dependency management. C++ has no equivalent standard — CMake is the most widely used build system, but it is a separate install, and dependency management is handled by third-party tools like Conan or vcpkg.
Variables & Types
Variable declaration
fn main() {
let score = 42; // immutable by default
let mut counter = 0; // mutable
counter += 1;
println!("{score} {counter}");
} #include <iostream>
int main() {
const int score = 42; // immutable (const)
int counter = 0; // mutable by default
counter += 1;
std::cout << score << " " << counter << std::endl;
} In Rust, variables are immutable by default and require
mut to allow mutation. In C++, variables are mutable by default; you must add const to make one immutable. This is the opposite of Rust's convention and a common source of accidental mutation in C++ code.Type inference
fn main() {
let numbers = vec![1, 2, 3]; // Vec<i32> inferred
let total: i64 = 100;
let message = String::from("hello");
println!("{:?} {} {}", numbers, total, message);
} #include <iostream>
#include <vector>
#include <string>
int main() {
auto numbers = std::vector{1, 2, 3}; // std::vector<int> inferred
int64_t total = 100;
auto message = std::string("hello");
for (auto number : numbers) std::cout << number << " ";
std::cout << total << " " << message << std::endl;
} Both languages support type inference. Rust infers types across whole expressions with no special keyword. C++ uses
auto to request inference — without auto, the type must be spelled out. Rust's inference is generally stronger and extends through more complex generics than C++'s auto.Constants & compile-time values
const MAX_CONNECTIONS: u32 = 1024;
static GREETING: &str = "hello";
fn main() {
println!("{MAX_CONNECTIONS} {GREETING}");
} #include <iostream>
#include <string_view>
constexpr uint32_t MAX_CONNECTIONS = 1024;
constexpr std::string_view GREETING = "hello";
int main() {
std::cout << MAX_CONNECTIONS << " " << GREETING << std::endl;
} Rust's
const is always computed at compile time. static names a global with a fixed address. C++ uses constexpr for compile-time constants and const for runtime-immutable values — the distinction is important: const int n = f(); is only evaluated at runtime if f() is not constexpr.Type aliases
type Meters = f64;
type Result<T> = std::result::Result<T, String>;
fn distance() -> Meters {
42.0
}
fn main() {
println!("{}", distance());
} #include <iostream>
#include <expected>
#include <string>
using Meters = double;
template<typename T>
using Result = std::expected<T, std::string>;
Meters distance() { return 42.0; }
int main() {
std::cout << distance() << std::endl;
} Rust uses the
type keyword for aliases; C++ uses using TypeName = ... (preferred over the older typedef). Both support generic/template aliases. Rust's type is purely an alias — it does not create a new distinct type.Shadowing
fn main() {
let value = 5;
let value = value * 2; // shadow with a new binding
let value = format!("value is {value}");
println!("{value}");
} #include <iostream>
#include <string>
#include <format>
int main() {
int value = 5;
// C++ cannot shadow in the same scope — use a new name or a new scope
{
int doubled = value * 2;
std::string message = std::format("value is {}", doubled);
std::cout << message << std::endl;
}
} Rust allows re-binding the same name in the same scope, which also permits changing the type (from
i32 to String above). C++ only permits shadowing in an inner scope — you cannot rebind a name at the same scope level. Rust shadowing is frequently used to re-parse or transform a value without inventing a second name.Strings
Owned vs borrowed strings
fn main() {
let owned: String = String::from("hello");
let borrowed: &str = "world";
let combined = format!("{owned} {borrowed}");
println!("{combined}");
} #include <iostream>
#include <string>
#include <string_view>
#include <format>
int main() {
std::string owned = "hello";
std::string_view borrowed = "world";
std::string combined = std::format("{} {}", owned, borrowed);
std::cout << combined << std::endl;
} Rust's
String (heap-allocated, owned) maps to C++'s std::string. Rust's &str (a borrowed slice with a lifetime) maps to C++'s std::string_view (C++17) — a non-owning view into a string buffer. Both &str and string_view are lightweight and avoid copies, but both carry no ownership guarantee.String formatting
fn main() {
let name = "Ferris";
let count = 3;
let message = format!("Hello, {name}! Count: {count:>5}");
println!("{message}");
} #include <iostream>
#include <format>
#include <string>
int main() {
std::string name = "Ferris";
int count = 3;
std::string message = std::format("Hello, {}! Count: {:>5}", name, count);
std::cout << message << std::endl;
} C++23's
std::format is directly inspired by Rust's format! macro — both use {} placeholders and share most format specifiers. The key difference is that Rust supports named capture of local variables ({name}) since Rust 1.58; C++ std::format uses positional arguments only.String methods
fn main() {
let sentence = String::from(" Hello, World! ");
println!("{}", sentence.trim());
println!("{}", sentence.to_uppercase());
println!("{}", sentence.contains("World"));
println!("{}", sentence.replace("World", "Rust"));
let words: Vec<&str> = "one two three".split(' ').collect();
println!("{words:?}");
} #include <iostream>
#include <string>
#include <algorithm>
#include <sstream>
#include <vector>
int main() {
std::string sentence = " Hello, World! ";
// trim (C++ has no built-in trim; use erase+find_if)
auto trimmed = sentence;
trimmed.erase(0, trimmed.find_first_not_of(" "));
trimmed.erase(trimmed.find_last_not_of(" ") + 1);
std::cout << trimmed << std::endl;
std::string upper = sentence;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
std::cout << upper << std::endl;
std::cout << (sentence.find("World") != std::string::npos) << std::endl;
// split via stringstream
std::istringstream stream("one two three");
std::vector<std::string> words;
std::string word;
while (stream >> word) words.push_back(word);
for (auto& w : words) std::cout << w << " ";
std::cout << std::endl;
} Rust's
String type has a rich built-in method surface (trim, to_uppercase, contains, replace, split). C++'s std::string is thinner — it lacks a standard trim, split requires a manual loop or std::istringstream, and case conversion goes through std::transform. C++23 adds std::ranges views that partially close this gap.Collections
Vectors / dynamic arrays
fn main() {
let mut numbers = vec![1, 2, 3];
numbers.push(4);
numbers.pop();
println!("length: {}", numbers.len());
println!("first: {}", numbers[0]);
println!("{numbers:?}");
} #include <iostream>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3};
numbers.push_back(4);
numbers.pop_back();
std::cout << "length: " << numbers.size() << std::endl;
std::cout << "first: " << numbers[0] << std::endl;
for (int number : numbers) std::cout << number << " ";
std::cout << std::endl;
} Vec<T> in Rust and std::vector<T> in C++ are the primary growable-array types and behave nearly identically. The names differ: push/pop vs push_back/pop_back, and len() vs size(). Both provide O(1) random access and amortized O(1) append.Hash maps
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 95);
scores.insert(String::from("Bob"), 87);
println!("{:?}", scores.get("Alice"));
scores.entry(String::from("Alice")).and_modify(|score| *score += 5);
println!("{:?}", scores);
} #include <iostream>
#include <unordered_map>
#include <string>
int main() {
std::unordered_map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
auto position = scores.find("Alice");
if (position != scores.end()) {
std::cout << position->second << std::endl;
position->second += 5;
}
for (auto& [name, score] : scores) {
std::cout << name << ": " << score << std::endl;
}
} Rust's
HashMap maps to C++'s std::unordered_map (hash-based, O(1) average). For sorted order, Rust uses BTreeMap and C++ uses std::map. A key difference: Rust's get returns Option<&V> (forces you to handle the missing case), while C++'s operator[] silently inserts a default-constructed value if the key is absent.Tuples
fn main() {
let point: (f64, f64) = (3.0, 4.0);
let (x, y) = point; // destructure
println!("x={x} y={y} first={}", point.0);
} #include <iostream>
#include <tuple>
int main() {
std::tuple<double, double> point = {3.0, 4.0};
auto [x, y] = point; // structured binding (C++17)
std::cout << "x=" << x << " y=" << y
<< " first=" << std::get<0>(point) << std::endl;
} Rust's tuple syntax (
(T1, T2)) is built into the language. C++ uses std::tuple<T1, T2> from the <tuple> header. Both support destructuring: Rust uses let (a, b) = t; and C++17 uses structured bindings (auto [a, b] = t;). Rust accesses elements by t.0, t.1; C++ uses std::get<0>(t).Control Flow
Match / switch
fn describe(score: u32) -> &'static str {
match score {
90..=100 => "excellent",
70..89 => "good",
50..69 => "passing",
_ => "failing",
}
}
fn main() {
println!("{}", describe(85));
} #include <iostream>
#include <string_view>
std::string_view describe(unsigned int score) {
if (score >= 90 && score <= 100) return "excellent";
if (score >= 70 && score < 90) return "good";
if (score >= 50 && score < 70) return "passing";
return "failing";
}
int main() {
std::cout << describe(85) << std::endl;
} Rust's
match supports range patterns (90..=100), guard clauses, and binding — and is exhaustive by default (the compiler rejects a non-exhaustive match). C++'s switch only works on integer-like types and requires break to prevent fall-through; range cases are not supported. Range logic in C++ typically falls back to if/else if.Conditional unwrapping
fn find_value(haystack: &[i32], needle: i32) -> Option<usize> {
haystack.iter().position(|&item| item == needle)
}
fn main() {
let numbers = [10, 20, 30, 40];
if let Some(index) = find_value(&numbers, 30) {
println!("Found at index {index}");
} else {
println!("Not found");
}
} #include <iostream>
#include <optional>
#include <vector>
#include <algorithm>
std::optional<size_t> find_value(const std::vector<int>& haystack, int needle) {
auto position = std::find(haystack.begin(), haystack.end(), needle);
if (position == haystack.end()) return std::nullopt;
return static_cast<size_t>(position - haystack.begin());
}
int main() {
std::vector<int> numbers = {10, 20, 30, 40};
if (auto index = find_value(numbers, 30)) {
std::cout << "Found at index " << *index << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
} Rust's
if let Some(x) = ... binds and unwraps an Option in one step. C++17's if (auto x = opt) does the same for std::optional — if the optional holds a value, the condition is true and x holds the optional object; dereference with *x to get the value.Loops
fn main() {
// while loop
let mut counter = 0;
while counter < 3 {
counter += 1;
}
// for-each
for number in [1, 2, 3] {
print!("{number} ");
}
println!();
// loop with break value
let result = loop {
break 42;
};
println!("{result}");
} #include <iostream>
int main() {
// while loop
int counter = 0;
while (counter < 3) {
counter++;
}
// range-for
for (int number : {1, 2, 3}) {
std::cout << number << " ";
}
std::cout << std::endl;
// loop with break — C++ has no expression-loop; use a lambda
int result = [&]() {
while (true) { return 42; }
}();
std::cout << result << std::endl;
} Rust's
loop is an infinite loop that can return a value via break expr. C++ has no equivalent; the closest idiom is an immediately-invoked lambda. Rust's for x in iter works on any type implementing Iterator; C++'s range-for works on anything with begin()/end() iterators.Functions & Closures
Functions
fn add(left: i32, right: i32) -> i32 {
left + right
}
fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
fn main() {
println!("{}", add(3, 4));
println!("{}", greet("Ferris"));
} #include <iostream>
#include <string>
#include <format>
int add(int left, int right) {
return left + right;
}
std::string greet(std::string_view name) {
return std::format("Hello, {}!", name);
}
int main() {
std::cout << add(3, 4) << std::endl;
std::cout << greet("Ferris") << std::endl;
} Function signatures look similar in both languages. Rust uses
fn name(param: Type) -> Return; C++ uses Return name(Type param). In Rust, the last expression in a function body is the implicit return value (no semicolon); C++ always requires an explicit return statement.Closures / lambdas
fn apply<F: Fn(i32) -> i32>(function: F, value: i32) -> i32 {
function(value)
}
fn main() {
let multiplier = 3;
let triple = |number| number * multiplier;
println!("{}", apply(triple, 7));
let double_it = |number: i32| -> i32 { number * 2 };
println!("{}", double_it(5));
} #include <iostream>
#include <functional>
int apply(std::function<int(int)> function, int value) {
return function(value);
}
int main() {
int multiplier = 3;
auto triple = [multiplier](int number) { return number * multiplier; };
std::cout << apply(triple, 7) << std::endl;
auto double_it = [](int number) -> int { return number * 2; };
std::cout << double_it(5) << std::endl;
} Rust closures use
|params| body and automatically capture from the enclosing scope. C++ lambdas use [capture](params) { body } and require explicit capture specification: [&] captures all by reference, [=] by value, or list specific variables. Rust's borrow checker enforces correct capture lifetimes at compile time; C++ relies on the programmer not to outlive captured references.Higher-order functions
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers.iter()
.filter(|&&number| number % 2 == 0)
.map(|&number| number * number)
.collect();
println!("{result:?}");
} #include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
auto result = numbers
| std::views::filter([](int number) { return number % 2 == 0; })
| std::views::transform([](int number) { return number * number; });
for (int value : result) std::cout << value << " ";
std::cout << std::endl;
} C++20 ranges use the pipe
| operator for chaining, closely resembling Rust's iterator chains. One major difference: C++ range views are lazy but do not automatically collect into a container — you iterate the view directly or use std::ranges::to<std::vector>(result) (C++23) to materialize it. Rust's .collect() always materializes.Structs & Methods
Struct definition
struct Point {
x: f64,
y: f64,
}
fn main() {
let origin = Point { x: 0.0, y: 0.0 };
let moved = Point { x: 3.0, ..origin };
println!("({}, {})", moved.x, moved.y);
} #include <iostream>
struct Point {
double x;
double y;
};
int main() {
Point origin = {0.0, 0.0};
Point moved = {3.0, origin.y}; // no struct-update syntax; copy fields manually
std::cout << "(" << moved.x << ", " << moved.y << ")" << std::endl;
} Both languages use
struct with named fields. Rust's struct-update syntax (..origin) copies remaining fields from an existing instance; C++ has no equivalent and requires manually copying each field. In C++, struct and class are nearly identical — the only difference is that struct members default to public and class members default to private.Methods via impl
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
fn new(width: f64, height: f64) -> Self {
Self { width, height }
}
fn area(&self) -> f64 {
self.width * self.height
}
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
}
fn main() {
let mut rect = Rectangle::new(4.0, 3.0);
rect.scale(2.0);
println!("area = {}", rect.area());
} #include <iostream>
class Rectangle {
public:
Rectangle(double width, double height)
: width_(width), height_(height) {}
double area() const { return width_ * height_; }
void scale(double factor) {
width_ *= factor;
height_ *= factor;
}
private:
double width_;
double height_;
};
int main() {
Rectangle rect(4.0, 3.0);
rect.scale(2.0);
std::cout << "area = " << rect.area() << std::endl;
} Rust separates data (
struct) from behavior (impl); C++ places both inside the class/struct body. Rust's &self (immutable receiver) corresponds to a C++ const member function; &mut self (mutable receiver) corresponds to a non-const member function. Rust has no constructors — new() is a convention for an associated function that returns Self.Destructors / Drop
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping {}", self.name);
}
}
fn main() {
let resource = Resource { name: String::from("file handle") };
println!("Using {}", resource.name);
// drop() called automatically at end of scope
} #include <iostream>
#include <string>
class Resource {
public:
Resource(std::string name) : name_(std::move(name)) {}
~Resource() {
std::cout << "Destroying " << name_ << std::endl;
}
const std::string& name() const { return name_; }
private:
std::string name_;
};
int main() {
Resource resource("file handle");
std::cout << "Using " << resource.name() << std::endl;
// ~Resource() called automatically at end of scope (RAII)
} Both languages guarantee deterministic destruction when a value goes out of scope. Rust calls the
Drop::drop trait implementation; C++ calls the destructor (~ClassName). This is the foundation of RAII in C++ and is equivalent to Rust's ownership-based resource management. Unlike C++ destructors, Rust's drop cannot be called explicitly — use std::mem::drop(value) to force early destruction.Enums & Variants
Simple enumerations
#[derive(Debug)]
enum Direction {
North,
South,
East,
West,
}
fn describe(direction: &Direction) -> &str {
match direction {
Direction::North => "going north",
Direction::South => "going south",
Direction::East => "going east",
Direction::West => "going west",
}
}
fn main() {
let heading = Direction::North;
println!("{}", describe(&heading));
} #include <iostream>
#include <string_view>
enum class Direction { North, South, East, West };
std::string_view describe(Direction direction) {
switch (direction) {
case Direction::North: return "going north";
case Direction::South: return "going south";
case Direction::East: return "going east";
case Direction::West: return "going west";
}
return "";
}
int main() {
Direction heading = Direction::North;
std::cout << describe(heading) << std::endl;
} C++ has two enum forms: the old
enum (which leaks names into the enclosing scope) and the modern enum class (C++11, scoped and strongly typed). Always prefer enum class; it is the closest equivalent to Rust's simple enum. Unlike Rust, a C++ enum class switch is not enforced as exhaustive — the compiler may warn but will not error.Enums with associated data
#[derive(Debug)]
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle(f64, f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(a, b, c) => {
let half_perimeter = (a + b + c) / 2.0;
(half_perimeter * (half_perimeter - a) * (half_perimeter - b) * (half_perimeter - c)).sqrt()
}
}
}
fn main() {
println!("{:.2}", area(&Shape::Circle { radius: 3.0 }));
println!("{:.2}", area(&Shape::Rectangle { width: 4.0, height: 5.0 }));
} #include <iostream>
#include <variant>
#include <cmath>
#include <numbers>
struct Circle { double radius; };
struct Rectangle { double width; double height; };
struct Triangle { double side_a; double side_b; double side_c; };
using Shape = std::variant<Circle, Rectangle, Triangle>;
double area(const Shape& shape) {
return std::visit([](auto&& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return std::numbers::pi * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
} else {
double half = (s.side_a + s.side_b + s.side_c) / 2.0;
return std::sqrt(half * (half - s.side_a) * (half - s.side_b) * (half - s.side_c));
}
}, shape);
}
int main() {
std::cout << std::fixed;
std::cout.precision(2);
std::cout << area(Circle{3.0}) << std::endl;
std::cout << area(Rectangle{4.0, 5.0}) << std::endl;
} Rust enums with associated data are one of its most powerful features. The C++ equivalent is
std::variant<T1, T2, ...> combined with std::visit. The ergonomics are dramatically different: Rust's match is concise and pattern-matching is a first-class language feature; std::visit requires a visitor object and if constexpr for type dispatch, which is significantly more verbose.Option / optional
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
if denominator == 0.0 {
None
} else {
Some(numerator / denominator)
}
}
fn main() {
match divide(10.0, 2.0) {
Some(result) => println!("Result: {result}"),
None => println!("Cannot divide by zero"),
}
// method chaining on Option
let doubled = divide(10.0, 2.0).map(|result| result * 2.0);
println!("{doubled:?}");
} #include <iostream>
#include <optional>
std::optional<double> divide(double numerator, double denominator) {
if (denominator == 0.0) return std::nullopt;
return numerator / denominator;
}
int main() {
if (auto result = divide(10.0, 2.0)) {
std::cout << "Result: " << *result << std::endl;
} else {
std::cout << "Cannot divide by zero" << std::endl;
}
// transform() added in C++23; manually chain with value_or
auto first = divide(10.0, 2.0);
auto doubled = first ? std::optional<double>((*first) * 2.0) : std::nullopt;
if (doubled) std::cout << *doubled << std::endl;
} Rust's
Option<T> and C++17's std::optional<T> serve the same purpose: a value that may or may not be present. Both support conditional unwrapping — Rust with if let Some(x), C++ with if (auto x = opt) then *x. C++23 added transform, and_then, and or_else to optional, matching Rust's map, and_then, and or_else combinators. The fundamental difference is that Rust's type system prevents you from using an Option as a plain value without unwrapping it first.Traits vs Templates & Concepts
Generic functions vs templates
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut biggest = &list[0];
for item in list.iter() {
if item > biggest {
biggest = item;
}
}
biggest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest: {}", largest(&chars));
} #include <iostream>
#include <vector>
#include <algorithm>
template<typename T>
const T& largest(const std::vector<T>& list) {
return *std::max_element(list.begin(), list.end());
}
int main() {
std::vector<int> numbers = {34, 50, 25, 100, 65};
std::cout << "Largest: " << largest(numbers) << std::endl;
std::vector<char> chars = {'y', 'm', 'a', 'q'};
std::cout << "Largest: " << largest(chars) << std::endl;
} Rust generics and C++ templates both generate specialized code at compile time (monomorphization vs. template instantiation). The key difference is in constraints: Rust requires explicit trait bounds (
T: PartialOrd), which are checked when the generic is defined. C++ templates without concepts compile the body against actual types at instantiation — errors appear at the call site, often with cryptic messages. C++20 Concepts bring Rust-style explicit constraints.Trait bounds vs C++20 Concepts
use std::fmt::Display;
fn print_pair<T: Display + Clone>(first: T, second: T) {
let cloned = first.clone();
println!("{cloned} and {second}");
}
fn main() {
print_pair(42, 100);
print_pair("hello", "world");
} #include <iostream>
#include <concepts>
#include <string_view>
template<typename T>
requires std::copyable<T>
void print_pair(T first, T second) {
T cloned = first;
std::cout << cloned << " and " << second << std::endl;
}
int main() {
print_pair(42, 100);
print_pair(std::string_view("hello"), std::string_view("world"));
} C++20 Concepts (
requires clauses) are the direct equivalent of Rust trait bounds. Both constrain which types a generic function or class accepts, and both produce clear error messages at the definition site rather than deep in template instantiation. Rust's approach predates concepts by years and is more central to the language; in C++, many codebases still use unconstrained templates for compatibility.Trait objects vs virtual dispatch
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn speak(&self) -> &str { "woof" } }
impl Animal for Cat { fn speak(&self) -> &str { "meow" } }
fn make_noise(animal: &dyn Animal) {
println!("{}", animal.speak());
}
fn main() {
let pets: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for pet in &pets {
make_noise(pet.as_ref());
}
} #include <iostream>
#include <vector>
#include <memory>
#include <string_view>
class Animal {
public:
virtual std::string_view speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
std::string_view speak() const override { return "woof"; }
};
class Cat : public Animal {
public:
std::string_view speak() const override { return "meow"; }
};
void make_noise(const Animal& animal) {
std::cout << animal.speak() << std::endl;
}
int main() {
std::vector<std::unique_ptr<Animal>> pets;
pets.push_back(std::make_unique<Dog>());
pets.push_back(std::make_unique<Cat>());
for (const auto& pet : pets) {
make_noise(*pet);
}
} Rust's
dyn Trait (a trait object) and C++'s abstract base class with virtual functions both enable runtime polymorphism through a vtable. The key difference: Rust separates the data type from the behavior interface entirely — a struct can implement many traits independently. C++ inheritance couples the interface to the base class. Rust also tracks the distinction statically: &dyn Trait is explicitly dynamic, whereas &impl Trait is static dispatch.Operator overloading
use std::ops::Add;
use std::fmt;
#[derive(Debug, Clone, Copy)]
struct Vector2 { x: f64, y: f64 }
impl Add for Vector2 {
type Output = Self;
fn add(self, other: Self) -> Self {
Self { x: self.x + other.x, y: self.y + other.y }
}
}
impl fmt::Display for Vector2 {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "({}, {})", self.x, self.y)
}
}
fn main() {
let point_a = Vector2 { x: 1.0, y: 2.0 };
let point_b = Vector2 { x: 3.0, y: 4.0 };
println!("{}", point_a + point_b);
} #include <iostream>
struct Vector2 {
double x;
double y;
Vector2 operator+(const Vector2& other) const {
return {x + other.x, y + other.y};
}
};
std::ostream& operator<<(std::ostream& stream, const Vector2& vector) {
return stream << "(" << vector.x << ", " << vector.y << ")";
}
int main() {
Vector2 point_a = {1.0, 2.0};
Vector2 point_b = {3.0, 4.0};
std::cout << (point_a + point_b) << std::endl;
} Both languages allow operator overloading. Rust implements operators by implementing standard-library traits (
Add, Sub, Mul, Display). C++ uses special member or free functions named operator+, operator<<, etc. Rust's trait-based approach means you can also abstract over "any type that supports Add" using generics, which is harder to express cleanly in C++.Ownership & Smart Pointers
Move semantics
fn consume(text: String) {
println!("Consumed: {text}");
}
fn main() {
let greeting = String::from("hello");
consume(greeting);
// println!("{greeting}"); // compile error: greeting was moved
} #include <iostream>
#include <string>
void consume(std::string text) {
std::cout << "Consumed: " << text << std::endl;
}
int main() {
std::string greeting = "hello";
consume(std::move(greeting));
// greeting is now in a valid but unspecified state
// accessing it is legal but the value is gone
std::cout << "(greeting is now empty: '" << greeting << "')" << std::endl;
} Rust moves values by default — after a move, the compiler prevents any use of the original binding (a compile-time error). C++ moves are opt-in via
std::move, and after a move the source object is in a "valid but unspecified state" — using it is legal C++ but usually wrong. Rust's move semantics are tracked by the borrow checker, eliminating an entire class of use-after-move bugs that C++ must rely on conventions and linters to catch.Unique ownership (Box / unique_ptr)
fn main() {
let boxed_value = Box::new(42);
println!("Value: {boxed_value}");
// boxed_value dropped and heap memory freed here
} #include <iostream>
#include <memory>
int main() {
auto boxed_value = std::make_unique<int>(42);
std::cout << "Value: " << *boxed_value << std::endl;
// boxed_value goes out of scope; unique_ptr destructor frees the heap memory
} Rust's
Box<T> is equivalent to C++'s std::unique_ptr<T>: both heap-allocate a single value with unique ownership. When the owner goes out of scope, the memory is freed automatically. In Rust, Box<T> is also used to create recursive data structures and trait objects (Box<dyn Trait>). Always use std::make_unique in C++ rather than new directly.Shared ownership (Rc / shared_ptr)
use std::rc::Rc;
fn main() {
let shared = Rc::new(String::from("shared data"));
let clone_a = Rc::clone(&shared);
let clone_b = Rc::clone(&shared);
println!("{}", shared);
println!("reference count: {}", Rc::strong_count(&shared));
drop(clone_a);
println!("after drop: {}", Rc::strong_count(&shared));
drop(clone_b); // last clone; drops original too when shared goes out of scope
} #include <iostream>
#include <memory>
#include <string>
int main() {
auto shared = std::make_shared<std::string>("shared data");
auto clone_a = shared;
auto clone_b = shared;
std::cout << *shared << std::endl;
std::cout << "reference count: " << shared.use_count() << std::endl;
clone_a.reset();
std::cout << "after reset: " << shared.use_count() << std::endl;
} Rust's
Rc<T> (single-threaded reference-counted) maps to C++'s std::shared_ptr<T>. For multi-threaded use, Rust uses Arc<T> (atomically reference-counted); C++'s shared_ptr is always atomic and therefore safe across threads. Both free the allocation when the last reference is dropped. Rust enforces which pointer type to use via the type system (Rc vs Arc); C++ has no such enforcement.Interior mutability
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared_counter = Rc::new(RefCell::new(0));
let counter_clone = Rc::clone(&shared_counter);
*counter_clone.borrow_mut() += 10;
println!("Counter: {}", shared_counter.borrow());
} #include <iostream>
#include <memory>
struct Counter {
mutable int value = 0; // mutable even through a const reference
};
int main() {
auto shared_counter = std::make_shared<Counter>();
auto counter_clone = shared_counter;
counter_clone->value += 10;
std::cout << "Counter: " << shared_counter->value << std::endl;
} Rust's
RefCell<T> moves borrow-checking from compile time to runtime, allowing mutation of data even when you only have a shared reference. C++ achieves the same by marking a field mutable, which allows it to be modified through a const pointer or reference. Rust panics at runtime if borrow rules are violated (RefCell); C++ has no runtime check — a mutable field can be modified from anywhere.Error Handling
Result / std::expected
use std::num::ParseIntError;
fn parse_positive(text: &str) -> Result<u32, ParseIntError> {
text.parse::<u32>()
}
fn main() {
match parse_positive("42") {
Ok(number) => println!("Parsed: {number}"),
Err(error) => println!("Error: {error}"),
}
match parse_positive("bad") {
Ok(number) => println!("Parsed: {number}"),
Err(error) => println!("Error: {error}"),
}
} #include <iostream>
#include <expected>
#include <string>
#include <charconv>
std::expected<unsigned int, std::string> parse_positive(std::string_view text) {
unsigned int number = 0;
auto [ptr, error_code] = std::from_chars(text.data(), text.data() + text.size(), number);
if (error_code != std::errc{}) {
return std::unexpected("failed to parse '" + std::string(text) + "'");
}
return number;
}
int main() {
auto result_a = parse_positive("42");
if (result_a) std::cout << "Parsed: " << *result_a << std::endl;
else std::cout << "Error: " << result_a.error() << std::endl;
auto result_b = parse_positive("bad");
if (result_b) std::cout << "Parsed: " << *result_b << std::endl;
else std::cout << "Error: " << result_b.error() << std::endl;
} C++23's
std::expected<T, E> is directly modeled after Rust's Result<T, E>. Both represent a value that is either a success (Ok/std::expected with a value) or a failure (Err/std::unexpected). std::expected also gained transform, and_then, and or_else (C++23) to match Rust's combinator methods.Error propagation
use std::fs;
use std::num::ParseIntError;
fn read_number(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let text = fs::read_to_string(path)?; // ? propagates the error
let number = text.trim().parse::<i32>()?;
Ok(number)
}
fn main() {
match read_number("missing.txt") {
Ok(number) => println!("Got: {number}"),
Err(error) => println!("Failed: {error}"),
}
} #include <iostream>
#include <fstream>
#include <expected>
#include <string>
std::expected<int, std::string> read_number(const std::string& path) {
std::ifstream file(path);
if (!file) return std::unexpected("cannot open file: " + path);
std::string text;
std::getline(file, text);
try {
return std::stoi(text);
} catch (const std::exception& exception) {
return std::unexpected(std::string("parse failed: ") + exception.what());
}
}
int main() {
auto result = read_number("missing.txt");
if (result) std::cout << "Got: " << *result << std::endl;
else std::cout << "Failed: " << result.error() << std::endl;
} Rust's
? operator propagates an error by returning early from the enclosing function, converting the error type if needed. C++ has no equivalent operator for std::expected — each call that can fail must be checked manually. The traditional C++ alternative is exceptions (throw/try/catch), which propagate automatically but have runtime overhead and make control flow invisible at the call site.Panic / terminate
fn require_positive(value: i32) -> i32 {
if value <= 0 {
panic!("value must be positive, got {value}");
}
value
}
fn main() {
println!("{}", require_positive(5));
// require_positive(-1); // would panic and abort
} #include <iostream>
#include <stdexcept>
int require_positive(int value) {
if (value <= 0) {
throw std::invalid_argument("value must be positive, got " + std::to_string(value));
}
return value;
}
int main() {
std::cout << require_positive(5) << std::endl;
// require_positive(-1); // would throw; unhandled => std::terminate
} Rust's
panic! unwinds the stack (or aborts, depending on the panic handler), terminates the current thread, and is intended for unrecoverable programming errors. C++ throws exceptions for the same purpose; an uncaught exception calls std::terminate. Unlike exceptions, a Rust panic! cannot be caught with normal control flow — only std::panic::catch_unwind (for FFI boundaries) can intercept it.Iterators & Ranges
Iterator chaining
fn main() {
let total: i32 = (1..=10)
.filter(|number| number % 2 == 0)
.map(|number| number * number)
.sum();
println!("Sum of squares of evens 1-10: {total}");
} #include <iostream>
#include <ranges>
#include <numeric>
int main() {
auto evens_squared = std::views::iota(1, 11)
| std::views::filter([](int number) { return number % 2 == 0; })
| std::views::transform([](int number) { return number * number; });
int total = 0;
for (int value : evens_squared) total += value;
std::cout << "Sum of squares of evens 1-10: " << total << std::endl;
} C++20 ranges with the pipe
| operator closely mirror Rust's iterator chains. std::views::iota is Rust's range (1..=10). Both are lazy — no work is done until the chain is consumed. std::ranges::fold_left (C++23) is Rust's .fold(0, |acc, x| acc + x). For simple cases like summing, Rust's .sum() is cleaner; C++ needs fold_left or std::reduce.Collecting iterators
fn main() {
let words = vec!["hello", "world", "rust"];
let uppercased: Vec<String> = words.iter()
.map(|word| word.to_uppercase())
.collect();
println!("{uppercased:?}");
let joined = uppercased.join(", ");
println!("{joined}");
} #include <iostream>
#include <vector>
#include <string>
#include <ranges>
#include <algorithm>
int main() {
std::vector<std::string> words = {"hello", "world", "rust"};
auto view = words | std::views::transform([](const std::string& word) {
std::string upper = word;
std::ranges::transform(upper, upper.begin(), ::toupper);
return upper;
});
std::vector<std::string> uppercased(view.begin(), view.end());
for (const auto& word : uppercased) std::cout << word << " ";
std::cout << std::endl;
// join with delimiter
std::string joined;
for (size_t index = 0; index < uppercased.size(); ++index) {
if (index > 0) joined += ", ";
joined += uppercased[index];
}
std::cout << joined << std::endl;
} Rust's
.collect() materializes a lazy iterator chain into a concrete collection type. C++ range views must be materialized by constructing a container from the view's begin/end iterators, or with std::ranges::to<std::vector>(view) (C++23). Rust's str::join is also absent from C++ — a manual loop or std::ostringstream is the standard idiom.Zipping iterators
fn main() {
let names = vec!["Alice", "Bob", "Carol"];
let scores = vec![95, 87, 72];
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
for (name, score) in &pairs {
println!("{name}: {score}");
}
} #include <iostream>
#include <vector>
#include <ranges>
#include <string_view>
int main() {
std::vector<std::string_view> names = {"Alice", "Bob", "Carol"};
std::vector<int> scores = {95, 87, 72};
for (auto [name, score] : std::views::zip(names, scores)) {
std::cout << name << ": " << score << std::endl;
}
std::cout << std::flush;
} std::views::zip was added in C++23, matching Rust's .zip(). Both produce a lazy view of paired elements. The C++ structured binding (auto [name, score]) in the range-for loop directly mirrors Rust's destructuring pattern. Before C++23, zipping required a manual loop with two indices or a third-party library.Concurrency
Threads
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Hello from a thread!");
});
handle.join().expect("Thread panicked");
println!("Thread finished");
} #include <iostream>
#include <thread>
int main() {
std::thread worker([]() {
std::cout << "Hello from a thread!" << std::endl;
});
worker.join();
std::cout << "Thread finished" << std::endl;
} Both languages provide OS threads with a closure/lambda as the thread body. A critical difference: Rust's type system enforces thread safety at compile time — you cannot share data across threads without using
Arc (shared ownership) and Mutex (synchronized access). In C++, sharing a raw pointer across threads compiles silently and causes undefined behavior if not synchronized correctly.Mutex-protected shared state
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter_clone = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut value = counter_clone.lock().unwrap();
*value += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Counter: {}", *counter.lock().unwrap());
} #include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
int counter = 0;
std::mutex mutex;
std::vector<std::thread> threads;
for (int index = 0; index < 5; ++index) {
threads.emplace_back([&counter, &mutex]() {
std::lock_guard<std::mutex> lock(mutex);
counter++;
});
}
for (auto& thread : threads) {
thread.join();
}
std::cout << "Counter: " << counter << std::endl;
} In Rust, the data (
0) is owned inside the Mutex — you cannot access the value without locking. In C++, the mutex and the data it protects are separate objects; the programmer must remember to lock before accessing. Rust's approach makes it impossible to forget — the type system refuses to compile code that accesses the value without a lock. C++'s std::lock_guard is RAII-based and automatically unlocks when it goes out of scope, just like Rust's MutexGuard.Async / await
// Rust async requires a runtime (e.g. tokio):
// #[tokio::main]
// async fn main() {
// let result = fetch_data().await;
// }
//
// async fn fetch_data() -> String {
// // async I/O here
// String::from("data")
// }
//
// Cargo.toml: tokio = { version = "1", features = ["full"] }
// Simple futures example without an external runtime:
use std::future::ready;
fn main() {
let future = ready(42);
// In real code: runtime.block_on(future) or .await inside async fn
println!("Future created (needs runtime to poll)");
} // C++20/23 coroutines are lower-level than Rust's async — they require
// a coroutine framework (e.g. cppcoro, libunifex, or C++23 std::generator).
// There is no standard async I/O runtime in C++ (no equivalent of Tokio).
//
// A C++23 generator (synchronous coroutine):
#include <generator>
#include <iostream>
std::generator<int> fibonacci() {
int previous = 0, current = 1;
while (true) {
co_yield previous;
auto next = previous + current;
previous = current;
current = next;
}
}
int main() {
for (int number : fibonacci() | std::views::take(8)) {
std::cout << number << " ";
}
std::cout << std::endl;
} Rust's
async/await is built around the Future trait and requires a runtime (Tokio, async-std) to poll futures. C++20 introduced coroutines as a low-level mechanism — they can model async, generators, and lazy sequences, but the standard library provides no async runtime. C++23 added std::generator for synchronous lazy sequences. For production async I/O in C++, third-party frameworks (Boost.Asio, cppcoro) are required.Interoperability
Calling C from Rust / C++
// Rust calling a C function:
unsafe extern "C" {
fn abs(value: i32) -> i32;
}
fn main() {
unsafe {
println!("{}", abs(-42));
}
} // C++ can call C directly (C++ is a superset of C for linking):
#include <cstdlib> // wraps C's <stdlib.h>
#include <iostream>
int main() {
std::cout << std::abs(-42) << std::endl;
} Rust declares C functions with
extern "C" blocks and must call them inside unsafe blocks, making the boundary explicit. C++ can call C functions without any annotation — it is binary-compatible with C and the standard library wraps all C headers. When C++ code needs to be called from C, it must use extern "C" linkage to disable name mangling.Unsafe code
fn main() {
let mut value = 42;
let raw_pointer = &mut value as *mut i32;
// Raw pointer operations require unsafe
unsafe {
*raw_pointer += 1;
println!("Value via raw pointer: {}", *raw_pointer);
}
println!("Value after: {value}");
} #include <iostream>
int main() {
int value = 42;
int* raw_pointer = &value;
// C++ has no unsafe regions — raw pointer access is always allowed
*raw_pointer += 1;
std::cout << "Value via raw pointer: " << *raw_pointer << std::endl;
std::cout << "Value after: " << value << std::endl;
} Rust isolates memory-unsafe operations (raw pointer dereference, calling extern functions, accessing
static mut) inside unsafe blocks, making every unsafe assumption explicit and searchable. In C++, all code is implicitly unsafe — raw pointer arithmetic, manual casts, and memory access are always available with no syntactic marker. Rust's unsafe does not disable the borrow checker; it only unlocks the small set of operations the checker cannot verify.