🦀/⚙️/18. C++ → Rust Semantic Deep Dives

C++ → Rust Semantic Deep Dives

What you'll learn: Detailed mappings for C++ concepts that don't have obvious Rust equivalents — the four named casts, SFINAE vs trait bounds, CRTP vs associated types, and other common friction points during translation.

The sections below map C++ concepts that don't have an obvious 1:1 Rust equivalent. These differences frequently trip up C++ programmers during translation work.

Casting Hierarchy: Four C++ Casts → Rust Equivalents

C++ has four named casts. Rust replaces them with different, more explicit mechanisms:

// C++ casting hierarchy
int i = static_cast<int>(3.14);            // 1. Numeric / up-cast
Derived* d = dynamic_cast<Derived*>(base); // 2. Runtime downcasting
int* p = const_cast<int*>(cp);              // 3. Cast away const
auto* raw = reinterpret_cast<char*>(&obj); // 4. Bit-level reinterpretation
C++ CastRust EquivalentSafetyNotes
static_cast (numeric)as keywordSafe but can truncate/wraplet i = 3.14_f64 as i32; — truncates to 3
static_cast (numeric, checked)From/IntoSafe, compile-time verifiedlet i: i32 = 42_u8.into(); — only widens
static_cast (numeric, fallible)TryFrom/TryIntoSafe, returns Resultlet i: u8 = 300_u16.try_into()?; — returns Err
dynamic_cast (downcast)match on enum / Any::downcast_refSafePattern matching for enums; Any for trait objects
const_castNo equivalentRust has no way to cast away &&mut in safe code. Use Cell/RefCell for interior mutability
reinterpret_caststd::mem::transmuteunsafeReinterprets bit pattern. Almost always wrong — prefer from_le_bytes() etc.
// Rust equivalents:

// 1. Numeric casts — prefer From/Into over `as`
let widened: u32 = 42_u8.into();             // Infallible widening — always prefer
let truncated = 300_u16 as u8;                // ⚠ Wraps to 44! Silent data loss
let checked: Result<u8, _> = 300_u16.try_into(); // Err — safe fallible conversion

// 2. Downcast: enum (preferred) or Any (when needed for type erasure)
use std::any::Any;

fn handle_any(val: &dyn Any) {
    if let Some(s) = val.downcast_ref::<String>() {
        println!("Got string: {s}");
    } else if let Some(n) = val.downcast_ref::<i32>() {
        println!("Got int: {n}");
    }
}

// 3. "const_cast" → interior mutability (no unsafe needed)
use std::cell::Cell;
struct Sensor {
    read_count: Cell<u32>,  // Mutate through &self
}
impl Sensor {
    fn read(&self) -> f64 {
        self.read_count.set(self.read_count.get() + 1); // &self, not &mut self
        42.0
    }
}

// 4. reinterpret_cast → transmute (almost never needed)
// Prefer safe alternatives:
let bytes: [u8; 4] = 0x12345678_u32.to_ne_bytes();  // ✅ Safe
let val = u32::from_ne_bytes(bytes);                   // ✅ Safe
// unsafe { std::mem::transmute::<u32, [u8; 4]>(val) } // ❌ Avoid

Guideline: In idiomatic Rust, as should be rare (use From/Into for widening, TryFrom/TryInto for narrowing), transmute should be exceptional, and const_cast has no equivalent because interior mutability types make it unnecessary.


Preprocessor → cfg, Feature Flags, and macro_rules!

C++ relies heavily on the preprocessor for conditional compilation, constants, and code generation. Rust replaces all of these with first-class language features.

#define constants → const or const fn

// C++
#define MAX_RETRIES 5
#define BUFFER_SIZE (1024 * 64)
#define SQUARE(x) ((x) * (x))  // Macro — textual substitution, no type safety
// Rust — type-safe, scoped, no textual substitution
const MAX_RETRIES: u32 = 5;
const BUFFER_SIZE: usize = 1024 * 64;
const fn square(x: u32) -> u32 { x * x }  // Evaluated at compile time

// Can be used in const contexts:
const AREA: u32 = square(12);  // Computed at compile time
static BUFFER: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];

#ifdef / #if#[cfg()] and cfg!()

// C++
#ifdef DEBUG
    log_verbose("Step 1 complete");
#endif

#if defined(LINUX) && !defined(ARM)
    use_x86_path();
#else
    use_generic_path();
