RAII#

RAII (Resource Acquisition Is Initialization) is fundamental to both C++ and Rust. Resources are acquired during initialization and released when the object goes out of scope. Rust’s ownership system makes RAII even more powerful by guaranteeing exactly when destructors run.

Constructors#

Source:

src/rust/structs

C++ has dedicated constructor syntax. Rust doesn’t have constructors as a language feature; instead, you define associated functions (commonly named new) that return Self.

C++:

class Point {
  double x_, y_;
public:
  // Default constructor
  Point() : x_(0), y_(0) {}

  // Parameterized constructor
  Point(double x, double y) : x_(x), y_(y) {}

  // Copy constructor (implicit or explicit)
  Point(const Point& other) = default;

  // Move constructor
  Point(Point&& other) noexcept = default;
};

int main() {
  Point p1;           // default constructor
  Point p2(3.0, 4.0); // parameterized constructor
  Point p3 = p2;      // copy constructor
}

Rust:

struct Point {
    x: f64,
    y: f64,
}

impl Point {
    // Associated function (not a method - no self)
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    // Default-like constructor
    fn origin() -> Self {
        Point { x: 0.0, y: 0.0 }
    }
}

fn main() {
    let p1 = Point::origin();      // "default" constructor
    let p2 = Point::new(3.0, 4.0); // parameterized constructor
    let p3 = p2;                   // move (Point doesn't impl Copy)
}

Default Trait#

Rust’s Default trait is similar to C++ default constructors:

#[derive(Default)]
struct Config {
    debug: bool,      // defaults to false
    timeout: u32,     // defaults to 0
    name: String,     // defaults to ""
}

fn main() {
    let config = Config::default();
    // Or with struct update syntax
    let custom = Config {
        debug: true,
        ..Default::default()
    };
}

Destructors and Drop#

Source:

src/rust/raii

C++ uses destructors (~ClassName). Rust uses the Drop trait with a drop method. Both are called automatically when the object goes out of scope.

C++:

#include <iostream>

class Resource {
  int* data;
public:
  Resource(int value) : data(new int(value)) {
    std::cout << "Acquired: " << *data << "\n";
  }

  ~Resource() {
    std::cout << "Released: " << *data << "\n";
    delete data;
  }

  // Rule of five: need copy/move constructors and assignments
  Resource(const Resource&) = delete;
  Resource& operator=(const Resource&) = delete;
};

int main() {
  Resource r(42);
  // destructor called at end of scope
}

Rust:

struct Resource {
    data: i32,
}

impl Resource {
    fn new(value: i32) -> Self {
        println!("Acquired: {}", value);
        Resource { data: value }
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Released: {}", self.data);
    }
}

fn main() {
    let r = Resource::new(42);
    // Drop::drop called at end of scope
}

Drop Order#

Variables are dropped in reverse order of declaration:

struct Droppable(i32);

impl Drop for Droppable {
    fn drop(&mut self) {
        println!("Dropping {}", self.0);
    }
}

fn main() {
    let a = Droppable(1);
    let b = Droppable(2);
    let c = Droppable(3);
}
// Output: Dropping 3, Dropping 2, Dropping 1

Explicit Drop#

Use std::mem::drop() to drop a value before its scope ends:

C++ (no direct equivalent, use scope or unique_ptr):

{
  Resource r(42);
  // use r
}  // r destroyed here
// continue without r

Rust:

fn main() {
    let r = Resource::new(42);
    // use r
    drop(r);  // explicitly drop
    // r is no longer valid
    println!("Resource already released");
}

Preventing Drop#

std::mem::forget() prevents the destructor from running. Useful for FFI when transferring ownership to C code:

use std::mem;

fn main() {
    let v = vec![1, 2, 3];
    mem::forget(v);  // destructor won't run, memory leaked
    // Use carefully - typically only for FFI
}

Rule of Three/Five vs Rust#

C++ requires implementing copy/move constructors and assignment operators (Rule of Three/Five). Rust handles this through traits:

C++ Rule of Five:

class Buffer {
  char* data;
  size_t size;
public:
  Buffer(size_t n) : data(new char[n]), size(n) {}
  ~Buffer() { delete[] data; }

  // Copy constructor
  Buffer(const Buffer& other) : data(new char[other.size]), size(other.size) {
    std::copy(other.data, other.data + size, data);
  }

  // Copy assignment
  Buffer& operator=(const Buffer& other) {
    if (this != &other) {
      delete[] data;
      size = other.size;
      data = new char[size];
      std::copy(other.data, other.data + size, data);
    }
    return *this;
  }

  // Move constructor
  Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
    other.data = nullptr;
    other.size = 0;
  }

  // Move assignment
  Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
      delete[] data;
      data = other.data;
      size = other.size;
      other.data = nullptr;
      other.size = 0;
    }
    return *this;
  }
};

Rust (much simpler):

struct Buffer {
    data: Vec<u8>,
}

impl Buffer {
    fn new(size: usize) -> Self {
        Buffer { data: vec![0; size] }
    }
}

// Move is automatic, no implementation needed
// Clone can be derived if needed
impl Clone for Buffer {
    fn clone(&self) -> Self {
        Buffer { data: self.data.clone() }
    }
}

// Drop is automatic for Vec, no implementation needed

this vs self#

Source:

src/rust/self_this

C++ has implicit this pointer. Rust requires explicit self parameter with three forms that integrate with ownership:

C++:

class Counter {
  int value = 0;
public:
  void increment() { this->value++; }  // this is implicit
  int get() const { return value; }
};

Rust:

struct Counter {
    value: i32,
}

impl Counter {
    fn increment(&mut self) {    // mutable borrow
        self.value += 1;
    }

    fn get(&self) -> i32 {       // immutable borrow
        self.value
    }

    fn into_value(self) -> i32 { // takes ownership, consumes self
        self.value
    }
}

See Also#