RustyCpp
RustyCpp Design Document & User Manual
A Rust-style borrow checker for C++ code.
Table of Contents
- Part I: Introduction
- Part II: Core Concepts
- Part III: Safety Annotations
- Part IV: The Rusty Type Library
- Part V: Analysis Features
- Part VI: Integration
- Part VII: Reference
- Part VIII: Advanced Topics
- Part IX: Future Roadmap
Part I: Introduction
1. Overview
What is RustyCpp?
RustyCpp is a static analyzer that applies Rust’s ownership and borrowing rules to C++ code. It catches memory safety issues at compile-time without runtime overhead.
Key Features:
- Detects use-after-move errors
- Prevents double mutable borrows
- Tracks reference lifetimes
- Catches dangling pointer/reference bugs
- Zero runtime cost (pure static analysis)
Motivation: Memory Safety in C++
C++ provides powerful low-level control but lacks compile-time memory safety guarantees. After 15+ years of systems programming in C++, the most troubling failures remain memory-related: segmentation faults, memory corruptions, dangling pointers, and use-after-free bugs. These issues cause sleepless nights and can take months to diagnose.
Common memory safety bugs include:
- Use-after-free: Accessing memory after it’s been deallocated
- Double-free: Freeing the same memory twice
- Dangling references: References to destroyed objects
- Iterator invalidation: Using iterators after container modification
- Data races: Concurrent mutable access to shared data
Rust solves these problems through its ownership system, enforced at compile time. However, rewriting existing C++ codebases in Rust is often impractical. RustyCpp brings Rust’s safety guarantees to C++ through static analysis and opt-in safety annotations—without requiring you to leave the C++ ecosystem.
Why Not Other Approaches?
Several approaches to C++ memory safety have been tried:
Interop with Rust: While some hope for seamless C++/Rust interoperability (like C++ has with C), deep integration between the two languages remains unlikely in the near term.
Macro-based solutions: Google engineers explored using C++’s macro system to track borrows at compile time. This approach proved impossible due to C++ language limitations.
Circle C++ / Safe C++: The Circle compiler (i.e., the Safe C++ proposal) implements Rust-style borrow checking with new C++ syntax. While technically impressive, it introduces breaking syntax changes and thus requires a new compiler.
RustyCpp’s approach: Rather than modifying the language or compiler, RustyCpp is a static analyzer that works with standard C++. It uses comment-based annotations (@safe, @unsafe) that are invisible to compilers, enabling gradual adoption without breaking existing code.
How It Works (High-Level Architecture)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ C++ Source │────▶│ LibClang │────▶│ RustyCpp IR │
│ with @safe │ │ Parser │ │ │
└─────────────────┘ └─────────────────┘ └────────┬────────┘
│
┌─────────────────┐ │
│ Violations │◀─────────────┤
│ Report │ │
└─────────────────┘ ┌────────▼────────┐
│ Analysis │
│ - Borrow Check│
│ - Move Check │
│ - Lifetime │
└─────────────────┘
- Parse: LibClang parses C++ source into an AST
- Transform: AST is converted to RustyCpp’s IR (Intermediate Representation)
- Analyze: Multiple analysis passes check for violations
- Report: Clear error messages with locations
2. Getting Started
Installation & Build Requirements
Prerequisites:
- Rust 1.70+ (for building the checker)
- LLVM/Clang 16+ (for LibClang)
- Z3 Solver (for constraint solving)
Build from source:
# Clone the repository
git clone <repository-url>
cd rusty-cpp
# Set environment variables
# macOS:
export Z3_SYS_Z3_HEADER=/opt/homebrew/include/z3.h
# Linux:
export Z3_SYS_Z3_HEADER=/usr/include/z3.h
# Build release binary
cargo build --release
# Binary is at: target/release/rusty-cpp-checker
Quick Start Example
Create a file example.cpp:
#include <rusty/box.hpp>
// @safe
void good_example() {
auto ptr = rusty::Box<int>::make(42);
int value = *ptr; // OK: ptr is valid
}
// @safe
void bad_example() {
auto ptr = rusty::Box<int>::make(42);
auto ptr2 = std::move(ptr); // ptr is moved
int value = *ptr; // ERROR: use after move
}
Running the Checker
# Basic usage
./rusty-cpp-checker example.cpp
# With include paths
./rusty-cpp-checker example.cpp -I include -I /usr/local/include
# With compile_commands.json
./rusty-cpp-checker example.cpp --compile-commands build/compile_commands.json
Understanding Output
Rusty C++ Checker
Analyzing: example.cpp
Auto-detected 7 C++ include path(s)
✗ Found 1 violation(s) in example.cpp:
In function 'bad_example': Use after move: variable 'ptr' was moved at line 12 and used at line 13
The output includes:
- Function name where the violation occurred
- Type of violation
- Line numbers for both the problematic operation and related context
Part II: Core Concepts
3. Ownership Model
Single Ownership Principle
In RustyCpp, every value has exactly one owner at any given time. When the owner goes out of scope, the value is destroyed.
// @safe
void ownership_example() {
auto box = rusty::Box<int>::make(42); // box owns the int
// box is the sole owner
} // box goes out of scope, int is destroyed
The Fundamental Difference: C++ vs Rust References
Understanding how RustyCpp bridges C++ and Rust reference models is key to using the tool effectively.
C++ References: Aliases
In C++, references are aliases - they are not first-class types. A reference is just another name for an existing object:
int x = 42;
int& ref = x; // ref is an alias for x
int y = ref; // Same as: int y = x;
ref = 100; // Same as: x = 100;
Key implications:
- You cannot “move” a reference -
std::move(ref)moves the underlying objectx - References cannot be null or reseated
- References don’t have their own identity separate from what they reference
Rust References: First-Class Types
In Rust, references are first-class types with their own ownership semantics:
let x = 42;
let r1: &mut i32 = &mut x; // r1 is a mutable reference
let r2 = r1; // r1 is MOVED to r2, r1 is now invalid
// println!("{}", r1); // ERROR: borrow of moved value
println!("{}", r2); // OK: r2 owns the reference now
Key implications:
&mut T(mutable reference) is not Copy - assigning moves it&T(immutable reference) is Copy - assigning copies it- References have their own lifetime and can be invalidated
RustyCpp’s Bridge
RustyCpp bridges this gap by:
- Tracking reference variables as first-class entities
- Applying Rust semantics to reference assignments at analysis time
- Providing
rusty::movefor explicit Rust-like reference moves - Forbidding
std::moveon references in@safecode
Move Semantics
Ownership can be transferred through moves:
// @safe
void move_example() {
auto box1 = rusty::Box<int>::make(42);
auto box2 = std::move(box1); // Ownership transferred to box2
// box1 is now in a moved-from state
}
Reference Assignment Semantics
RustyCpp automatically applies Rust-like semantics to reference assignments:
| C++ Reference Type | Rust Equivalent | Assignment Behavior |
|---|---|---|
T& (non-const) |
&mut T |
Move - original becomes invalid |
const T& |
&T |
Copy - both remain valid |
T&& (named) |
&mut T |
Move - original becomes invalid |
Mutable references move:
// @safe
void mutable_ref_moves() {
int x = 42;
int& r1 = x; // r1 borrows x mutably
int& r2 = r1; // r1 is MOVED to r2 (Rust-like)
int y = r2; // OK: r2 is valid
int z = r1; // ERROR: r1 has been moved
}
Const references copy:
// @safe
void const_ref_copies() {
int x = 42;
const int& r1 = x; // r1 borrows x immutably
const int& r2 = r1; // r1 is COPIED to r2
const int& r3 = r1; // r1 can be copied multiple times
int a = r1; // OK: all are still valid
int b = r2; // OK
int c = r3; // OK
}
std::move vs rusty::move
| Type | std::move |
rusty::move |
|---|---|---|
Value T |
Moves the value | Moves the value (same) |
Mutable ref T& |
Moves underlying object | Moves the reference |
Rvalue ref T&& |
Moves underlying object | Moves the reference |
Const ref const T& |
Returns rvalue ref | Compile error |
rusty::move provides Rust-like semantics for references:
// @safe
void rusty_move_example() {
int x = 42;
int& ref = x;
// rusty::move on a mutable reference invalidates it
int& ref2 = rusty::move(ref);
// ref is now invalid, ref2 is the active borrow
}
std::move Restrictions in @safe Code
In @safe code, using std::move on references is forbidden because it has confusing semantics - it moves the underlying object, not the reference:
// The problem:
void problematic() {
std::unique_ptr<int> ptr(new int(42));
std::unique_ptr<int>& ref = ptr;
// std::move(ref) moves *ptr*, not ref
// ref still looks valid but points to a moved-from object
std::unique_ptr<int> ptr2 = std::move(ref);
// No compile error, but undefined behavior territory
}
| Expression | In @safe code |
|---|---|
std::move(value) |
Allowed |
std::move(reference) |
Forbidden |
rusty::move(value) |
Allowed |
rusty::move(reference) |
Allowed |
Use-After-Move Detection
RustyCpp detects when a moved-from variable is used:
// @safe
void use_after_move() {
auto ptr = rusty::Box<int>::make(42);
auto ptr2 = std::move(ptr);
*ptr; // ERROR: Use after move
}
Reassignment Recovery
Unlike true relocation, moved variables can become valid again through reassignment:
// @safe
void recovery() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box); // box is "moved"
box = rusty::Box<int>::make(100); // box is valid again
*box; // OK
}
4. Borrowing Rules
Immutable vs Mutable Borrows
- Immutable borrow (
const T&): Read-only access, multiple allowed - Mutable borrow (
T&): Read-write access, only one allowed
// @safe
void borrow_example() {
int x = 42;
// Multiple immutable borrows: OK
const int& r1 = x;
const int& r2 = x;
// Single mutable borrow: OK
int& m1 = x;
// But not both at once...
}
The Exclusivity Rule
You can have either:
- One or more immutable borrows (
&T), OR - Exactly one mutable borrow (
&mut T)
But never both simultaneously:
// @safe
void exclusivity_violation() {
int x = 42;
int& mut_ref = x; // Mutable borrow
const int& ref = x; // ERROR: Cannot borrow immutably while mutably borrowed
}
Partial Borrows (Struct Fields)
Like Rust, RustyCpp can track borrows at the individual field level rather than treating structs as atomic units. Different fields of a struct can be borrowed independently:
struct Point {
int x;
int y;
};
// @safe
void partial_borrow() {
Point p{1, 2};
int& x_ref = p.x; // Borrow p.x
int& y_ref = p.y; // OK: p.y is independent
int& x_ref2 = p.x; // ERROR: p.x already borrowed
}
Conflict Detection
Same field cannot be borrowed mutably twice:
// @safe
void bad_double_mutable_borrow() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
std::string& r1 = p.first;
std::string& r2 = p.first; // ERROR: field 'p.first' already mutably borrowed
}
Mixed Mutable/Immutable Conflicts
Mutable and immutable borrows of the same field conflict:
// @safe
void bad_mixed_borrow() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
const std::string& r1 = p.first; // Immutable borrow
std::string& r2 = p.first; // ERROR: cannot borrow mutably while immutably borrowed
}
Multiple Immutable Borrows Allowed
Multiple immutable borrows of the same field are permitted:
// @safe
void multiple_immutable() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
const std::string& r1 = p.first;
const std::string& r2 = p.first; // OK: multiple immutable borrows allowed
const std::string& r3 = p.first; // OK
}
Whole-Struct vs Field Borrow Conflicts
Cannot borrow the whole struct while fields are borrowed, and vice versa:
// @safe
void whole_vs_field() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
std::string& r = p.first; // Borrow field
Pair& q = p; // ERROR: cannot borrow 'p' while 'p.first' is borrowed
}
// @safe
void field_vs_whole() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
Pair& q = p; // Borrow whole struct
std::string& r = p.first; // ERROR: cannot borrow field while 'p' is borrowed
}
Nested Field Borrows
RustyCpp supports arbitrarily nested field paths:
struct Inner { std::string data; int count; };
struct Outer { Inner inner; std::string name; };
// @safe
void nested_borrows() {
Outer o;
std::string& r1 = o.inner.data; // Borrow nested field
int& r2 = o.inner.count; // OK: different field
std::string& r3 = o.name; // OK: different top-level field
std::string& r4 = o.inner.data; // ERROR: already borrowed
}
Partial Moves
RustyCpp also tracks moves at the individual field level:
// @safe
void partial_move_example() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
std::string a = std::move(p.first); // Move only p.first
std::string b = p.second; // OK: p.second not moved
std::string c = std::move(p.first); // ERROR: field 'p.first' already moved
}
Whole-Struct Move After Partial Move
Cannot move the entire struct after a partial move:
// @safe
void bad_whole_after_partial() {
struct Pair { std::string first; std::string second; };
Pair p{"hello", "world"};
std::string a = std::move(p.first); // Partial move
Pair q = std::move(p); // ERROR: Cannot move 'p' because partially moved
}
Transitive Borrows
Borrows form chains that are tracked recursively:
// @safe
void transitive_borrow() {
int x = 42;
int& ref1 = x; // ref1 borrows x
int& ref2 = ref1; // ref2 borrows ref1 (transitively borrows x)
x = 100; // ERROR: Cannot modify x while borrowed through ref1 -> ref2
}
RustyCpp can handle borrow chains of any depth:
// @safe
void long_chain() {
int x = 42;
int& r1 = x;
int& r2 = r1; // r1 moved to r2
int& r3 = r2; // r2 moved to r3
int& r4 = r3; // r3 moved to r4
int y = r4; // OK: only r4 is valid
int z = r1; // ERROR: r1 was moved
}
Error messages show the complete borrow chain:
ERROR: Cannot move 'x' because it is borrowed
Borrow chain: ref3 -> ref2 -> ref1 -> x
5. Lifetimes
Scope-Based Lifetime Tracking
Every variable has a lifetime determined by its scope. References must not outlive the data they refer to:
// @safe
void lifetime_example() {
std::string outer;
{
std::string inner = "hello";
outer = inner; // OK: copies the value
} // inner's lifetime ends here
use(outer); // OK: outer has its own copy
}
// @safe
void bad_lifetime_example() {
const std::string& ref = get_temporary(); // ref binds to temporary
use(ref); // ERROR: temporary has been destroyed
}
Dangling Reference Detection
RustyCpp catches references that outlive their referents:
// @safe
int& dangling_reference() {
int x = 42;
return x; // ERROR: Returning reference to local variable
}
// @safe
const std::string& get_name() {
std::string name = "hello";
return name; // ERROR: Returning reference to local
}
Lifetime Annotations
For complex lifetime relationships, use @lifetime annotations:
// @lifetime: (&'a, &'b) -> &'a
// Returns a reference with the same lifetime as the first parameter
const int& first(const int& a, const int& b) {
return a;
}
// @lifetime: (&'a) -> &'a
// The returned reference lives as long as the input
const std::string& identity(const std::string& s) {
return s;
}
Cross-Function Lifetime Checking
The checker validates that lifetime annotations are respected:
// @safe
void cross_function_example() {
const int& ref = identity(42); // ERROR: 42 is a temporary
// identity returns &'a where 'a is the input's lifetime
// But 42's lifetime ends at the semicolon
}
Important: Unlike the Rust compiler, RustyCpp does not have strong lifetime inference capabilities. Cross-function lifetime checking relies on
@lifetimeannotations that you provide. Without annotations, RustyCpp cannot deduce how parameter lifetimes relate to return lifetimes. Always annotate functions that return references to ensure proper checking.
Part III: Safety Annotations
6. The Safety Annotation System
Two-State Model: @safe vs @unsafe
RustyCpp uses a simple two-state model:
| State | Meaning | Checked? |
|---|---|---|
@safe |
Code follows Rust’s safety rules | Yes |
@unsafe |
Code does unsafe things intentionally | No |
| (none) | Third-party/legacy code | No |
// @safe
void checked_function() {
// All borrow rules enforced here
}
// @unsafe
void unchecked_function() {
// No checking - you're on your own
}
void legacy_function() {
// No annotation = not checked (assumed third-party)
}
Calling Rules Matrix
| Caller → Can Call | @safe | @unsafe |
|---|---|---|
| @safe | ✅ Yes | ❌ No (use @unsafe block) |
| @unsafe | ✅ Yes | ✅ Yes |
Key Insight: This is a clean two-state model. To call unsafe code from @safe functions, use an @unsafe { } block.
Annotation Syntax
Annotations attach to the next code element only:
// @safe - Apply to next element only
void safe_function() {
// ✅ CAN call other @safe functions
safe_helper();
// ❌ CANNOT call @unsafe functions directly
// unsafe_func(); // ERROR
// ✅ CAN call @unsafe via @unsafe block
// @unsafe
{
unsafe_func(); // OK: in @unsafe block
std::vector<int> vec; // OK: STL in @unsafe block
}
}
// @unsafe - Apply to next element only (or no annotation = same)
void unsafe_function() {
// ✅ Can call anything
safe_function(); // OK
another_unsafe(); // OK
std::vector<int> vec; // OK
}
// No annotation = @unsafe by default
void legacy_function() {
// Treated as @unsafe
// ✅ Can call anything
std::vector<int> vec; // OK
}
Annotation Suffixes
Annotations support any suffix for documentation purposes:
// @safe-verified on 2025-01-17
void audited_function() { }
// @unsafe: calls legacy code
void wrapper_function() { }
// @safe, reviewed by security team
void reviewed_function() { }
Annotation Hierarchy
Annotations cascade from outer to inner scopes:
Namespace → Class → Function
Inner annotations override outer ones:
// @safe
namespace myapp {
// All functions in this namespace---this file only---are @safe by default
void safe_by_inheritance() { } // @safe (from namespace)
// @unsafe
void explicitly_unsafe() { } // @unsafe (overrides namespace)
// @unsafe
class LegacyWrapper {
void unsafe_method() { } // @unsafe (from class)
// @safe
void safe_method() { } // @safe (overrides class)
};
}
Header-to-Implementation Propagation
Safety annotations in headers automatically apply to implementations:
// === math.h ===
// @safe
int calculate(int a, int b);
// @unsafe
void process_raw_memory(void* ptr);
// === math.cpp ===
#include "math.h"
int calculate(int a, int b) {
// Automatically @safe from header
return a + b;
}
void process_raw_memory(void* ptr) {
// Automatically @unsafe from header
// Pointer operations allowed
}
@unsafe Blocks for Escape Hatches
Within a @safe function, use @unsafe blocks for specific unsafe operations:
// @safe
void mostly_safe() {
int x = 42;
// @unsafe
{
// This block can call unsafe functions, use raw pointers, etc.
legacy_c_function(&x);
std::vector<int> vec; // STL is @unsafe
vec.push_back(x);
}
// Back to safe code
int y = x + 1;
}
Note: The function is still checked as @safe. The @unsafe block only allows calling unsafe functions within it.
Per-File Namespace Scope
Namespace annotations are per-file, not global:
// === file1.cpp ===
// @safe
namespace myapp {
void func1() { } // @safe
}
// === file2.cpp ===
// @unsafe
namespace myapp {
void func2() { } // @unsafe - different file, different annotation
}
This enables gradual migration: annotate files independently.
STL and External Code
All STL and external functions are @unsafe by default. To use them in @safe code:
Option 1: Use @unsafe blocks
// @safe
void use_stl() {
// @unsafe
{
std::vector<int> vec = {1, 2, 3};
vec.push_back(4); // OK in unsafe block
}
}
Option 2: Use Rusty structures (recommended)
// @safe
void use_rusty() {
rusty::Vec<int> vec = {1, 2, 3};
vec.push_back(4); // No unsafe block needed
}
Option 3: External annotations for audited functions
// @external: {
// my_audited_function: [safe, () -> void]
// }
void my_audited_function();
// @safe
void caller() {
my_audited_function(); // OK: marked [safe] via external annotation
}
7. What Gets Checked
Summary Table
| Code Type | Borrow Checking | Move Checking | Lifetime Checking |
|---|---|---|---|
@safe functions |
Yes | Yes | Yes |
@unsafe functions |
No | No | No |
| Unannotated functions | No | No | No |
@unsafe blocks in @safe |
No | No | No |
@unsafe blocks are escape hatches - all safety checking is skipped within them, even inside @safe functions.
@safe Functions: Full Analysis
All analysis passes run on @safe code:
// @safe
void fully_checked() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box);
*box; // ERROR: Use after move (detected)
}
@unsafe Functions: Skipped
@unsafe functions are completely skipped:
// @unsafe
void not_checked() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box);
*box; // No error reported - function is @unsafe
}
Unannotated Code: Skipped
Third-party headers and legacy code without annotations are not checked:
#include <yaml-cpp/yaml.h> // No annotations
// @safe
void my_function() {
YAML::Node node; // yaml-cpp internals not checked
node["key"] = "value"; // Only my_function is checked
}
Part IV: The Rusty Type Library
8. Smart Pointers
rusty::Box<T> (Unique Ownership)
Equivalent to Rust’s Box<T> or C++’s std::unique_ptr<T>:
#include <rusty/box.hpp>
// @safe
void box_example() {
// Create a boxed value
auto box = rusty::Box<int>::make(42);
// Dereference
int value = *box;
// Move ownership
auto box2 = std::move(box);
// box is now invalid
}
rusty::Arc<T> (Thread-Safe Shared Ownership)
Equivalent to Rust’s Arc<T> or C++’s std::shared_ptr<T> with atomic reference counting:
#include <rusty/arc.hpp>
// @safe
void arc_example() {
auto arc1 = rusty::Arc<int>::make(42);
auto arc2 = arc1; // Clones the Arc (increments ref count)
// Both arc1 and arc2 point to the same data
// Data is freed when last Arc is destroyed
}
rusty::Rc<T> (Single-Thread Shared Ownership)
Equivalent to Rust’s Rc<T> - like Arc but not thread-safe (faster):
#include <rusty/rc.hpp>
// @safe
void rc_example() {
auto rc1 = rusty::Rc<int>::make(42);
auto rc2 = rc1; // Clones the Rc
// NOT thread-safe - use Arc for multithreaded code
}
Note: For raw pointer operations, see Section 24: Raw Pointer Safety in Part VIII.
9. Interior Mutability
Interior mutability allows mutation through shared references (const&), enabling patterns that would otherwise require mutable references.
When to Use Which Type
| Type | Thread Safe | Borrow Checking | Best For |
|---|---|---|---|
Cell<T> |
❌ No | None | Small Copy types (int, bool, enum) |
RefCell<T> |
❌ No | Runtime | Complex types needing borrow guards |
UnsafeCell<T> |
❌ No | None | Building custom abstractions |
rusty::Cell<T> (Copy Types)
Allows mutation through shared references for Copy types. Zero overhead - just stores the value directly.
#include <rusty/cell.hpp>
// @safe
void cell_example() {
rusty::Cell<int> cell(42);
int value = cell.get(); // Returns a copy
cell.set(100); // Mutates through shared ref
int old = cell.replace(200); // Replace and get old value
}
Constraints: T must be trivially copyable (std::is_trivially_copyable_v<T>).
Complete Cell API:
| Method | Description |
|---|---|
get() |
Returns a copy of the value |
set(val) |
Sets a new value |
replace(val) |
Sets new value, returns old |
swap(other_cell) |
Swaps values with another Cell |
take() |
Returns value, leaves default in place |
update(f) |
Applies function f to value in-place |
get_mut() |
Returns raw pointer (@unsafe) |
// @safe
void cell_advanced() {
rusty::Cell<int> cell(42);
// Update in-place with function
cell.update([](int x) { return x * 2; }); // cell is now 84
// Swap two cells
rusty::Cell<int> other(10);
cell.swap(other); // cell=10, other=84
// Take value (for default-constructible types)
int taken = cell.take(); // cell is now 0
}
rusty::RefCell<T> (Runtime Borrow Checking)
Allows borrowing with runtime checks. Returns RAII guards (Ref<T>, RefMut<T>) that track borrows.
#include <rusty/refcell.hpp>
// @safe
void refcell_example() {
rusty::RefCell<std::string> cell("hello");
{
auto borrow = cell.borrow(); // Immutable borrow (Ref<T>)
std::cout << *borrow << std::endl;
} // borrow released here
{
auto mut_borrow = cell.borrow_mut(); // Mutable borrow (RefMut<T>)
*mut_borrow = "world";
} // mut_borrow released here
// Panics at runtime if borrow rules violated:
// auto b1 = cell.borrow_mut();
// auto b2 = cell.borrow_mut(); // PANIC: already mutably borrowed
}
RefCell Borrow Rules:
- Multiple
borrow()calls allowed simultaneously - Only one
borrow_mut()allowed at a time - Cannot have
borrow()andborrow_mut()simultaneously - Violations throw
std::runtime_error
Complete RefCell API:
| Method | Returns | Description |
|---|---|---|
borrow() |
Ref<T> |
Immutable borrow guard |
borrow_mut() |
RefMut<T> |
Mutable borrow guard |
can_borrow() |
bool |
Check if immutable borrow possible |
can_borrow_mut() |
bool |
Check if mutable borrow possible |
replace(val) |
T |
Replace value (must not be borrowed) |
swap(other) |
void |
Swap with another RefCell |
take() |
T |
Take value, leave default |
get() |
T |
Get copy (for copyable types) |
Borrow Guards:
// Ref<T> - immutable borrow guard
{
rusty::Ref<std::string> guard = cell.borrow();
const std::string& value = *guard; // operator*
size_t len = guard->length(); // operator->
std::string copy = guard.clone(); // explicit copy
} // guard destroyed, borrow released
// RefMut<T> - mutable borrow guard
{
rusty::RefMut<std::string> guard = cell.borrow_mut();
std::string& value = *guard; // mutable reference
guard->append(" world"); // modify through ->
} // guard destroyed, borrow released
rusty::UnsafeCell<T> (Primitive)
The building block for interior mutability. No safety guarantees - you must ensure correctness manually.
#include <rusty/unsafe_cell.hpp>
// @unsafe
void unsafe_cell_example() {
rusty::UnsafeCell<int> cell(42);
int* ptr = cell.get(); // @unsafe - returns raw pointer
*ptr = 100;
// Caller must ensure no data races or aliasing violations
}
UnsafeCell Methods:
| Method | Safety | Description |
|---|---|---|
get() |
@unsafe | Returns T* to inner value |
get_const() |
@safe | Returns const T* for reading |
get_mut() |
@safe | Returns T& (requires non-const method) |
as_mut_unchecked() |
@unsafe | Returns T& through shared access |
as_ref_unchecked() |
@unsafe | Returns const T& through shared access |
replace(val) |
@unsafe | Replace value, return old |
Note: UnsafeCell is typically used to build other abstractions like Cell and RefCell. Most code should use those higher-level types instead.
Using Interior Mutability in Classes
class Counter {
rusty::Cell<int> count; // Cell for trivially copyable types
public:
Counter() : count(0) {}
// @safe - Can mutate through const reference
void increment() const {
count.set(count.get() + 1);
}
// @safe - Read-only access
int get() const {
return count.get();
}
};
class Cache {
rusty::RefCell<std::map<int, std::string>> data;
public:
Cache() : data() {}
// @safe - Insert with mutable borrow
void insert(int key, std::string value) const {
auto guard = data.borrow_mut();
(*guard)[key] = std::move(value);
}
// @safe - Lookup with immutable borrow
bool contains(int key) const {
auto guard = data.borrow();
return guard->count(key) > 0;
}
};
10. Optional & Error Types
rusty::Option<T>
Represents an optional value:
#include <rusty/option.hpp>
// @safe
void option_example() {
rusty::Option<int> some = rusty::Some(42);
rusty::Option<int> none = rusty::None;
if (some.is_some()) {
int value = some.unwrap(); // Panics if None
}
int value = some.unwrap_or(0); // Returns 0 if None
// Pattern matching style
some.match(
[](int v) { std::cout << "Got: " << v << std::endl; },
[]() { std::cout << "Got nothing" << std::endl; }
);
}
rusty::Result<T, E>
Represents either success or error:
#include <rusty/result.hpp>
// @safe
rusty::Result<int, std::string> divide(int a, int b) {
if (b == 0) {
return rusty::Err<std::string>("division by zero");
}
return rusty::Ok(a / b);
}
// @safe
void result_example() {
auto result = divide(10, 2);
if (result.is_ok()) {
int value = result.unwrap();
}
int value = result.unwrap_or(0);
// Propagate errors
// auto value = result?; // Not available in C++, use unwrap_or/match
}
11. Function Pointers
rusty::SafeFn<Sig> / rusty::UnsafeFn<Sig>
Type-safe function pointer wrappers:
#include <rusty/fn.hpp>
// @safe
int safe_add(int a, int b) { return a + b; }
// @unsafe
int unsafe_add(int a, int b) { return a + b; }
// @safe
void fn_example() {
// SafeFn can only hold @safe functions
rusty::SafeFn<int(int, int)> safe_fn = &safe_add;
int result = safe_fn(1, 2); // OK: safe to call
// UnsafeFn can hold any function
rusty::UnsafeFn<int(int, int)> unsafe_fn = &unsafe_add;
// unsafe_fn(1, 2); // ERROR: requires @unsafe context
// @unsafe
{
int result = unsafe_fn.call_unsafe(1, 2); // OK in @unsafe block
}
}
rusty::SafeMemFn<Sig> / rusty::UnsafeMemFn<Sig>
For member function pointers:
class Calculator {
public:
// @safe
int add(int a, int b) { return a + b; }
};
// @safe
void mem_fn_example() {
rusty::SafeMemFn<int(Calculator::*)(int, int)> mem_fn = &Calculator::add;
Calculator calc;
int result = (calc.*mem_fn)(1, 2);
}
12. Move Semantics
rusty::move() vs std::move()
#include <rusty/move.hpp>
// @safe
void move_comparison() {
int x = 42;
int& ref = x;
// std::move on reference: just casts to rvalue (reference still valid)
// rusty::move on reference: invalidates the reference (Rust semantics)
int& ref2 = rusty::move(ref); // ref is now invalid
// ref; // ERROR: use after move
}
rusty::copy() for Explicit Copies
When you want to be explicit about copying:
// @safe
void copy_example() {
int x = 42;
int y = rusty::copy(x); // Explicit copy
// x is still valid
}
Reference Assignment Semantics
| Operation | Mutable Ref (T&) |
Const Ref (const T&) |
|---|---|---|
ref2 = ref1 |
Move (ref1 invalid) | Copy (both valid) |
rusty::move(ref) |
Invalidates ref | Compile error |
rusty::copy(ref) |
Creates copy | Creates copy |
Part V: Analysis Features
13. Borrow Checking
Double Mutable Borrow Detection
// @safe
void double_mutable() {
int x = 42;
int& ref1 = x;
int& ref2 = x; // ERROR: Cannot borrow 'x' as mutable more than once
}
Mixed Mutable/Immutable Conflicts
// @safe
void mixed_borrow() {
int x = 42;
int& mut_ref = x;
const int& const_ref = x; // ERROR: Cannot borrow 'x' as immutable
// because it is also borrowed as mutable
}
Scope-Based Cleanup
Borrows are released when they go out of scope:
// @safe
void scope_cleanup() {
int x = 42;
{
int& ref = x; // Borrow starts
} // Borrow ends here
int& ref2 = x; // OK: previous borrow is out of scope
}
14. Move Analysis
Use-After-Move Detection
// @safe
void use_after_move() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box);
*box; // ERROR: Use after move: 'box' was moved
}
Loop Analysis (2-Iteration Simulation)
The checker simulates 2 loop iterations to catch move errors:
// @safe
void loop_move() {
auto box = rusty::Box<int>::make(42);
for (int i = 0; i < 2; i++) {
auto temp = std::move(box); // ERROR: Use after move on second iteration
}
}
Reassignment Recovery
A moved variable becomes valid again after reassignment:
// @safe
void reassignment() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box); // box is moved
box = rusty::Box<int>::make(100); // box is valid again
*box; // OK
}
15. RAII & Container Safety
RAII tracking extends RustyCpp’s borrow checking to handle C++-specific patterns where object lifetimes and resource ownership can lead to dangling references, use-after-free, and other memory safety issues.
Reference/Pointer Stored in Container
Detects when a pointer or reference is stored in a container that outlives the pointee:
// @safe
void bad_pointer_in_container() {
std::vector<int*> vec;
{
int x = 42;
vec.push_back(&x); // Store pointer to x
} // x destroyed here
// ERROR: vec[0] is now a dangling pointer
*vec[0] = 10;
}
Detected operations:
push_back,push_front,insert,emplace,emplace_back,emplace_front,assignwith pointer/reference arguments- Container types:
vector,list,deque,set,map,unordered_*,array,span
Iterator Outlives Container
Detects when an iterator survives longer than its source container:
// @safe
void bad_iterator_outlives_container() {
std::vector<int>::iterator it;
{
std::vector<int> v = {1, 2, 3};
it = v.begin(); // it borrows from v
} // v destroyed here
// ERROR: it is now invalid
int val = *it;
}
Detected iterator-returning methods:
begin,end,cbegin,cend,rbegin,rend,find,lower_bound,upper_bound
Iterator Invalidation (Modification During Iteration)
// @safe
void iterator_invalidation() {
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin();
vec.push_back(4); // May invalidate iterators (reallocation)
*it; // ERROR: Iterator may be invalidated
}
User-Defined RAII Types
RustyCpp recognizes classes with user-defined destructors as RAII types, enabling proper lifetime tracking:
class FileHandle {
FILE* f;
public:
FileHandle(const char* path) : f(fopen(path, "r")) {}
~FileHandle() { if (f) fclose(f); } // User-defined destructor
};
// RustyCpp now tracks FileHandle as an RAII type
Member Lifetime Tracking
Detects when references to object members outlive the containing object:
// @safe
void bad_member_reference() {
const std::string* ptr;
{
struct Wrapper { std::string data; };
Wrapper w;
w.data = "hello";
ptr = &w.data; // ptr references w.data
} // w destroyed, w.data destroyed
// ERROR: ptr is now dangling
std::cout << *ptr;
}
new/delete Tracking
Detects double-free and use-after-free with raw heap allocations:
// @safe
void bad_double_free() {
int* ptr = new int(42);
delete ptr;
delete ptr; // ERROR: double free
}
// @safe
void bad_use_after_free() {
int* ptr = new int(42);
delete ptr;
*ptr = 10; // ERROR: use after free
}
Lambda Capture Escape Analysis
RustyCpp uses escape analysis to allow safe reference captures while catching dangerous ones:
// @safe
std::function<int()> bad_lambda_escape() {
int x = 42;
auto lambda = [&x]() { return x; }; // Captures x by reference
return lambda; // ERROR: lambda escapes, x will be destroyed
}
// @safe
void good_lambda_local_use() {
int x = 42;
auto lambda = [&x]() { return x; }; // OK: lambda doesn't escape
int result = lambda(); // Used locally, x still alive
}
Capture rules:
| Capture Type | Status |
|---|---|
Reference ([&], [&x]) in non-escaping lambda |
Allowed |
| Reference captures that escape (returned, stored) | Forbidden |
Copy captures ([x], [=]) |
Always allowed |
Move captures ([y = std::move(x)]) |
Always allowed |
this capture |
Always forbidden (raw pointer) |
Return Reference to Local
// @safe
const std::string& bad_return_ref() {
std::string local = "hello";
return local; // ERROR: returning reference to local variable
}
Comparison with Rust
| Feature | Rust | RustyCpp |
|---|---|---|
| Move detection | ✅ | ✅ |
| Use-after-move | ✅ | ✅ |
| Iterator outlives container | ✅ | ✅ |
| Reference in container | ✅ | ✅ |
| Lambda escape analysis | ✅ | ✅ |
| User-defined RAII | N/A | ✅ (C++ specific) |
| Non-Lexical Lifetimes (NLL) | ✅ | ❌ (may cause false positives) |
16. Const Propagation
Pointer Member Mutations
The checker tracks const-correctness through pointer members:
class Container {
int* data;
public:
void mutate() const {
*data = 42; // Mutating through pointer in const method
}
};
// @safe
void const_propagation() {
Container c;
const Container& ref = c;
ref.mutate(); // ERROR: Mutating through const reference
}
Interior Mutability Handling
Cell and similar types are handled specially:
class Wrapper {
rusty::Cell<int> value;
public:
// @safe
void set(int v) const {
value.set(v); // OK: Cell provides interior mutability
}
};
Part VI: Integration
17. Build System Integration
CMake Integration
# Find the checker binary
find_program(RUSTY_CPP_CHECKER rusty-cpp-checker)
# Add a custom target to run the checker
add_custom_target(borrow_check
COMMAND ${RUSTY_CPP_CHECKER}
${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
-I ${CMAKE_CURRENT_SOURCE_DIR}/include
COMMENT "Running RustyCpp borrow checker"
)
# Or integrate with compile_commands.json
add_custom_target(borrow_check_all
COMMAND ${RUSTY_CPP_CHECKER}
--compile-commands ${CMAKE_BINARY_DIR}/compile_commands.json
${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp
COMMENT "Running RustyCpp on all sources"
)
Makefile Integration
RUSTY_CPP_CHECKER := rusty-cpp-checker
INCLUDE_DIRS := -I include -I third_party/include
borrow_check: $(SOURCES)
$(RUSTY_CPP_CHECKER) $(SOURCES) $(INCLUDE_DIRS)
.PHONY: borrow_check
compile_commands.json Support
RustyCpp can read compiler flags from compile_commands.json:
# Generate compile_commands.json with CMake
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..
# Run checker with compile commands
./rusty-cpp-checker src/main.cpp --compile-commands build/compile_commands.json
18. Include Path Configuration
CLI Flags
./rusty-cpp-checker file.cpp -I include -I /usr/local/include
Environment Variables
export CPLUS_INCLUDE_PATH=/project/include:/third_party/include
export CPATH=/usr/include
./rusty-cpp-checker file.cpp
Auto-Detection
RustyCpp automatically detects common include paths:
/usr/include/usr/local/include- System C++ headers (
/include/c++/) - LLVM/Clang headers
19. Gradual Adoption Strategy
File-by-File Migration
Start by annotating one file at a time:
// === legacy_code.cpp ===
// No annotations - not checked
// === new_code.cpp ===
// @safe
namespace myapp {
// This file is checked
}
Namespace-Level Annotations
Annotate entire namespaces:
// @safe
namespace myapp::core {
// All functions here are @safe
}
// @unsafe
namespace myapp::legacy {
// All functions here are @unsafe (not checked)
}
Mixing Safe and Unsafe Code
Use @unsafe blocks to call into legacy code:
// @safe
void safe_wrapper() {
// @unsafe
{
legacy_function(); // Call to unannotated code
}
// Continue with safe code
}
Part VII: Reference
20. Annotation Reference
Complete Syntax
// Function annotation (attaches to next function)
// @safe
void safe_function() { }
// @unsafe
void unsafe_function() { }
// Class annotation (all methods inherit)
// @safe
class SafeClass {
void method(); // @safe
};
// Namespace annotation (all contents inherit)
// @safe
namespace safe_ns {
void func(); // @safe
}
// Block annotation (within @safe function)
// @safe
void func() {
// @unsafe
{
// Unsafe code here
}
}
@lifetime Specification
// Return reference with same lifetime as input
// @lifetime: (&'a) -> &'a
const T& identity(const T& x);
// Return reference with lifetime of first parameter
// @lifetime: (&'a, &'b) -> &'a
const T& first(const T& a, const T& b);
// Multiple constraints
// @lifetime: (&'a, &'b) -> &'a where 'a: 'b
const T& select(const T& a, const T& b);
// Mutable reference
// @lifetime: (&'a mut) -> &'a mut
T& get_mut(T& x);
// Owned return (no lifetime)
// @lifetime: (&'a) -> owned
T clone(const T& x);
External Annotations for Third-Party Code
For code you can’t modify, use external annotation files:
// @external: {
// std::vector::push_back: [unsafe, (&'a mut, T) -> void]
// std::vector::operator[]: [unsafe, (&'a, size_t) -> &'a]
// my_lib::safe_func: [safe, (&'a) -> &'a]
// }
21. Error Messages
Common Violations and Fixes
| Error | Cause | Fix |
|---|---|---|
| “Use after move” | Using variable after std::move |
Don’t use moved variables, or reassign first |
| “Cannot borrow as mutable more than once” | Multiple &mut |
Use separate scopes or restructure |
| “Cannot borrow as immutable while mutably borrowed” | & and &mut conflict |
Release mutable borrow first |
| “Returning reference to local variable” | Dangling reference | Return by value or use parameter lifetime |
| “Iterator may be invalidated” | Container modified during iteration | Copy or use indices |
Understanding Borrow Chains
ERROR: Cannot move 'x' because it is borrowed
Borrow chain: ref3 -> ref2 -> ref1 -> x
ref1 borrows x at line 5
ref2 borrows ref1 at line 6
ref3 borrows ref2 at line 7
Cannot move x while this chain exists
22. Limitations & Known Issues
What’s Not Checked
- Runtime thread safety: No data race detection (use ThreadSanitizer); Send/Sync traits ARE enforced at compile-time
- Exception safety: Stack unwinding not modeled
- Virtual functions: Limited dynamic dispatch analysis
- Complex templates: SFINAE, partial specialization limitations
False Positives
Some patterns may trigger false positives:
- Complex control flow
- Template metaprogramming
- Macro-heavy code
Use @unsafe to suppress false positives when necessary.
Template Limitations
- Variadic templates: Partial support
- SFINAE: Not fully analyzed
- Concepts (C++20): Basic support
Part VIII: Advanced Topics
23. Relocation vs Move Semantics
The Problem with C++ Move Semantics
C++11 move semantics leave the source object in a “valid but unspecified state”:
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1 is now "valid but unspecified" - maybe empty, maybe not
v1.push_back(4); // Legal! v1 is still a valid vector
This creates issues:
- Moved-from objects can still be used (accidentally)
- Destructors still run on moved-from objects
- No compile-time enforcement of “don’t use after move”
True Relocation (Safe C++ Approach)
The Safe C++ proposal introduces rel for true relocation:
// Safe C++ syntax (not RustyCpp)
std2::vector<int> v1 = {1, 2, 3};
std2::vector<int> v2 = rel v1; // v1 is now UNINITIALIZED
v1.push_back(4); // COMPILE ERROR: v1 is uninitialized
After relocation:
- Source becomes truly uninitialized (not valid-but-unspecified)
- Compiler tracks initialization state
- Use of uninitialized variable is a compile error
RustyCpp’s Approach
RustyCpp uses static analysis on top of standard C++ move semantics:
// @safe
void example() {
auto v1 = std::vector<int>{1, 2, 3};
auto v2 = std::move(v1); // RustyCpp marks v1 as "moved"
v1.push_back(4); // ERROR: Use after move (detected by analyzer)
}
Key differences from Safe C++:
| Aspect | Safe C++ (rel) |
RustyCpp |
|---|---|---|
| Mechanism | Language feature | Static analysis |
| Moved-from state | Truly uninitialized | Valid-but-unspecified (C++ semantics) |
| Enforcement | Compiler built-in | External checker |
| Runtime behavior | No destructor call | Destructor still runs |
| Recovery | Must reinitialize | Reassignment makes valid again |
Reassignment Recovery
Unlike true relocation, RustyCpp allows recovery through reassignment:
// @safe
void recovery() {
auto box = rusty::Box<int>::make(42);
auto box2 = std::move(box); // box is "moved"
box = rusty::Box<int>::make(100); // box is valid again
*box; // OK
}
This matches C++ semantics where moved-from objects can be reused.
24. Raw Pointer Safety
Pointers vs References in RustyCpp
In Rust, raw pointers (*const T, *mut T) are inherently unsafe - dereferencing them requires an unsafe block. RustyCpp takes a similar approach: raw pointer operations are restricted in @safe code.
Key distinction:
- References (
T&,const T&): Safe, borrow-checked, cannot be null - Raw pointers (
T*,const T*): Unsafe, require@unsafecontext
Raw Pointer Operations Require @unsafe
In @safe code, the following operations are forbidden:
// @safe
void forbidden_in_safe() {
int x = 42;
// ❌ Address-of operator creates raw pointer
int* ptr = &x; // ERROR: requires @unsafe
// ❌ Pointer dereference
int y = *ptr; // ERROR: requires @unsafe
}
// @unsafe
void allowed_in_unsafe() {
int x = 42;
// ✅ All pointer operations allowed
int* ptr = &x;
int y = *ptr;
*ptr = 100;
}
rusty::Ptr<T> / rusty::MutPtr<T> (Safe Pointer Wrappers)
RustyCpp provides safe pointer types that can be used in @safe code:
#include <rusty/ptr.hpp>
// @safe - Ptr and MutPtr are safe to use!
void ptr_example() {
int x = 42;
// Use addr_of() / addr_of_mut() to create safe pointers
rusty::Ptr<int> p = rusty::addr_of(x); // Safe immutable pointer
rusty::MutPtr<int> mp = rusty::addr_of_mut(x); // Safe mutable pointer
int y = *p; // Read through const pointer - safe
*mp = 100; // Write through mutable pointer - safe
}
| Type | C++ Equivalent | Rust Equivalent | Safety |
|---|---|---|---|
rusty::Ptr<T> |
const T* |
*const T |
✅ Safe |
rusty::MutPtr<T> |
T* |
*mut T |
✅ Safe |
const T* (raw) |
- | - | ❌ Requires @unsafe |
T* (raw) |
- | - | ❌ Requires @unsafe |
Key distinction:
- Raw C++ pointers (
T*,const T*): Require@unsafecontext rusty::Ptr<T>/rusty::MutPtr<T>: Safe to use in@safecoderusty::addr_of()/rusty::addr_of_mut(): Safe way to create pointers from references
Safe Patterns for Pointer-Like Behavior
// ❌ Raw pointer - requires @unsafe
int* get_ptr(std::vector<int>& vec) {
return &vec[0];
}
// ✅ rusty::Ptr - safe pointer (use addr_of)
rusty::Ptr<int> get_safe_ptr(std::vector<int>& vec) {
return rusty::addr_of(vec[0]);
}
// ✅ Reference - safe and borrow-checked
int& get_ref(std::vector<int>& vec) {
return vec[0];
}
// ✅ Smart pointer - safe ownership
rusty::Box<int> make_owned() {
return rusty::Box<int>::make(42);
}
When Raw Pointers Are Necessary
Raw pointers are sometimes unavoidable:
- Interfacing with C APIs
- Working with memory-mapped I/O
- Implementing custom allocators
- FFI boundaries
In these cases, isolate the pointer usage in @unsafe functions:
// @unsafe
void* allocate_memory(size_t size) {
return malloc(size);
}
// @safe
void safe_wrapper() {
// @unsafe
{
void* mem = allocate_memory(1024);
// Use mem...
free(mem);
}
}
25. Thread Safety Model
Current Status: Implemented
RustyCpp provides compile-time thread safety via Rust-like Send and Sync traits, enforced through C++20 concepts.
Send Trait: Transfer Across Threads
The Send trait indicates a type can be safely transferred (moved) to another thread.
#include <rusty/traits.hpp>
// rusty::Send concept - compile-time check
template<typename T>
concept Send = rusty::is_send<T>::value;
// Types that are Send:
// ✅ Primitives (int, float, etc.)
// ✅ rusty::Box<T> if T is Send
// ✅ rusty::Arc<T> if T is Send + Sync
// ✅ rusty::Mutex<T> if T is Send
// ✅ rusty::Cell<T> if T is Send
// ✅ rusty::RefCell<T> if T is Send
// ✅ std::unique_ptr<T> if T is Send
// Types that are NOT Send:
// ❌ rusty::Rc<T> - non-atomic reference count
// ❌ Raw pointers (T*, const T*)
Sync Trait: Share Across Threads
The Sync trait indicates &T can be safely shared between threads.
// rusty::Sync concept - compile-time check
template<typename T>
concept Sync = rusty::is_sync<T>::value;
// Types that are Sync:
// ✅ Primitives (int, float, etc.)
// ✅ rusty::Arc<T> if T is Send + Sync
// ✅ rusty::Box<T> if T is Sync
// ✅ rusty::Mutex<T> if T is Send
// ✅ rusty::Atomic<T>
// ✅ const T& if T is Sync
// Types that are NOT Sync:
// ❌ rusty::Rc<T> - non-atomic
// ❌ rusty::Cell<T> - unsynchronized interior mutability
// ❌ rusty::RefCell<T> - unsynchronized interior mutability
// ❌ T& (mutable references) - never Sync
// ❌ Raw pointers
Thread Spawning with Send Enforcement
Use rusty::thread::spawn() to launch threads with compile-time Send checking:
#include <rusty/thread.hpp>
// @safe
void spawn_example() {
// Data to send to thread must satisfy Send concept
auto data = rusty::Box<int>::make(42);
// spawn() requires all arguments to be Send
auto handle = rusty::thread::spawn([](rusty::Box<int> d) {
// Use data in thread
return *d * 2;
}, std::move(data));
// JoinHandle<T> - Rust-style semantics
int result = handle.join(); // Blocks and returns result
}
// Compile-time error example:
void error_example() {
rusty::Rc<int> rc = rusty::Rc<int>::make(42);
// ❌ COMPILE ERROR: Rc<T> is not Send
// auto handle = rusty::thread::spawn([](rusty::Rc<int> r) {
// return *r;
// }, std::move(rc));
}
JoinHandle: Rust-Style Thread Management
JoinHandle<T> provides Rust-like thread lifecycle management:
#include <rusty/thread.hpp>
void join_handle_example() {
auto handle = rusty::thread::spawn([]() {
return 42;
});
// Check if finished (non-blocking)
if (handle.is_finished()) {
// Thread completed
}
// Join and get result (blocks)
int result = handle.join();
// ❌ Cannot join twice
// handle.join(); // Throws: "Thread already joined"
}
// Rust semantics: detach on drop
void detach_on_drop() {
{
auto handle = rusty::thread::spawn([]() {
// Long-running task
});
// handle goes out of scope without join()
} // Thread is DETACHED (not blocked), Rust-style
}
// Explicit detach
void explicit_detach() {
auto handle = rusty::thread::spawn([]() { /* work */ });
handle.detach(); // Explicitly detach
}
Scoped Threads: Guaranteed Join
For threads that must complete before a scope exits, use rusty::thread::scope():
#include <rusty/thread.hpp>
void scoped_example() {
std::vector<int> data = {1, 2, 3, 4, 5};
rusty::thread::scope([&](rusty::thread::Scope& s) {
// Spawn threads within scope - NO Send requirement!
// Scope guarantees threads complete before data dies
s.spawn([&data]() {
// Can safely borrow local data
for (int x : data) {
process(x);
}
});
s.spawn([&data]() {
// Multiple threads can share &data
compute(data);
});
}); // Blocks until ALL scoped threads complete
// data is still valid here
}
Key difference from spawn():
spawn(): RequiresSend, thread may outlive callerscope(): NoSendrequired, threads guaranteed to join before scope ends
Marking User Types as Send
Use the RUSTY_MARK_SEND macro for your own types:
#include <rusty/send_trait.hpp>
class MyThreadSafeData {
std::atomic<int> value;
public:
explicit MyThreadSafeData(int v) : value(v) {}
int get() const { return value.load(); }
};
// Mark as Send (type can be transferred across threads)
RUSTY_MARK_SEND(MyThreadSafeData)
// For template types: Send if T is Send
template<typename T>
class MyContainer {
T data;
};
RUSTY_MARK_SEND_TEMPLATE(MyContainer, T)
// Now these compile:
void use_custom_types() {
MyThreadSafeData data(42);
auto h1 = rusty::thread::spawn([](MyThreadSafeData d) {
return d.get();
}, std::move(data));
MyContainer<int> container;
auto h2 = rusty::thread::spawn([](MyContainer<int> c) {
// ...
}, std::move(container));
}
Reference Rules for Send/Sync
RustyCpp follows Rust’s rules for references:
// RULE 1: const T& is Send if T is Sync
// (Immutable shared reference can be sent if type is shareable)
template<typename T>
struct is_send<const T&> : is_sync<T> {};
// RULE 2: T& (mutable ref) is Send if T is Send
// (Mutable reference can be sent if type itself can be sent)
template<typename T>
struct is_send<T&> : is_send<T> {};
// RULE 3: const T& is Sync if T is Sync
template<typename T>
struct is_sync<const T&> : is_sync<T> {};
// RULE 4: T& (mutable ref) is NEVER Sync
// (Cannot share mutable references - would cause data races)
template<typename T>
struct is_sync<T&> : std::false_type {};
Send/Sync Summary Table
| Type | Send | Sync | Notes |
|---|---|---|---|
int, float, primitives |
✅ | ✅ | Always thread-safe |
rusty::Box<T> |
if T is Send | if T is Sync | Unique ownership |
rusty::Arc<T> |
if T is Send+Sync | if T is Send+Sync | Shared ownership |
rusty::Rc<T> |
❌ | ❌ | Non-atomic refcount |
rusty::Mutex<T> |
if T is Send | if T is Send | Synchronizes access |
rusty::Cell<T> |
if T is Send | ❌ | Unsync interior mut |
rusty::RefCell<T> |
if T is Send | ❌ | Unsync interior mut |
rusty::Atomic<T> |
✅ | ✅ | Lock-free sync |
T*, const T* |
❌ | ❌ | Raw pointers unsafe |
const T& |
if T is Sync | if T is Sync | Immutable borrow |
T& |
if T is Send | ❌ | Mutable borrow |
What’s NOT Checked
RustyCpp’s thread safety is compile-time via concepts, not runtime analysis:
- ❌ Data race detection (use ThreadSanitizer)
- ❌ Mutex lock ordering / deadlock detection
- ❌ Atomics ordering correctness
- ❌ Dynamic thread safety analysis
For runtime thread safety checking, use ThreadSanitizer:
clang++ -fsanitize=thread -g source.cpp
26. Initialization Analysis
What We Check
RustyCpp performs basic initialization tracking:
// @safe
void init_example() {
int x; // Uninitialized
int y = x; // ERROR: Use of uninitialized variable
int z;
z = 42; // Now initialized
int w = z; // OK
}
Scope-Based Tracking
Initialization state is tracked through control flow:
// @safe
void conditional_init() {
int x;
if (condition) {
x = 1;
} else {
x = 2;
}
use(x); // OK: x initialized on all paths
}
// @safe
void partial_init() {
int x;
if (condition) {
x = 1;
}
use(x); // ERROR: x may be uninitialized
}
Limitations
Current initialization analysis does not cover:
- Member initialization order: Constructor initializer list ordering not checked
- Delayed initialization: Objects initialized after declaration in complex patterns
- Placement new: Memory reuse patterns
Comparison with Safe C++
| Feature | Safe C++ | RustyCpp |
|---|---|---|
| Uninitialized locals | Compile error | Static analysis warning |
| Relocation tracking | Built-in | Via move analysis |
| Member init order | Checked | Not checked |
| Placement new | Tracked | Not tracked |
27. Lifetime Elision Rules
What is Lifetime Elision?
When lifetime annotations are omitted, the compiler/analyzer infers them based on common patterns. This reduces boilerplate while maintaining safety.
RustyCpp’s Elision Rules
Rule 1: Single reference parameter
If there’s exactly one reference parameter, the return lifetime matches it:
// No annotation needed - inferred as: (&'a) -> &'a
const std::string& first_char(const std::string& s);
// Equivalent to:
// @lifetime: (&'a) -> &'a
const std::string& first_char(const std::string& s);
Rule 2: Multiple parameters, one is this
For methods, return lifetime matches this:
class Container {
// Inferred as: (&'self) -> &'self
const Item& get() const;
// Inferred as: (&'self mut) -> &'self mut
Item& get_mut();
};
Rule 3: Multiple reference parameters
Explicit annotation required:
// ERROR: Ambiguous - which parameter's lifetime?
const T& select(const T& a, const T& b);
// Must specify:
// @lifetime: (&'a, &'b) -> &'a
const T& select(const T& a, const T& b);
When to Write Explicit Annotations
Always write explicit @lifetime when:
- Multiple reference parameters exist
- Return lifetime differs from the obvious choice
- Complex lifetime relationships exist (outlives constraints)
- Documenting API contracts for library code
// Complex case: return lifetime tied to first param only
// @lifetime: (&'a, &'b) -> &'a where 'a: 'b
const T& longer_lived(const T& a, const T& b);
// Owned return (no lifetime relationship)
// @lifetime: (&'a) -> owned
std::string to_string(const MyClass& obj);
28. Pattern Matching & Sum Types
The Limitation of C++ Optional/Variant
C++ std::optional and std::variant have safety issues:
std::optional<int> opt;
int value = *opt; // UB: accessing empty optional
std::variant<int, std::string> var = 42;
std::string& s = std::get<std::string>(var); // Throws: wrong type
What Safe C++ Proposes: Choice Types
Safe C++ introduces first-class sum types with pattern matching:
// Safe C++ syntax (not RustyCpp)
choice Result<T, E> {
Ok(T),
Err(E),
};
Result<int, std::string> divide(int a, int b) {
if (b == 0) return .Err("division by zero");
return .Ok(a / b);
}
void use_result() {
auto result = divide(10, 2);
match (result) {
.Ok(value) => std::cout << "Got: " << value;
.Err(msg) => std::cout << "Error: " << msg;
}; // Exhaustiveness checked at compile time
}
Key features:
- Exhaustiveness checking (must handle all cases)
- No way to access wrong variant
- Pattern binding extracts values safely
RustyCpp’s Approach: Wrapper Types
We provide rusty::Option<T> and rusty::Result<T, E> with safer APIs:
#include <rusty/option.hpp>
#include <rusty/result.hpp>
// @safe
void safe_optional() {
rusty::Option<int> opt = rusty::None;
// Safe access patterns
if (opt.is_some()) {
int value = opt.unwrap(); // OK: checked first
}
int value = opt.unwrap_or(0); // Safe: provides default
// Callback-based matching
opt.match(
[](int v) { std::cout << "Got: " << v; },
[]() { std::cout << "None"; }
);
}
Comparison
| Feature | Safe C++ Choice | rusty::Option/Result |
|---|---|---|
| Exhaustiveness | Compile-time enforced | Runtime (match callbacks) |
| Pattern syntax | Language built-in | Library methods |
| Zero-cost | Yes | Mostly (some virtual calls) |
| Type safety | Complete | Partial (unwrap can panic) |
Best Practices with rusty::Option
// AVOID: Unchecked unwrap
int bad(rusty::Option<int> opt) {
return opt.unwrap(); // May panic!
}
// PREFER: Check first or use safe alternatives
int good1(rusty::Option<int> opt) {
return opt.unwrap_or(0);
}
int good2(rusty::Option<int> opt) {
if (opt.is_some()) {
return opt.unwrap();
}
return 0;
}
// BEST: Use match for exhaustive handling
int best(rusty::Option<int> opt) {
int result = 0;
opt.match(
[&](int v) { result = v; },
[&]() { result = 0; }
);
return result;
}
29. Comparison with Safe C++ Proposal
Overview
The Safe C++ Proposal is a comprehensive language extension proposal. RustyCpp is a static analyzer that works with standard C++.
Feature Comparison
| Feature | Safe C++ Proposal | RustyCpp |
|---|---|---|
| Approach | Language extension | Static analyzer |
| Syntax | New keywords (safe, rel, ^) |
Comment annotations (@safe) |
| Borrow types | T^ (mutable), const T^ (shared) |
Standard C++ references |
| Relocation | rel keyword |
std::move + analysis |
| Sum types | choice + match |
Library types + callbacks |
| Thread safety | send/sync traits |
Not implemented |
| Lifetime params | First-class syntax | Comment annotations |
| Runtime checks | Panic on bounds, etc. | Pure static analysis |
| Compiler support | Requires compiler changes | Works with any C++20 compiler |
| Adoption | Rewrite with new syntax | Gradual annotation |
Philosophical Differences
Safe C++: “Change the language to make safety the default”
- New syntax for safe constructs
- Unsafe operations require explicit
unsafeblocks - Breaking changes to semantics (relocation vs move)
RustyCpp: “Add safety checking to existing C++”
- Works with standard C++ syntax
- Opt-in via annotations
- Non-breaking (analyzer is optional)
When to Use Which
Use Safe C++ (when available) if:
- Starting a new project from scratch
- Team is willing to learn new syntax
- Maximum safety guarantees needed
- Can wait for compiler support
Use RustyCpp if:
- Working with existing C++ codebase
- Need safety checking today
- Gradual migration is important
- Cannot change compiler
Interoperability Vision
In the future, RustyCpp could potentially:
- Recognize Safe C++ syntax when it becomes available
- Provide migration tooling from annotated C++ to Safe C++
- Serve as a stepping stone toward full Safe C++ adoption
Part IX: Future Roadmap
30. Planned Features
Short Term (Next 6 Months)
Enhanced Lifetime Analysis
- More sophisticated elision rules
- Better cross-function tracking
- Improved error messages with suggestions
Template Improvements
- Better variadic template support
- SFINAE-aware analysis
- Concept constraint checking
IDE Integration
- Language Server Protocol (LSP) support
- Real-time error highlighting
- Quick-fix suggestions
Medium Term (6-12 Months)
Thread Safety (Basic)
- Annotations for thread-safe types
- Detection of obvious data races
- Mutex guard lifetime tracking
Unsafe Type Qualifier
- Mark types as inherently unsafe
- Propagate unsafety through templates
- Better integration with legacy code
Custom Annotations
- User-defined safety annotations
- Plugin system for custom checks
- Project-specific rules
Long Term (12+ Months)
Safe C++ Compatibility
- Recognize Safe C++ syntax
- Migration tooling
- Hybrid analysis mode
Advanced Analysis
- Whole-program analysis mode
- Inter-procedural optimization
- Incremental checking
Ecosystem
- Pre-analyzed annotations for popular libraries
- Community annotation sharing
- CI/CD integration guides
Non-Goals
Some features we intentionally won’t implement:
- Runtime thread safety: Data race detection at runtime (use ThreadSanitizer); we DO have compile-time Send/Sync enforcement
- Exception flow analysis: Too complex, limited benefit
- Runtime instrumentation: We’re purely static
- Automatic code fixes: Too risky for safety-critical code
31. FAQ & Troubleshooting
Q: Why isn’t my function being checked?
A: Make sure it has a @safe annotation:
// @safe <-- Need this
void my_function() { }
Q: How do I suppress a false positive?
A: Use @unsafe block or function annotation:
// @safe
void func() {
// @unsafe
{
// False positive code here
}
}
Q: Why are third-party headers causing errors?
A: Make sure third-party headers are detected as external. Check:
- Include paths are correct
- Headers don’t have
@safeannotations
Q: How do I check only specific files?
A: Pass specific files to the checker:
./rusty-cpp-checker src/safe_module.cpp src/core.cpp
Q: Can I use this with existing codebases?
A: Yes! Use gradual adoption:
- Start with new files marked
@safe - Wrap calls to legacy code in
@unsafeblocks - Gradually migrate more code as you gain confidence
Appendix: Quick Reference Card
ANNOTATIONS
-----------
// @safe Function/class/namespace is checked
// @unsafe Function/class/namespace is NOT checked
// @unsafe { } Block within @safe function for unsafe ops
LIFETIME SYNTAX
---------------
// @lifetime: (&'a) -> &'a Same lifetime as input
// @lifetime: (&'a, &'b) -> &'a First param's lifetime
// @lifetime: (&'a mut) -> &'a mut Mutable reference
// @lifetime: -> owned No lifetime (owned return)
RUSTY TYPES
-----------
rusty::Box<T> Unique ownership (like unique_ptr)
rusty::Arc<T> Thread-safe shared (like shared_ptr + atomic)
rusty::Rc<T> Single-thread shared
rusty::Cell<T> Interior mutability for Copy types
rusty::RefCell<T> Interior mutability with runtime checks
rusty::Option<T> Optional value
rusty::Result<T,E> Success or error
BORROW RULES
------------
Multiple &T : OK
Single &mut T: OK
&T + &mut T : ERROR
Move + use : ERROR
Last updated: January 2026