#endif
// Rust — attribute-based conditional compilation
#[cfg(debug_assertions)]
fn log_verbose(msg: &str) { eprintln!("[VERBOSE] {msg}"); }

#[cfg(not(debug_assertions))]
fn log_verbose(_msg: &str) { /* compiled away in release */ }

// Combine conditions:
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn use_x86_path() { /* ... */ }

#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
fn use_generic_path() { /* ... */ }

// Runtime check (condition is still compile-time, but usable in expressions):
if cfg!(target_os = "windows") {
    println!("Running on Windows");
}

Feature flags in Cargo.toml

# Cargo.toml — replace #ifdef FEATURE_FOO
[features]
default = ["json"]
json = ["dep:serde_json"]       # Optional dependency
verbose-logging = []            # Flag with no extra dependency
gpu-support = ["dep:cuda-sys"]  # Optional GPU support
// Conditional code based on feature flags:
#[cfg(feature = "json")]
pub fn parse_config(data: &str) -> Result<Config, Error> {
    serde_json::from_str(data).map_err(Error::from)
}

#[cfg(feature = "verbose-logging")]
macro_rules! verbose {
    ($($arg:tt)*) => { eprintln!("[VERBOSE] {}", format!($($arg)*)); }
}
#[cfg(not(feature = "verbose-logging"))]
macro_rules! verbose {
    ($($arg:tt)*) => { }; // Compiles to nothing
}

#define MACRO(x)macro_rules!

// C++ — textual substitution, notoriously error-prone
#define DIAG_CHECK(cond, msg) \
    do { if (!(cond)) { log_error(msg); return false; } } while(0)
// Rust — hygienic, type-checked, operates on syntax tree
macro_rules! diag_check {
    ($cond:expr, $msg:expr) => {
        if !($cond) {
            log_error($msg);
            return Err(DiagError::CheckFailed($msg.to_string()));
        }
    };
}

fn run_test() -> Result<(), DiagError> {
    diag_check!(temperature < 85.0, "GPU too hot");
    diag_check!(voltage > 0.8, "Rail voltage too low");
    Ok(())
}
C++ PreprocessorRust EquivalentAdvantage
#define PI 3.14const PI: f64 = 3.14;Typed, scoped, visible to debugger
#define MAX(a,b) ((a)>(b)?(a):(b))macro_rules! or generic fn max<T: Ord>No double-evaluation bugs
#ifdef DEBUG#[cfg(debug_assertions)]Checked by compiler, no typo risk
#ifdef FEATURE_X#[cfg(feature = "x")]Cargo manages features; dependency-aware
#include "header.h"mod module; + use module::Item;No include guards, no circular includes
#pragma onceNot neededEach .rs file is a module — included exactly once

Header Files and #include → Modules and use

In C++, the compilation model revolves around textual inclusion:

// widget.h — every translation unit that uses Widget includes this
#pragma once
#include <string>
#include <vector>

class Widget {
public:
    Widget(std::string name);
    void activate();
private:
    std::string name_;
    std::vector<int> data_;
};
// widget.cpp — separate definition
#include "widget.h"
Widget::Widget(std::string name) : name_(std::move(name)) {}
void Widget::activate() { /* ... */ }

In Rust, there are no header files, no forward declarations, no include guards:

// src/widget.rs — declaration AND definition in one file
pub struct Widget {
    name: String,         // Private by default
    data: Vec<i32>,
}

impl Widget {
    pub fn new(name: String) -> Self {
        Widget { name, data: Vec::new() }
    }
    pub fn activate(&self) { /* ... */ }
}
// src/main.rs — import by module path
mod widget;  // Tells compiler to include src/widget.rs
use widget::Widget;

fn main() {
    let w = Widget::new("sensor".to_string());
    w.activate();
}
C++RustWhy it's better
#include "foo.h"mod foo; in parent + use foo::Item;No textual inclusion, no ODR violations
#pragma once / include guardsNot neededEach .rs file is a module — compiled once
Forward declarationsNot neededCompiler sees entire crate; order doesn't matter
class Foo; (incomplete type)Not neededNo separate declaration/definition split
.h + .cpp for each classSingle .rs fileNo declaration/definition mismatch bugs
using namespace std;use std::collections::HashMap;Always explicit — no global namespace pollution
Nested namespace a::bNested mod a { mod b { } } or a/b.rsFile system mirrors module tree

friend and Access Control → Module Visibility

C++ uses friend to grant specific classes or functions access to private members. Rust has no friend keyword — instead, privacy is module-scoped:

