Claude-skill-registry convert-cpp-rust
Convert C++ code to idiomatic Rust. Use when migrating C++ projects to Rust, translating C++ patterns to idiomatic Rust, or refactoring C++ codebases. Extends meta-convert-dev with C++-to-Rust specific patterns, including FFI-based gradual migration.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/convert-cpp-rust" ~/.claude/skills/majiayu000-claude-skill-registry-convert-cpp-rust && rm -rf "$T"
skills/data/convert-cpp-rust/SKILL.mdConvert C++ to Rust
Convert C++ code to idiomatic Rust. This skill extends
meta-convert-dev with C++-to-Rust specific type mappings, idiom translations, and FFI strategies for gradual migration.
This Skill Extends
- Foundational conversion patterns (APTV workflow, testing strategies, FFI patterns)meta-convert-dev
For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.
This Skill Adds
- Type mappings: C++ types → Rust types (RAII → ownership, smart pointers → Box/Rc/Arc)
- Idiom translations: C++ patterns → idiomatic Rust (templates → generics, virtual functions → traits)
- Error handling: C++ exceptions → Rust Result<T, E>
- Memory/Ownership: RAII/smart pointers → ownership/borrowing system
- FFI Integration: cxx crate for safe C++/Rust interop during migration
- Template patterns: C++ templates → Rust generics with trait bounds
This Skill Does NOT Cover
- General conversion methodology - see
meta-convert-dev - C++ language fundamentals - see
lang-cpp-dev - Rust language fundamentals - see
lang-rust-dev - Reverse conversion (Rust → C++) - see
(if exists)convert-rust-cpp - Advanced C++ metaprogramming (SFINAE, CRTP) - complex patterns require case-by-case analysis
Quick Reference
| C++ | Rust | Notes |
|---|---|---|
| | Owned, heap-allocated UTF-8 |
/ | | Borrowed string slice |
/ | / | Specify size explicitly |
| | Rust prefers explicit unsigned types |
/ | / | Direct mapping |
| | Direct mapping |
| | Growable array |
| | Fixed-size array |
| / | Unordered / ordered |
| | Single ownership, heap allocation |
| / | Reference counting (single/multi-threaded) |
| | Nullable type |
| + | Type-safe error handling |
| or | Errors vs unrecoverable failures |
| with trait bounds | Generics with constraints |
/ | + blocks | Separation of data and behavior |
functions | + | Dynamic dispatch via trait objects |
| | Module system |
| in | Explicit nullability |
When Converting Code
- Analyze source thoroughly - Understand C++ object lifetimes, RAII patterns, and ownership semantics
- Map types first - Create type equivalence table, especially smart pointers → Rust ownership
- Preserve semantics - Maintain C++'s RAII cleanup guarantees in Rust's ownership system
- Adopt target idioms - Don't write "C++ code in Rust syntax" (avoid unnecessary Rc/Arc)
- Handle edge cases - nullptr checks, exception safety, move semantics, template instantiation
- Test equivalence - Same inputs → same outputs, verify memory safety
- Consider FFI - For large codebases, use cxx crate for gradual migration
Type System Mapping
Primitive Types
| C++ | Rust | Notes |
|---|---|---|
| | Direct mapping |
| | C++ char is 1 byte, not Unicode |
/ / | | Rust char is Unicode scalar value (4 bytes) |
| | Guaranteed 8-bit signed |
| | Guaranteed 16-bit signed |
| | Guaranteed 32-bit signed |
| | Guaranteed 64-bit signed |
| | Guaranteed 8-bit unsigned |
| | Guaranteed 16-bit unsigned |
| | Guaranteed 32-bit unsigned |
| | Guaranteed 64-bit unsigned |
| | Platform-dependent unsigned |
| | Platform-dependent signed |
| | 32-bit floating point |
| | 64-bit floating point |
| - | No direct equivalent; use external crate if needed |
| | Unit type |
String Types
| C++ | Rust | Notes |
|---|---|---|
| | Owned, heap-allocated, UTF-8 enforced |
| | Borrowed string slice for parameters |
| | Move semantics → ownership transfer |
| / | Prefer &str; use raw pointer only for FFI |
| / | Mutable buffer or raw pointer |
(C++17) | | Non-owning string reference |
(C++20) | | Rust String is always UTF-8 |
Collection Types
| C++ | Rust | Notes |
|---|---|---|
| | Growable, owned array |
| / | Borrowed slice for parameters |
| | Fixed-size array on stack |
| | Double-ended queue |
| - | Use Vec<T> or VecDeque<T>; linked lists rare in Rust |
| | Ordered map, K must be Ord |
| | Hash table, K must be Hash + Eq |
| | Ordered set |
| | Hash set |
| | Tuple |
| | Tuple |
(C++20) | / | Non-owning view |
Smart Pointer Types
| C++ | Rust | Notes |
|---|---|---|
| | Single ownership, heap allocation |
| | Owned dynamic array |
| | Reference counting (single-threaded) |
(thread-safe) | | Atomic reference counting (multi-threaded) |
| / | Weak reference (Rc/Arc) |
Raw pointer | / / | Prefer owned/borrowed types; use raw only for FFI |
| | Immutable raw pointer (unsafe) |
(mutable) | | Mutable raw pointer (unsafe) |
Optional and Variant Types
| C++ | Rust | Notes |
|---|---|---|
(C++17) | | Nullable type, compile-time safety |
(nullable) | | Heap-allocated nullable |
(C++17) | | Tagged union, type-safe variant |
(C++17) | - | Use generics or enums; avoid type erasure |
| - | Use generics or trait objects; avoid in safe Rust |
Function Types
| C++ | Rust | Notes |
|---|---|---|
| | Function pointer |
| | Closure trait (or FnMut, FnOnce) |
Lambda | | Closure syntax |
Lambda | with captured refs | Borrow checker enforces safety |
Lambda | | Move closure (takes ownership) |
Composite Types
| C++ | Rust | Notes |
|---|---|---|
| | Similar syntax, fields private by default in Rust modules |
| + | Separate data (struct) from methods (impl) |
| (fieldless) | C-like enum |
(C++11) | | Rust enums are always scoped |
| Tagged union (manual) | with variants | Rust enums are sum types |
| (unsafe) | Avoid; use enums instead |
| Inheritance hierarchy | Composition + traits | Rust favors composition over inheritance |
Idiom Translation
Pattern 1: RAII and Resource Management
C++:
class FileHandle { private: FILE* file; public: FileHandle(const char* path, const char* mode) : file(fopen(path, mode)) { if (!file) { throw std::runtime_error("Failed to open file"); } } ~FileHandle() { if (file) { fclose(file); } } // Delete copy, allow move FileHandle(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete; FileHandle(FileHandle&& other) noexcept : file(other.file) { other.file = nullptr; } FILE* get() { return file; } }; // Usage - automatic cleanup void processFile(const char* filename) { FileHandle file(filename, "r"); // Use file.get() // Automatically closed when function exits }
Rust:
use std::fs::File; use std::io::{self, Read}; struct FileHandle { file: File, } impl FileHandle { fn new(path: &str) -> io::Result<Self> { let file = File::open(path)?; Ok(FileHandle { file }) } } // Drop trait provides automatic cleanup (like C++ destructor) impl Drop for FileHandle { fn drop(&mut self) { // File::drop is called automatically - no manual close needed println!("FileHandle dropped"); } } // Usage - automatic cleanup via Drop fn process_file(filename: &str) -> io::Result<()> { let mut file = FileHandle::new(filename)?; // Use file Ok(()) // Automatically dropped when function exits } // Or more idiomatically, use std::fs::File directly fn process_file_idiomatic(filename: &str) -> io::Result<()> { let file = File::open(filename)?; // Use file Ok(()) // File implements Drop, automatic cleanup }
Why this translation:
- Rust's
trait is analogous to C++ destructors for RAIIDrop - Constructors that can fail use
instead of exceptionsResult<T, E> - Move semantics are default in Rust (no need for
)std::move - Ownership system eliminates need for manual delete copy constructors
- The
operator propagates errors ergonomically?
Pattern 2: Smart Pointers and Ownership
C++:
#include <memory> #include <vector> class Node { public: int value; std::shared_ptr<Node> next; Node(int v) : value(v), next(nullptr) {} }; class LinkedList { private: std::shared_ptr<Node> head; public: void push(int value) { auto new_node = std::make_shared<Node>(value); new_node->next = head; head = new_node; } std::optional<int> pop() { if (!head) { return std::nullopt; } int value = head->value; head = head->next; return value; } };
Rust:
// Avoid shared_ptr pattern in Rust when possible // Prefer Box for single ownership struct Node { value: i32, next: Option<Box<Node>>, } struct LinkedList { head: Option<Box<Node>>, } impl LinkedList { fn new() -> Self { LinkedList { head: None } } fn push(&mut self, value: i32) { let new_node = Box::new(Node { value, next: self.head.take(), // Moves ownership }); self.head = Some(new_node); } fn pop(&mut self) -> Option<i32> { self.head.take().map(|node| { self.head = node.next; node.value }) } } // If shared ownership is truly needed (rare), use Rc use std::rc::Rc; struct SharedNode { value: i32, next: Option<Rc<SharedNode>>, } // Note: Rc creates immutable shared ownership // For interior mutability, use Rc<RefCell<T>>
Why this translation:
- Rust prefers single ownership (
) over shared ownership (Box
/Rc
)Arc
replaces nullable pointersOption<Box<T>>
method moves ownership out of an Option, replacing with None.take()- Shared ownership (Rc/Arc) should be used sparingly in Rust
- Reference counting happens at compile-time via ownership tracking, not runtime
Pattern 3: Templates vs Generics
C++:
template<typename T> class Container { private: std::vector<T> data; public: void add(const T& item) { data.push_back(item); } template<typename Predicate> std::vector<T> filter(Predicate pred) const { std::vector<T> result; for (const auto& item : data) { if (pred(item)) { result.push_back(item); } } return result; } size_t size() const { return data.size(); } }; // Usage Container<int> numbers; numbers.add(1); numbers.add(2); auto evens = numbers.filter([](int x) { return x % 2 == 0; });
Rust:
struct Container<T> { data: Vec<T>, } impl<T> Container<T> { fn new() -> Self { Container { data: Vec::new() } } fn add(&mut self, item: T) { self.data.push(item); } fn size(&self) -> usize { self.data.len() } } // Conditional implementation for types that implement Clone impl<T: Clone> Container<T> { fn filter<F>(&self, pred: F) -> Vec<T> where F: Fn(&T) -> bool, { self.data .iter() .filter(|item| pred(item)) .cloned() .collect() } } // Usage let mut numbers = Container::new(); numbers.add(1); numbers.add(2); let evens = numbers.filter(|x| x % 2 == 0); // More idiomatic: use iterators directly let numbers = vec![1, 2, 3, 4, 5]; let evens: Vec<_> = numbers.iter() .filter(|x| *x % 2 == 0) .copied() .collect();
Why this translation:
- Rust generics require explicit trait bounds (e.g.,
)T: Clone
clause provides cleaner syntax for complex boundswhere- Rust's iterator pattern is more idiomatic than manual collection
- Generic functions use trait bounds instead of template parameter concepts
- No implicit constraints like C++ templates (explicit is better)
Pattern 4: Inheritance vs Composition + Traits
C++:
class Animal { public: virtual void make_sound() const = 0; // Pure virtual virtual ~Animal() = default; }; class Dog : public Animal { public: void make_sound() const override { std::cout << "Woof!\n"; } }; class Cat : public Animal { public: void make_sound() const override { std::cout << "Meow!\n"; } }; void animal_sounds(const std::vector<std::unique_ptr<Animal>>& animals) { for (const auto& animal : animals) { animal->make_sound(); } } int main() { std::vector<std::unique_ptr<Animal>> animals; animals.push_back(std::make_unique<Dog>()); animals.push_back(std::make_unique<Cat>()); animal_sounds(animals); }
Rust:
// Define behavior with a trait (like C++ pure virtual interface) trait Animal { fn make_sound(&self); } // Implement trait for concrete types struct Dog; impl Animal for Dog { fn make_sound(&self) { println!("Woof!"); } } struct Cat; impl Animal for Cat { fn make_sound(&self) { println!("Meow!"); } } // Option 1: Dynamic dispatch with trait objects (like C++ virtual) fn animal_sounds_dyn(animals: &[Box<dyn Animal>]) { for animal in animals { animal.make_sound(); } } // Option 2: Static dispatch with generics (no runtime overhead) fn animal_sounds_generic<A: Animal>(animals: &[A]) { for animal in animals { animal.make_sound(); } } fn main() { // Dynamic dispatch (runtime polymorphism) let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog), Box::new(Cat), ]; animal_sounds_dyn(&animals); // Static dispatch (compile-time polymorphism) let dogs = vec![Dog, Dog]; animal_sounds_generic(&dogs); }
Why this translation:
- Rust uses traits instead of inheritance for polymorphism
provides runtime polymorphism (like C++ virtual functions)dyn Trait- Generic bounds provide zero-cost compile-time polymorphism
- No inheritance hierarchy - composition and traits are preferred
- Trait objects require explicit
orBox<dyn Trait>&dyn Trait
Pattern 5: Exception Handling to Result Types
C++:
#include <stdexcept> #include <string> #include <fstream> class FileError : public std::runtime_error { public: FileError(const std::string& msg) : std::runtime_error(msg) {} }; std::string readFile(const std::string& path) { std::ifstream file(path); if (!file.is_open()) { throw FileError("Failed to open file: " + path); } std::string content; std::string line; while (std::getline(file, line)) { content += line + "\n"; } if (file.bad()) { throw FileError("Error reading file: " + path); } return content; } void processFile(const std::string& path) { try { std::string content = readFile(path); // Process content } catch (const FileError& e) { std::cerr << "File error: " << e.what() << "\n"; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; } }
Rust:
use std::fs; use std::io; use std::path::Path; // Custom error type #[derive(Debug)] enum FileError { Io(io::Error), InvalidContent(String), } impl From<io::Error> for FileError { fn from(err: io::Error) -> Self { FileError::Io(err) } } impl std::fmt::Display for FileError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { FileError::Io(e) => write!(f, "IO error: {}", e), FileError::InvalidContent(msg) => write!(f, "Invalid content: {}", msg), } } } impl std::error::Error for FileError {} fn read_file(path: &Path) -> Result<String, FileError> { // ? operator propagates errors (like C++ exception unwinding) let content = fs::read_to_string(path)?; Ok(content) } fn process_file(path: &Path) { match read_file(path) { Ok(content) => { // Process content println!("Read {} bytes", content.len()); } Err(FileError::Io(e)) => { eprintln!("File error: {}", e); } Err(FileError::InvalidContent(msg)) => { eprintln!("Invalid content: {}", msg); } } } // Or use the ? operator to propagate fn process_file_propagate(path: &Path) -> Result<(), FileError> { let content = read_file(path)?; // Process content Ok(()) }
Why this translation:
- Rust uses
instead of exceptions for recoverable errorsResult<T, E> - The
operator replaces?
for error propagationtry/catch
trait enables automatic error conversion (like exception hierarchies)From- Pattern matching on Result is explicit and type-safe
- Unrecoverable errors use
instead of exceptionspanic!()
Memory & Ownership Translation
C++ RAII vs Rust Ownership
| C++ Pattern | Rust Equivalent | Key Difference |
|---|---|---|
| Constructor acquires resource | Constructor returns | Fallible construction explicit |
| Destructor releases resource | trait | Automatic, deterministic cleanup |
| Copy constructor | trait | Explicit, not automatic |
| Move constructor | Default move semantics | Moves are implicit, borrowing is explicit |
parameter | parameter | Borrow checker enforces lifetime |
parameter | parameter | Takes ownership by default |
| | Single ownership |
| / | Avoid when possible; prefer borrowing |
Smart Pointer Translation Guide
// C++: unique_ptr for single ownership std::unique_ptr<Widget> widget = std::make_unique<Widget>(); use_widget(*widget); // Dereference auto moved = std::move(widget); // Explicit move
// Rust: Box for single ownership let widget = Box::new(Widget::new()); use_widget(&widget); // Automatic deref via Deref trait let moved = widget; // Implicit move (widget no longer usable)
// C++: shared_ptr for shared ownership std::shared_ptr<Data> data = std::make_shared<Data>(); auto copy = data; // Reference count increased
// Rust: Prefer borrowing over shared ownership let data = Data::new(); use_data(&data); // Borrow instead of clone use_data_again(&data); // Can borrow multiple times // Only use Rc if truly needed (shared ownership) use std::rc::Rc; let data = Rc::new(Data::new()); let copy = Rc::clone(&data); // Reference count increased
Lifetime Annotations (No C++ Equivalent)
Rust's borrow checker requires explicit lifetime annotations when relationships aren't clear:
// Rust: Lifetime ensures returned reference is valid fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } // Struct containing references struct Parser<'a> { source: &'a str, position: usize, } impl<'a> Parser<'a> { fn new(source: &'a str) -> Self { Parser { source, position: 0 } } fn current(&self) -> &'a str { &self.source[self.position..] } }
C++ has no equivalent to lifetimes - the compiler doesn't track reference validity at compile time. Rust's lifetimes prevent dangling references that would compile in C++ but cause runtime errors.
FFI & Interoperability (10th Pillar)
For large C++ codebases, gradual migration using FFI is often the best strategy. The
cxx crate provides safe C++/Rust interop.
Why FFI Matters for C++ → Rust
Instead of rewriting everything at once:
- Convert performance-critical modules to Rust first
- Keep stable C++ code as-is during transition
- Test new Rust code against existing C++ test suite
- Gradually replace C++ modules over time
- Roll back easily if issues arise
The cxx Crate
The
cxx crate provides safe, zero-overhead C++ interop:
# Cargo.toml [dependencies] cxx = "1.0" [build-dependencies] cxx-build = "1.0"
Basic FFI Example
C++ side (src/cpp/widget.h):
#pragma once #include <memory> #include <string> class Widget { private: int value_; public: Widget(int value); int get_value() const; void set_value(int value); std::string to_string() const; }; std::unique_ptr<Widget> create_widget(int value);
C++ implementation (src/cpp/widget.cpp):
#include "widget.h" #include <sstream> Widget::Widget(int value) : value_(value) {} int Widget::get_value() const { return value_; } void Widget::set_value(int value) { value_ = value; } std::string Widget::to_string() const { std::ostringstream oss; oss << "Widget(" << value_ << ")"; return oss.str(); } std::unique_ptr<Widget> create_widget(int value) { return std::make_unique<Widget>(value); }
Rust FFI bridge (src/bridge.rs):
#[cxx::bridge] mod ffi { // Shared structs (visible to both C++ and Rust) struct Config { name: String, value: i32, } // C++ types and functions unsafe extern "C++" { include!("myproject/widget.h"); // Opaque C++ type type Widget; // C++ functions fn create_widget(value: i32) -> UniquePtr<Widget>; fn get_value(self: &Widget) -> i32; fn set_value(self: Pin<&mut Widget>, value: i32); fn to_string(self: &Widget) -> String; } // Rust functions callable from C++ extern "Rust" { fn process_widget(widget: &Widget) -> i32; fn create_config(name: String, value: i32) -> Config; } } // Implement Rust functions fn process_widget(widget: &ffi::Widget) -> i32 { let current = widget.get_value(); current * 2 } fn create_config(name: String, value: i32) -> ffi::Config { ffi::Config { name, value } } // Use C++ from Rust pub fn use_cpp_widget() { let widget = ffi::create_widget(42); println!("Widget: {}", widget.to_string()); let doubled = process_widget(&widget); println!("Processed: {}", doubled); }
Build script (build.rs):
fn main() { cxx_build::bridge("src/bridge.rs") .file("src/cpp/widget.cpp") .flag_if_supported("-std=c++17") .compile("myproject-cpp"); println!("cargo:rerun-if-changed=src/bridge.rs"); println!("cargo:rerun-if-changed=src/cpp/widget.h"); println!("cargo:rerun-if-changed=src/cpp/widget.cpp"); }
Data Type Marshalling
| C++ Type | cxx Bridge Type | Rust Type | Notes |
|---|---|---|---|
| | | Direct pass by value |
| | | Copied across boundary |
| | | Zero-copy borrow |
| | | Ownership transfer |
| | | Reference counted |
| | | Shared borrow |
| | | Exclusive borrow |
| | | Copied across boundary |
| | | Zero-copy view |
Gradual Migration Strategy
┌─────────────────────────────────────────────────────────────┐ │ GRADUAL MIGRATION PHASES │ ├─────────────────────────────────────────────────────────────┤ │ Phase 1: SETUP │ │ • Add cxx to Cargo.toml │ │ • Create FFI bridge module │ │ • Set up build.rs to compile C++ code │ │ • Verify C++ and Rust can call each other │ ├─────────────────────────────────────────────────────────────┤ │ Phase 2: IDENTIFY TARGET MODULES │ │ • Find performance bottlenecks (profile C++ code) │ │ • Identify frequently-changing modules (benefit from Rust) │ │ • Map dependencies between modules │ │ • Choose initial module with minimal dependencies │ ├─────────────────────────────────────────────────────────────┤ │ Phase 3: CONVERT FIRST MODULE │ │ • Translate C++ module to Rust │ │ • Expose Rust module via cxx bridge │ │ • Keep C++ interface unchanged (drop-in replacement) │ │ • Test Rust implementation against C++ test suite │ ├─────────────────────────────────────────────────────────────┤ │ Phase 4: INTEGRATION │ │ • Replace C++ module calls with Rust calls │ │ • Run full integration tests │ │ • Monitor for issues (memory leaks, performance) │ │ • Rollback to C++ if needed │ ├─────────────────────────────────────────────────────────────┤ │ Phase 5: ITERATE │ │ • Repeat for next module │ │ • Gradually reduce C++ footprint │ │ • Eventually remove cxx bridge (all Rust) │ └─────────────────────────────────────────────────────────────┘
FFI Best Practices
- Keep FFI boundary thin - Convert types at the boundary, work with native types internally
- Avoid complex types - Prefer simple types (integers, strings) over complex structs
- Handle errors explicitly - C++ exceptions don't cross FFI boundary safely
- Test FFI thoroughly - Memory bugs can occur at language boundaries
- Document ownership - Be clear about who owns data (C++ or Rust)
- Measure overhead - Profile FFI calls if performance-critical
FFI Error Handling
#[cxx::bridge] mod ffi { unsafe extern "C++" { include!("myproject/api.h"); // C++ function that can throw fn risky_operation(value: i32) -> Result<String>; } } // Use from Rust fn call_cpp() -> Result<(), Box<dyn std::error::Error>> { // cxx converts C++ exceptions to Rust Result let result = ffi::risky_operation(42)?; println!("Success: {}", result); Ok(()) }
C++ side:
// C++ function that throws std::string risky_operation(int32_t value) { if (value < 0) { throw std::runtime_error("Negative value not allowed"); } return "Success"; }
Common Pitfalls
1. Overusing Rc/Arc (Avoid C++ shared_ptr Mindset)
Problem: Translating every
shared_ptr to Rc/Arc.
// Bad: Unnecessary shared ownership use std::rc::Rc; struct Node { value: i32, children: Vec<Rc<Node>>, // Over-engineered }
Solution: Prefer borrowing or single ownership:
// Good: Use Box for owned children struct Node { value: i32, children: Vec<Box<Node>>, } // Or borrow when possible fn process_nodes(nodes: &[Node]) { // Work with borrowed references }
2. Fighting the Borrow Checker with Clones
Problem: Cloning everywhere to satisfy the borrow checker.
// Bad: Excessive cloning fn process(data: &Vec<String>) -> Vec<String> { data.clone() // Unnecessary full copy .into_iter() .filter(|s| s.len() > 5) .collect() }
Solution: Use references properly:
// Good: Work with references fn process(data: &[String]) -> Vec<&str> { data.iter() .filter(|s| s.len() > 5) .map(|s| s.as_str()) .collect() } // Or if ownership is needed, be explicit fn process_owned(data: Vec<String>) -> Vec<String> { data.into_iter() .filter(|s| s.len() > 5) .collect() }
3. Null Pointer Mistakes
Problem: Treating
Option<T> like nullable pointers without checking.
// Bad: Unwrapping without checking (panics at runtime) fn get_value(opt: Option<i32>) -> i32 { opt.unwrap() // Panics if None }
Solution: Handle None explicitly:
// Good: Pattern matching fn get_value(opt: Option<i32>) -> i32 { match opt { Some(v) => v, None => 0, // Default value } } // Or use combinators fn get_value(opt: Option<i32>) -> i32 { opt.unwrap_or(0) }
4. Ignoring Lifetime Errors
Problem: Returning references that outlive their source.
// Bad: Compiler error - returning reference to local fn create_string() -> &str { let s = String::from("hello"); &s // Error: s dropped at end of function }
Solution: Return owned data or use proper lifetimes:
// Good: Return owned String fn create_string() -> String { String::from("hello") } // Or if parameter-based: fn first_word(s: &str) -> &str { s.split_whitespace().next().unwrap_or("") }
5. Transliterating C++ Patterns
Problem: Writing "C++ code in Rust syntax" instead of idiomatic Rust.
// Bad: Transliterated C++ style struct Container { data: Vec<i32>, } impl Container { fn get(&self, index: usize) -> Option<i32> { if index < self.data.len() { Some(self.data[index]) } else { None } } }
Solution: Use Rust idioms:
// Good: Idiomatic Rust struct Container { data: Vec<i32>, } impl Container { fn get(&self, index: usize) -> Option<&i32> { self.data.get(index) // Built-in method } } // Or even simpler - use Vec directly fn get_item(data: &[i32], index: usize) -> Option<&i32> { data.get(index) }
6. Manual Iterator Loops
Problem: Using C++-style loops instead of iterators.
// Bad: C++ style loop let mut sum = 0; for i in 0..numbers.len() { sum += numbers[i]; }
Solution: Use iterator methods:
// Good: Idiomatic iterator let sum: i32 = numbers.iter().sum(); // Or for more complex operations let sum: i32 = numbers.iter() .filter(|&&x| x > 0) .sum();
Tooling
| Tool | Purpose | Notes |
|---|---|---|
crate | Safe C++/Rust FFI | Recommended for gradual migration |
| Generate Rust FFI bindings from C++ headers | For C-compatible C++ APIs |
| Generate C/C++ headers from Rust | Expose Rust to C++ |
| Automatically call C++ from Rust | Higher-level than cxx |
crate | Embed C++ directly in Rust | For quick experiments |
| Expand macros and generics | Understand template translation |
| IDE support | Catch lifetime/borrow errors early |
| Linter | Suggests idiomatic Rust patterns |
Examples
Example 1: Simple - String Processing
Before (C++):
#include <string> #include <algorithm> std::string to_uppercase(const std::string& input) { std::string result = input; std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c) { return std::toupper(c); }); return result; } int main() { std::string text = "hello world"; std::string upper = to_uppercase(text); // text is still valid (copied) }
After (Rust):
fn to_uppercase(input: &str) -> String { input.to_uppercase() } fn main() { let text = "hello world"; let upper = to_uppercase(&text); // text is still valid (borrowed, not moved) }
Example 2: Medium - Optional Values and Error Handling
Before (C++):
#include <optional> #include <stdexcept> #include <map> class UserDatabase { private: std::map<int, std::string> users; public: void add_user(int id, const std::string& name) { if (users.count(id) > 0) { throw std::runtime_error("User already exists"); } users[id] = name; } std::optional<std::string> get_user(int id) const { auto it = users.find(id); if (it != users.end()) { return it->second; } return std::nullopt; } bool remove_user(int id) { return users.erase(id) > 0; } }; int main() { UserDatabase db; try { db.add_user(1, "Alice"); auto user = db.get_user(1); if (user) { std::cout << "Found: " << *user << "\n"; } } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; } }
After (Rust):
use std::collections::HashMap; #[derive(Debug)] enum DbError { UserExists, } struct UserDatabase { users: HashMap<i32, String>, } impl UserDatabase { fn new() -> Self { UserDatabase { users: HashMap::new(), } } fn add_user(&mut self, id: i32, name: String) -> Result<(), DbError> { if self.users.contains_key(&id) { return Err(DbError::UserExists); } self.users.insert(id, name); Ok(()) } fn get_user(&self, id: i32) -> Option<&String> { self.users.get(&id) } fn remove_user(&mut self, id: i32) -> bool { self.users.remove(&id).is_some() } } fn main() { let mut db = UserDatabase::new(); match db.add_user(1, String::from("Alice")) { Ok(()) => { if let Some(user) = db.get_user(1) { println!("Found: {}", user); } } Err(DbError::UserExists) => { eprintln!("Error: User already exists"); } } }
Example 3: Complex - Polymorphism with Smart Pointers
Before (C++):
#include <memory> #include <vector> #include <iostream> class Shape { public: virtual double area() const = 0; virtual void describe() const = 0; virtual ~Shape() = default; }; class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } void describe() const override { std::cout << "Circle with radius " << radius << "\n"; } }; class Rectangle : public Shape { private: double width, height; public: Rectangle(double w, double h) : width(w), height(h) {} double area() const override { return width * height; } void describe() const override { std::cout << "Rectangle " << width << "x" << height << "\n"; } }; class ShapeCollection { private: std::vector<std::unique_ptr<Shape>> shapes; public: void add_shape(std::unique_ptr<Shape> shape) { shapes.push_back(std::move(shape)); } double total_area() const { double total = 0; for (const auto& shape : shapes) { total += shape->area(); } return total; } void describe_all() const { for (const auto& shape : shapes) { shape->describe(); std::cout << " Area: " << shape->area() << "\n"; } } }; int main() { ShapeCollection collection; collection.add_shape(std::make_unique<Circle>(5.0)); collection.add_shape(std::make_unique<Rectangle>(4.0, 6.0)); collection.describe_all(); std::cout << "Total area: " << collection.total_area() << "\n"; }
After (Rust):
// Define trait (like C++ abstract base class) trait Shape { fn area(&self) -> f64; fn describe(&self) -> String; } // Concrete implementations struct Circle { radius: f64, } impl Circle { fn new(radius: f64) -> Self { Circle { radius } } } impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.radius * self.radius } fn describe(&self) -> String { format!("Circle with radius {}", self.radius) } } struct Rectangle { width: f64, height: f64, } impl Rectangle { fn new(width: f64, height: f64) -> Self { Rectangle { width, height } } } impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn describe(&self) -> String { format!("Rectangle {}x{}", self.width, self.height) } } // Collection using trait objects (dynamic dispatch) struct ShapeCollection { shapes: Vec<Box<dyn Shape>>, } impl ShapeCollection { fn new() -> Self { ShapeCollection { shapes: Vec::new() } } fn add_shape(&mut self, shape: Box<dyn Shape>) { self.shapes.push(shape); } fn total_area(&self) -> f64 { self.shapes.iter().map(|s| s.area()).sum() } fn describe_all(&self) { for shape in &self.shapes { println!("{}", shape.describe()); println!(" Area: {}", shape.area()); } } } fn main() { let mut collection = ShapeCollection::new(); collection.add_shape(Box::new(Circle::new(5.0))); collection.add_shape(Box::new(Rectangle::new(4.0, 6.0))); collection.describe_all(); println!("Total area: {}", collection.total_area()); }
Limitations
This skill has limited coverage in some areas due to gaps in the foundation skills:
Coverage Gaps
| Pillar | lang-cpp-dev | lang-rust-dev | Mitigation |
|---|---|---|---|
| Module System | ~ | ✓ | C++ namespaces → Rust modules documented via web research |
| Error Handling | ~ | ✓ | C++ exception patterns researched from cppreference.com |
| Serialization | ✗ | ✓ | Common C++ serialization libraries researched |
| FFI | ~ | ~ | Extended via meta-convert-dev FFI pillar and cxx crate docs |
Known Limitations
- C++ Module System: C++20 modules are new; this skill focuses on namespace translation
- Advanced Metaprogramming: SFINAE, CRTP, and template metaprogramming require case-by-case analysis
- Coroutines: C++20 coroutines have no direct Rust equivalent; use async/await patterns
External Resources Used
| Resource | What It Provided | Reliability |
|---|---|---|
| cxx crate docs | FFI patterns and examples | High (official) |
| cppreference.com | C++ exception model | High (community standard) |
| Rust Book | Ownership patterns | High (official) |
See Also
- Foundational patterns (APTV workflow, FFI pillar, testing strategies)meta-convert-dev
- C++ development patternslang-cpp-dev
- Rust development patternslang-rust-dev
- Similar modern language → Rust conversionconvert-golang-rust
Cross-cutting pattern skills:
- Async, threads, channels across languagespatterns-concurrency-dev
- JSON, validation, struct tags across languagespatterns-serialization-dev
- Templates, macros, generics across languagespatterns-metaprogramming-dev