Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C++ → Rust Semantic Deep Dives
C++ → Rust 语义深潜

What you’ll learn: Detailed mappings for C++ concepts that do not have obvious Rust equivalents — the four named casts, SFINAE vs trait bounds, CRTP vs associated types, and other places where translation work often gets sticky.
本章将学到什么: 那些在 C++ 里很常见、但在 Rust 里没有明显一一对应物的概念,到底应该怎么映射,包括四种具名 cast、SFINAE 与 trait bound、CRTP 与关联类型,以及其他迁移时很容易卡壳的地方。

The sections below focus on exactly those C++ concepts that tend to trip people during translation work because there is no clean 1:1 substitution.
下面这些内容,专门挑的就是那种“看着好像能类比,但真翻译时总感觉哪不对劲”的 C++ 概念。很多迁移工作卡壳,恰恰就卡在这些细语义上。

Casting Hierarchy: Four C++ Casts → Rust Equivalents
cast 体系:C++ 四种具名转换在 Rust 里的对应物

C++ has four named casts. Rust does not mirror that hierarchy directly; instead, it splits the job into several more explicit mechanisms.
C++ 有四种大家都背过的具名 cast。Rust 没有把这套层级照搬过来,而是把这些用途拆散,交给几种更明确的机制分别处理。

// 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 numericas keywordUsually safe but may truncate or wrap
常能用,但可能截断或绕回
let i = 3.14_f64 as i32; truncates to 3
static_cast widening numericFrom / IntoSafe and explicit
安全、语义更明确
let i: i32 = 42_u8.into();
static_cast fallible numericTryFrom / TryIntoSafe, returns Result
可能失败,就显式返回结果
let i: u8 = 300_u16.try_into()?;
dynamic_cast downcastEnum match or Any::downcast_refSafePrefer enums when the variant set is closed
闭集场景优先枚举匹配
const_castNo direct equivalentUse Cell / RefCell for interior mutability instead
内部可变性才是正路
reinterpret_caststd::mem::transmuteunsafeUsually the wrong first choice
通常先该找更安全的替代法
#![allow(unused)]
fn main() {
// 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 used sparingly, From / Into should handle safe widening, TryFrom / TryInto should handle narrowing, transmute should be treated as exceptional, and const_cast simply does not exist as a normal tool.
经验建议: 惯用 Rust 里,as 应该尽量少用;安全放宽靠 From / Into,可能失败的缩窄靠 TryFrom / TryIntotransmute 则属于非常规武器。至于 const_cast,Rust 干脆就没给它留正常入口。


std::function → Function Pointers, impl Fn, and Box<dyn Fn>
std::function → 函数指针、impl FnBox<dyn Fn>

C++ std::function<R(Args...)> is a type-erased callable wrapper. Rust splits that space into several options with different trade-offs.
C++ 里的 std::function<R(Args...)> 属于类型擦除后的可调用对象包装器。Rust 没用一个东西把所有需求全吃掉,而是拆成了几种不同方案,各有代价和适用面。

// 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; };
}
#![allow(unused)]
fn main() {
// 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 callable parameterTemplate parameterimpl Fn(Args) -> Ret
Generic trait bound formtemplate<typename F>F: Fn(Args) -> Ret
Stored type-erased callablestd::function<R(Args)>Box<dyn Fn(Args) -> Ret>
Mutable callbackMutable lambda in std::functionBox<dyn FnMut(Args) -> Ret>
One-shot consumed callbackMoved callableBox<dyn FnOnce(Args) -> Ret>

Performance note: impl Fn is the zero-overhead choice because it monomorphizes like a C++ template. Box<dyn Fn> carries the same general class of overhead as std::function: indirection plus heap allocation.
性能提醒: impl Fn 基本就是零额外开销路线,和模板实例化很像;Box<dyn Fn> 则和 std::function 一样,要付出堆分配和动态分发成本。


Container Mapping: C++ STL → Rust std::collections
容器映射:C++ STL → Rust std::collections

C++ STL ContainerRust EquivalentNotes
std::vector<T>Vec<T>APIs are very close; Rust bounds-checks by default
std::array<T, N>[T; N]Fixed-size stack array
std::deque<T>VecDeque<T>Ring buffer, efficient at both ends
std::list<T>LinkedList<T>Rarely preferred in Rust
std::forward_list<T>No std equivalentUsually Vec or VecDeque instead
std::unordered_map<K, V>HashMap<K, V>Type bounds on keys are explicit
std::map<K, V>BTreeMap<K, V>Ordered map
std::unordered_set<T>HashSet<T>Requires Hash + Eq
std::set<T>BTreeSet<T>Requires Ord
std::priority_queue<T>BinaryHeap<T>Max-heap by default
std::stack<T>Vec<T>Usually no dedicated stack type needed
std::queue<T>VecDeque<T>Queue patterns map naturally here
std::stringStringUTF-8, owned
std::string_view&strBorrowed UTF-8 slice
std::span<T>&[T] / &mut [T]Slices are first-class in Rust
std::tuple<A, B, C>(A, B, C)Native syntax
std::pair<A, B>(A, B)Just a two-element tuple
std::bitset<N>No std equivalentUse crates like bitvec if needed

Key differences:
需要特别记住的差异:

  • HashMap and HashSet state key requirements explicitly through traits like Hash and Eq.
    HashMapHashSet 会把键类型要求通过 trait 显式写出来,不会等到模板深处才炸一大片错误。
  • Vec indexing with v[i] panics on out-of-bounds. Use .get(i) when absence should be handled explicitly.
    Vecv[i] 越界会 panic。只要下标不百分百可信,就优先 .get(i)
  • There is no built-in multimap / multiset; build those patterns with maps to vectors or similar structures.
    标准库里没有现成 multimap / multiset,通常用 HashMap<K, Vec<V>> 这种方式自己拼出来。

Exception Safety → Panic Safety
异常安全 → panic 安全

C++ exception safety is often explained with the no-throw / strong / basic guarantee ladder. Rust’s ownership model changes the conversation quite a bit.
C++ 里讲异常安全,常会提 no-throw、strong、basic 这三档保证。Rust 因为错误处理和所有权模型不一样,这个话题会换一种面貌出现。

C++ LevelMeaningRust Equivalent
No-throwFunction never throwsReturn Result and avoid panic for routine errors
StrongCommit-or-rollbackOften comes naturally from ownership and early-return
BasicInvariants preserved, resources cleaned upRust’s default cleanup model via Drop

How Rust ownership helps
Rust 所有权为什么会帮上忙

#![allow(unused)]
fn main() {
// 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 this strong guarantee often means manual rollback logic or copy-and-swap patterns. In Rust, ? plus ownership frequently gives the same outcome almost for free.
在 C++ 里,这种强保证往往要靠手写回滚逻辑或者 copy-and-swap。Rust 这边用 ? 配合所有权,经常天然就站到类似结果上了。

catch_unwind — the rough analogue of catch(...)
catch_unwind:大致对应 catch(...)

#![allow(unused)]
fn main() {
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 panic-safe captures
UnwindSafe:描述 unwind 过程中是否安全

#![allow(unused)]
fn main() {
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()Err(MyError::...) or occasionally panic!()
try { } catch (const E& e)match result or ? propagation
catch (...)std::panic::catch_unwind(...)
noexceptReturning Result<T, E> for routine errors
RAII cleanup during unwindingDrop::drop() during panic unwind
std::uncaught_exceptions()std::thread::panicking()
-fno-exceptionspanic = "abort" in Cargo profile

Bottom line: Most Rust code uses Result<T, E> instead of exceptions for routine failure. panic! is for bugs and broken invariants, not for ordinary control flow. That alone removes a huge amount of classic exception-safety anxiety.
一句话概括: Rust 把日常失败交给 Result<T, E>,把 panic! 留给 bug 和不变量损坏。这一下就把很多传统“异常安全焦虑”直接压下去了。


C++ to Rust Migration Patterns
C++ 到 Rust 的迁移模式

Quick Reference: C++ → Rust Idiom Map
速查:C++ 惯用法到 Rust 惯用法

C++ PatternRust IdiomNotes
class Derived : public Baseenum Variant { A {...}, B {...} }Closed sets often want enums
virtual void method() = 0trait MyTrait { fn method(&self); }Open extension points map to traits
dynamic_cast<Derived*>(ptr)match on enum or explicit downcastPrefer exhaustive enum matches when possible
vector<unique_ptr<Base>>Vec<Box<dyn Trait>>Use only when true runtime polymorphism is needed
shared_ptr<T>Rc<T> or Arc<T>But prefer plain ownership first
enable_shared_from_this<T>Arena pattern like Vec<T> + indicesOften simpler and cycle-free
Stored framework base pointers everywherePass a context parameterAvoid ambient pointer tangles
try { } catch (...) { }match on Result or ?Errors stay explicit
std::optional<T>Option<T>Exhaustive handling required
const std::string& parameter&str parameterAccepts both String and &str naturally
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 already the default
CMake + make + extra lint wiringcargo build / test / clippy / fmtTooling tends to be more unified

Migration Strategy
迁移策略

  1. Start with data types. Translate structs and enums first, because that forces ownership questions into the open early.
    先从数据类型下手。 先翻结构体和枚举,所有权问题会被尽早逼出来。
  2. Turn factories into enums when the variant set is closed. Many class hierarchies are really just tagged unions wearing a tuxedo.
    变体集合固定时,优先把工厂模式改成枚举。 很多看似威风的类层次,扒开一看其实就是带标签联合体。
  3. Break god objects into focused structs. Rust usually rewards smaller, more explicit responsibility boundaries.
    把上帝对象拆掉。 Rust 更偏爱职责明确的小结构,而不是一个对象什么都挂。
  4. Replace stored pointers with borrows or explicit handles. Long-lived raw pointer graphs are usually a smell when moving into Rust.
    把到处乱存的指针换成借用或显式句柄。 一大堆长生命周期裸指针图,迁到 Rust 时往往就是味道最重的地方。
  5. Use Box<dyn Trait> sparingly. It is valuable, but it should not become the knee-jerk replacement for every base-class pointer.
    Box<dyn Trait> 要节制用。 它当然有用,但别把每个基类指针都条件反射地翻成它。
  6. Let the compiler participate. Rust’s errors are often part of the design process, not just complaints after the fact.
    让编译器参与设计。 Rust 报错很多时候不是单纯挑刺,而是在把设计问题提前暴露出来。

Header Files and #include → Modules and use
头文件与 #include → 模块与 use

The C++ compilation model revolves around textual inclusion. Rust has no header files, no forward declarations, and no include guards in that style.
C++ 的编译模型核心是文本包含。Rust 则完全不是这条思路:没有头文件,没有前置声明,也不用靠 include guard 保命。

// 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() { /* ... */ }
#![allow(unused)]
fn main() {
// 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 is better
#include "foo.h"mod foo; plus use foo::Item;No textual inclusion, less duplication
#pragma onceNot neededEach module is compiled once
Forward declarationsNot neededThe compiler sees the crate structure directly
.h + .cpp splitOne .rs file is often enoughDeclaration and definition cannot drift apart
using namespace std;use std::collections::HashMap;Imports stay explicit
Nested namespacesNested mod treeFile system and module tree line up naturally

friend and Access Control → Module Visibility
friend 与访问控制 → 模块可见性

C++ uses friend for selective access to private members. Rust does not have a friend keyword; instead, privacy is defined at the module level.
C++ 里常用 friend 给特定类或函数开后门。Rust 压根没有这个关键字,它把访问控制的核心单位换成了模块。

// 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
privateDefault visibilityAccessible inside the same module only
模块内可见
protectedNo direct equivalentpub(super) sometimes covers related needs
publicpubVisible everywhere
friend class FooPut Foo in the same moduleModule privacy replaces friend
pub(crate)Visible inside the current crate only
pub(super)Visible to the parent module
pub(in crate::path)Visible to a chosen module subtree

Key insight: C++ privacy is per-class; Rust privacy is per-module. Once that switch flips in your head, a lot of Rust API layout starts to make much more sense.
关键认知: C++ 的私有性是“按类划分”,Rust 的私有性是“按模块划分”。脑子里这个开关一旦切过来,很多 Rust API 设计就顺眼多了。


volatile → Atomics and read_volatile / write_volatile
volatile → 原子类型与显式 volatile 读写

In C++, volatile often means “do not optimize this away,” especially for MMIO. Rust intentionally has no volatile keyword and instead forces explicit operations.
在 C++ 里,volatile 经常被拿来表示“别把这次读写优化掉”,尤其是在 MMIO 里。Rust 则故意不提供这个关键字,而是要求显式调用对应操作。

// 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
#![allow(unused)]
fn main() {
// Rust: explicit volatile operations — only in unsafe code
use std::ptr;

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

unsafe {
    // SAFETY: GPIO_REG is a valid memory-mapped I/O address.
    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, Rust uses atomics. In truth, modern C++ should too; volatile is not the right tool for thread synchronization there either.
至于并发共享状态,Rust 用的是原子类型。说白了,现代 C++ 也应该这么干,volatile 本来就不是拿来做线程同步的。

// 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};
#![allow(unused)]
fn main() {
// 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 MMIOptr::read_volatile / ptr::write_volatileExplicit and usually unsafe
volatile for thread signalingAtomicBool, AtomicU32, etc.Same fix C++ should also use
std::atomic<T>std::sync::atomic::AtomicTConceptually 1:1
memory_order_acquireOrdering::AcquireSame memory ordering idea

static Variables → static, const, LazyLock, OnceLock
静态变量 → staticconstLazyLockOnceLock

Basic static and const
基础版 staticconst

// C++
const int MAX_RETRIES = 5;                    // Compile-time constant
static std::string CONFIG_PATH = "/etc/app";  // Static init — order undefined!
#![allow(unused)]
fn main() {
// 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 the classic problem that global constructors across translation units run in unspecified order. Rust avoids that whole category for plain statics because static values must be const-initialized.
C++ 里最招人烦的老问题之一,就是不同翻译单元的全局构造顺序不确定。Rust 对普通 static 直接卡死成 const 初始化,于是这类问题能少掉一大截。

For runtime-initialized globals, use LazyLock or OnceLock.
如果确实需要运行时初始化的全局对象,就上 LazyLockOnceLock

#![allow(unused)]
fn main() {
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
}
}
#![allow(unused)]
fn main() {
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 are compile-time constants
constexpr int X = 5;const X: i32 = 5;Rust const is already constexpr-like
File-scope static intstatic plus atomics or other safe wrappersMutable global state is handled more carefully
static std::string s = "hi";static S: &str = "hi"; or LazyLock<String>Pick the simpler form when possible
Complex global objectLazyLock<T>Avoids init-order issues
thread_localthread_local!Same high-level purpose

constexprconst fn
constexprconst fn

C++ constexpr marks things for compile-time evaluation. Rust’s equivalent is the combination of const and const fn.
C++ 里 constexpr 负责标记编译期求值能力;Rust 这边对应的是 constconst fn 这套组合。

// C++
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int val = factorial(5);  // Computed at compile time → 120
#![allow(unused)]
fn main() {
// 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
constexpr variableconst variableBoth compile-time
constevalNo direct equivalentRust does not split this out the same way
if constexprNo direct equivalentOften replaced by traits, generics, or cfg
constinitstatic with const initializerRust already expects const init for statics

Current limitations of const fn: not every ordinary operation is allowed in const context yet, although the boundary keeps moving as Rust evolves.
const fn 的现实限制: 它还不是“什么普通代码都能塞进去”的状态,不过可用范围一直在扩张,别拿很老的印象去判断它。


SFINAE and enable_if → Trait Bounds and where Clauses
SFINAE 与 enable_if → trait bound 与 where 子句

In C++, SFINAE powers conditional template programming, but readability is often terrible. Rust replaces the whole pattern with trait bounds.
C++ 里 SFINAE 是条件模板编程的核心手段,但可读性经常相当劝退。Rust 基本就是拿 trait bound 把这整套体验换掉了。

// 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; }
#![allow(unused)]
fn main() {
// 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: TraitMuch clearer
std::is_integral_v<T>A trait bound or specific impl setNo _v machinery clutter
SFINAE overload setsSeparate trait implsEach case stands alone
if constexpr on type categoriesTrait impl dispatch or cfgUsually simpler
C++20 conceptRust traitVery close in intent
requires clausewhere clauseSimilar placement, cleaner style
Deep template errorsCall-site trait mismatch errorsOften much easier to read

Key insight: If C++20 concepts feel familiar, that is because they are philosophically close to Rust traits. The difference is that Rust has built the whole generic model around traits from the start.
关键点: 如果已经熟悉 C++20 concept,会发现 Rust trait 在理念上非常接近。区别在于 Rust 从一开始就是围着 trait 建的整套泛型体系,而不是后来再补进去。


Preprocessor → cfg, Feature Flags, and macro_rules!
预处理器 → cfg、feature flag 与 macro_rules!

C++ leans heavily on the preprocessor for constants, conditional compilation, and code generation. Rust deliberately replaces all of that with first-class language mechanisms.
C++ 很多项目对预处理器依赖极重,常量、条件编译、代码生成全往里塞。Rust 的态度则更明确:这几类需求都应该由语言级机制分别接手,而不是继续搞文本替换一锅炖。

#define constants → const or const fn
#define 常量 → constconst fn

// C++
#define MAX_RETRIES 5
#define BUFFER_SIZE (1024 * 64)
#define SQUARE(x) ((x) * (x))  // Macro — textual substitution, no type safety
#![allow(unused)]
fn main() {
// 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!()
#ifdef / #if#[cfg()]cfg!()

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

#if defined(LINUX) && !defined(ARM)
    use_x86_path();
#else
    use_generic_path();
#endif
#![allow(unused)]
fn main() {
// 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 里的 feature flag

# 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
#![allow(unused)]
fn main() {
// 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!
函数式宏 → macro_rules!

// C++ — textual substitution, notoriously error-prone
#define DIAG_CHECK(cond, msg) \
    do { if (!(cond)) { log_error(msg); return false; } } while(0)
#![allow(unused)]
fn main() {
// 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 and scoped
有类型,也有作用域
#define MAX(a,b) ((a)>(b)?(a):(b))macro_rules! or generic fn max<T: Ord>No double evaluation traps
不会重复求值坑人
#ifdef DEBUG#[cfg(debug_assertions)]Checked by compiler
编译器会真检查
#ifdef FEATURE_X#[cfg(feature = "x")]Feature system is Cargo-aware
和依赖系统直接联动
#include "header.h"mod module; + use module::Item;No textual inclusion
#pragma onceNot neededEach .rs module is compiled once