// C++
class Engine {
    friend class Car;   // Car can access private members
    int rpm_;
    void set_rpm(int r) { rpm_ = r; }
public:
    int rpm() const { return rpm_; }
};
// Rust — items in the same module can access all fields, no `friend` needed
mod vehicle {
    pub struct Engine {
        rpm: u32,  // Private to the module (not to the struct!)
    }

    impl Engine {
        pub fn new() -> Self { Engine { rpm: 0 } }
        pub fn rpm(&self) -> u32 { self.rpm }
    }

    pub struct Car {
        engine: Engine,
    }

    impl Car {
        pub fn new() -> Self { Car { engine: Engine::new() } }
        pub fn accelerate(&mut self) {
            self.engine.rpm = 3000; // ✅ Same module — direct field access
        }
        pub fn rpm(&self) -> u32 {
            self.engine.rpm  // ✅ Same module — can read private field
        }
    }
}

fn main() {
    let mut car = vehicle::Car::new();
    car.accelerate();
    // car.engine.rpm = 9000;  // ❌ Compile error: `engine` is private
    println!("RPM: {}", car.rpm()); // ✅ Public method on Car
}
C++ AccessRust EquivalentScope
private(default, no keyword)Accessible within the same module only
protectedNo direct equivalentUse pub(super) for parent module access
publicpubAccessible everywhere
friend class FooPut Foo in the same moduleModule-level privacy replaces friend
pub(crate)Visible within the crate but not to external dependents
pub(super)Visible to the parent module only
pub(in crate::path)Visible within a specific module subtree

Key insight: C++ privacy is per-class. Rust privacy is per-module. This means you control access by choosing which types live in the same module — colocated types have full access to each other's private fields.


volatile → Atomics and read_volatile/write_volatile

In C++, volatile tells the compiler not to optimize away reads/writes — typically used for memory-mapped hardware registers. Rust has no volatile keyword.

// C++: volatile for hardware registers
volatile uint32_t* const GPIO_REG = reinterpret_cast<volatile uint32_t*>(0x4002'0000);
*GPIO_REG = 0x01;              // Write not optimized away
uint32_t val = *GPIO_REG;     // Read not optimized away
// Rust: explicit volatile operations — only in unsafe code
use std::ptr;

const GPIO_REG: *mut u32 = 0x4002_0000 as *mut u32;

unsafe {
    ptr::write_volatile(GPIO_REG, 0x01);   // Write not optimized away
    let val = ptr::read_volatile(GPIO_REG); // Read not optimized away
}

For concurrent shared state (the other common C++ volatile use), Rust uses atomics:

// C++: volatile is NOT sufficient for thread safety (common mistake!)
volatile bool stop_flag = false;  // ❌ Data race — UB in C++11+

// Correct C++:
std::atomic<bool> stop_flag{false};
// Rust: atomics are the only way to share mutable state across threads
use std::sync::atomic::{AtomicBool, Ordering};

static STOP_FLAG: AtomicBool = AtomicBool::new(false);

// From another thread:
STOP_FLAG.store(true, Ordering::Release);

// Check:
if STOP_FLAG.load(Ordering::Acquire) {
    println!("Stopping");
}
C++ UsageRust EquivalentNotes
volatile for hardware registersptr::read_volatile / ptr::write_volatileRequires unsafe — correct for MMIO
volatile for thread signalingAtomicBool / AtomicU32 etc.C++ volatile is wrong for this too!
std::atomic<T>std::sync::atomic::AtomicTSame semantics, same orderings
std::atomic<T>::load(memory_order_acquire)AtomicT::load(Ordering::Acquire)1:1 mapping

static Variables → static, const, LazyLock, OnceLock

Basic static and const

// C++
const int MAX_RETRIES = 5;                    // Compile-time constant
static std::string CONFIG_PATH = "/etc/app";  // Static init — order undefined!
// Rust
const MAX_RETRIES: u32 = 5;                   // Compile-time constant, inlined
static CONFIG_PATH: &str = "/etc/app";         // 'static lifetime, fixed address

The static initialization order fiasco

C++ has a well-known problem: global constructors in different translation units execute in unspecified order. Rust avoids this entirely — static values must be compile-time constants (no constructors).

For runtime-initialized globals, use LazyLock (Rust 1.80+) or OnceLock:

use std::sync::LazyLock;

// Equivalent to C++ `static std::regex` — initialized on first access, thread-safe
static CONFIG_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
    regex::Regex::new(r"^[a-z]+_diag$").expect("invalid regex")
});

fn is_valid_diag(name: &str) -> bool {
    CONFIG_REGEX.is_match(name)  // First call initializes; subsequent calls are fast
}
use std::sync::OnceLock;

// OnceLock: initialized once, can be set from runtime data
static DB_CONN: OnceLock<String> = OnceLock::new();

fn init_db(connection_string: &str) {
    DB_CONN.set(connection_string.to_string())
        .expect("DB_CONN already initialized");
}

fn get_db() -> &'static str {
    DB_CONN.get().expect("DB not initialized")
}
C++RustNotes
const int X = 5;const X: i32 = 5;Both compile-time. Rust requires type annotation
constexpr int X = 5;const X: i32 = 5;Rust const is always constexpr
static int count = 0; (file scope)static COUNT: AtomicI32 = AtomicI32::new(0);Mutable statics require unsafe or atomics
static std::string s = "hi";static S: &str = "hi"; or LazyLock<String>No runtime constructor for simple cases
static MyObj obj; (complex init)static OBJ: LazyLock<MyObj> = LazyLock::new(|| { ... });Thread-safe, lazy, no init order issues
thread_localthread_local! { static X: Cell<u32> = Cell::new(0); }Same semantics

constexprconst fn

C++ constexpr marks functions and variables for compile-time evaluation. Rust uses const fn and const for the same purpose:

// C++
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int val = factorial(5);  // Computed at compile time → 120
// Rust
const fn factorial(n: u32) -> u32 {
    if n <= 1 { 1 } else { n * factorial(n - 1) }
}
const VAL: u32 = factorial(5);  // Computed at compile time → 120

// Also works in array sizes and match patterns:
const LOOKUP: [u32; 5] = [factorial(1), factorial(2), factorial(3),
                           factorial(4), factorial(5)];
C++RustNotes
constexpr int f()const fn f() -> i32Same intent — compile-time evaluable
constexpr variableconst variableRust const is always compile-time
consteval (C++20)No equivalentconst fn can also run at runtime
if constexpr (C++17)No equivalent (use cfg! or generics)Trait specialization fills some use cases
constinit (C++20)static with const initializerRust static must be const-initialized by default

Current limitations of const fn (stabilized as of Rust 1.82):

  • No trait methods (can't call .len() on a Vec in const context)
  • No heap allocation (Box::new, Vec::new not const)
  • No floating-point arithmeticstabilized in Rust 1.82
  • Can't use for loops (use recursion or while with manual index)

SFINAE and enable_if → Trait Bounds and where Clauses

In C++, SFINAE (Substitution Failure Is Not An Error) is the mechanism behind conditional generic programming. It is powerful but notoriously unreadable. Rust replaces it entirely with trait bounds:

// C++: SFINAE-based conditional function (pre-C++20)
template<typename T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>
T double_it(T val) { return val * 2; }

template<typename T,
         std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T double_it(T val) { return val * 2.0; }

// C++20 concepts — cleaner but still verbose:
template<std::integral T>
T double_it(T val) { return val * 2; }
// Rust: trait bounds — readable, composable, excellent error messages
use std::ops::Mul;

fn double_it<T: Mul<Output = T> + From<u8>>(val: T) -> T {
    val * T::from(2)
}

// Or with where clause for complex bounds:
fn process<T>(val: T) -> String
where
    T: std::fmt::Display + Clone + Send,
{
    format!("Processing: {}", val)
}

// Conditional behavior via separate impls (replaces SFINAE overloads):
trait Describable {
    fn describe(&self) -> String;
}

impl Describable for u32 {
    fn describe(&self) -> String { format!("integer: {self}") }
}

impl Describable for f64 {
    fn describe(&self) -> String { format!("float: {self:.2}") }
}
C++ Template MetaprogrammingRust EquivalentReadability
std::enable_if_t<cond>where T: Trait🟢 Clear English
std::is_integral_v<T>Bound on a numeric trait or specific types🟢 No _v / _t suffixes
SFINAE overload setsSeparate impl Trait for ConcreteType blocks🟢 Each impl stands alone
if constexpr (std::is_same_v<T, int>)Specialization via trait impls🟢 Compile-time dispatched
C++20 concepttrait🟢 Nearly identical intent
requires clausewhere clause🟢 Same position, similar syntax
Compilation fails deep inside templateCompilation fails at the call site with trait mismatch🟢 No 200-line error cascades

Key insight: C++ concepts (C++20) are the closest thing to Rust traits. If you're familiar with C++20 concepts, think of Rust traits as concepts that have been a first-class language feature since 1.0, with a coherent implementation model (trait impls) instead of duck typing.


std::function → Function Pointers, impl Fn, and Box<dyn Fn>

C++ std::function<R(Args...)> is a type-erased callable. Rust has three options, each with different trade-offs:

// C++: one-size-fits-all (heap-allocated, type-erased)
#include <functional>
std::function<int(int)> make_adder(int n) {
    return [n](int x) { return x + n; };
}
// Rust Option 1: fn pointer — simple, no captures, no allocation
fn add_one(x: i32) -> i32 { x + 1 }
let f: fn(i32) -> i32 = add_one;
println!("{}", f(5)); // 6

// Rust Option 2: impl Fn — monomorphized, zero overhead, can capture
fn apply(val: i32, f: impl Fn(i32) -> i32) -> i32 { f(val) }
let n = 10;
let result = apply(5, |x| x + n);  // Closure captures `n`

// Rust Option 3: Box<dyn Fn> — type-erased, heap-allocated (like std::function)
fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + n)
}
let adder = make_adder(10);
println!("{}", adder(5));  // 15

// Storing heterogeneous callables (like vector<function<int(int)>>):
let callbacks: Vec<Box<dyn Fn(i32) -> i32>> = vec![
    Box::new(|x| x + 1),
    Box::new(|x| x * 2),
    Box::new(make_adder(100)),
];
for cb in &callbacks {
    println!("{}", cb(5));  // 6, 10, 105
}
When to useC++ EquivalentRust Choice
Top-level function, no capturesFunction pointerfn(Args) -> Ret
Generic function accepting callablesTemplate parameterimpl Fn(Args) -> Ret (static dispatch)
Trait bound in genericstemplate<typename F>F: Fn(Args) -> Ret
Stored callable, type-erasedstd::function<R(Args)>Box<dyn Fn(Args) -> Ret>
Callback that mutates statestd::function with mutable lambdaBox<dyn FnMut(Args) -> Ret>
One-shot callback (consumed)std::function (moved)Box<dyn FnOnce(Args) -> Ret>

Performance note: impl Fn has zero overhead (monomorphized, like a C++ template). Box<dyn Fn> has the same overhead as std::function (vtable + heap allocation). Prefer impl Fn unless you need to store heterogeneous callables.


Container Mapping: C++ STL → Rust std::collections

C++ STL ContainerRust EquivalentNotes
std::vector<T>Vec<T>Nearly identical API. Rust checks bounds by default
std::array<T, N>[T; N]Stack-allocated fixed-size array
std::deque<T>std::collections::VecDeque<T>Ring buffer. Efficient push/pop at both ends
std::list<T>std::collections::LinkedList<T>Rarely used in Rust — Vec is almost always faster
std::forward_list<T>No equivalentUse Vec or VecDeque
std::unordered_map<K, V>std::collections::HashMap<K, V>Uses SipHash by default (DoS-resistant)
std::map<K, V>std::collections::BTreeMap<K, V>B-tree; keys sorted; K: Ord required
std::unordered_set<T>std::collections::HashSet<T>T: Hash + Eq required
std::set<T>std::collections::BTreeSet<T>Sorted set; T: Ord required
std::priority_queue<T>std::collections::BinaryHeap<T>Max-heap by default (same as C++)
std::stack<T>Vec<T> with .push() / .pop()No separate stack type needed
std::queue<T>VecDeque<T> with .push_back() / .pop_front()No separate queue type needed
std::stringStringUTF-8 guaranteed, not null-terminated
std::string_view&strBorrowed UTF-8 slice
std::span<T> (C++20)&[T] / &mut [T]Rust slices have been a first-class type since 1.0
std::tuple<A, B, C>(A, B, C)First-class syntax, destructurable
std::pair<A, B>(A, B)Just a 2-element tuple
std::bitset<N>No std equivalentUse the bitvec crate or [u8; N/8]

Key differences:

  • Rust's HashMap/HashSet require K: Hash + Eq — the compiler enforces this at the type level, unlike C++ where using an unhashable key gives a template error deep in the STL
  • Vec indexing (v[i]) panics on out-of-bounds by default. Use .get(i) for Option<&T> or iterators to avoid bounds checks entirely
  • No std::multimap or std::multiset — use HashMap<K, Vec<V>> or BTreeMap<K, Vec<V>>

Exception Safety → Panic Safety

C++ defines three levels of exception safety (Abrahams guarantees):

C++ LevelMeaningRust Equivalent
No-throwFunction never throwsFunction never panics (returns Result)
Strong (commit-or-rollback)If it throws, state is unchangedOwnership model makes this natural — if ? returns early, partially built values are dropped
BasicIf it throws, invariants are preservedRust's default — Drop runs, no leaks

How Rust's ownership model helps

// Strong guarantee for free — if file.write() fails, config is unchanged
fn update_config(config: &mut Config, path: &str) -> Result<(), Error> {
    let new_data = fetch_from_network()?; // Err → early return, config untouched
    let validated = validate(new_data)?;   // Err → early return, config untouched
    *config = validated;                   // Only reached on success (commit)
    Ok(())
}

In C++, achieving the strong guarantee requires manual rollback or the copy-and-swap idiom. In Rust, ? propagation gives you the strong guarantee by default for most code.

catch_unwind — Rust's equivalent of catch(...)

use std::panic;

// Catch a panic (like catch(...) in C++) — rarely needed
let result = panic::catch_unwind(|| {
    // Code that might panic
    let v = vec![1, 2, 3];
    v[10]  // Panics! (index out of bounds)
});

match result {
    Ok(val) => println!("Got: {val}"),
    Err(_) => eprintln!("Caught a panic — cleaned up"),
}

UnwindSafe — marking types as panic-safe

use std::panic::UnwindSafe;

// Types behind &mut are NOT UnwindSafe by default — the panic may have
// left them in a partially-modified state
fn safe_execute<F: FnOnce() + UnwindSafe>(f: F) {
    let _ = std::panic::catch_unwind(f);
}

// Use AssertUnwindSafe to override when you've audited the code:
use std::panic::AssertUnwindSafe;
let mut data = vec![1, 2, 3];
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
    data.push(4);
}));
C++ Exception PatternRust Equivalent
throw MyException()return Err(MyError::...) (preferred) or panic!("...")
try { } catch (const E& e)match result { Ok(v) => ..., Err(e) => ... } or ?
catch (...)std::panic::catch_unwind(...)
noexcept-> Result<T, E> (errors are values, not exceptions)
RAII cleanup in stack unwindingDrop::drop() runs during panic unwinding
std::uncaught_exceptions()std::thread::panicking()
-fno-exceptions compile flagpanic = "abort" in Cargo.toml [profile]

Bottom line: In Rust, most code uses Result<T, E> instead of exceptions, making error paths explicit and composable. panic! is reserved for bugs (like assert! failures), not routine errors. This means "exception safety" is largely a non-issue — the ownership system handles cleanup automatically.


C++ to Rust Migration Patterns

Quick Reference: C++ → Rust Idiom Map

C++ PatternRust IdiomNotes
class Derived : public Baseenum Variant { A {...}, B {...} }Prefer enums for closed sets
virtual void method() = 0trait MyTrait { fn method(&self); }Use for open/extensible interfaces
dynamic_cast<Derived*>(ptr)match value { Variant::A(data) => ..., }Exhaustive, no runtime failure
vector<unique_ptr<Base>>Vec<Box<dyn Trait>>Only when genuinely polymorphic
shared_ptr<T>Rc<T> or Arc<T>Prefer Box<T> or owned values first
enable_shared_from_this<T>Arena pattern (Vec<T> + indices)Eliminates reference cycles entirely
Base* m_pFramework in every classfn execute(&mut self, ctx: &mut Context)Pass context, don't store pointers
try { } catch (...) { }match result { Ok(v) => ..., Err(e) => ... }Or use ? for propagation
std::optional<T>Option<T>match required, can't forget None
const std::string& parameter&str parameterAccepts both String and &str
enum class Foo { A, B, C }enum Foo { A, B, C }Rust enums can also carry data
auto x = std::move(obj)let x = obj;Move is the default, no std::move needed
CMake + make + lintcargo build / test / clippy / fmtOne tool for everything

Migration Strategy

  1. Start with data types: Translate structs and enums first — this forces you to think about ownership
  2. Convert factories to enums: If a factory creates different derived types, it should probably be enum + match
  3. Convert god objects to composed structs: Group related fields into focused structs
  4. Replace pointers with borrows: Convert Base* stored pointers to &'a T lifetime-bounded borrows
  5. Use Box<dyn Trait> sparingly: Only for plugin systems and test mocking
  6. Let the compiler guide you: Rust's error messages are excellent — read them carefully