Categories
Blog Code

Hypothetical Rust-ish Borrowing in C++

Last night I got into a insomniac code-spiral about replicating some of Rust’s borrowing warnings in C++. In place of Rust’s initial variable declararion of let I use wrapper Owner<T> in C++ that internally tracks ownership. Passing to another Owner<T> will take the ownership, or a Borrower<T> class can accept a properly owned value and reference it as a constant.

// Function borrowing a value can only read const ref
static void use(Borrower<int> v) {
  (void)*v;
}

// Function can mutate value via ownership which generates 'warnings' if used again incorrectly
static void use_mut(Owner<int> v) {
  *v = 5;
}

int main() {
  // Rather than just 'int', Owner<int> tracks the lifetime of the value
  Owner<int> x{3};

  // Borrowing value before mutating causes no problems
  use(x);

  // Mutating value passes ownership, has_been_moved set on original x
  use_mut(std::move(x));

  // Uncomment for owner_already_borrowed = 1
  //use(x);

  // Uncomment for owner_already_moved = 1
  //use_mut(std::move(x));

  // Uncomment for another owner_already_borrowed++
  //Borrower<int> y = x;

  //Uncomment for owner_use_after_move = 1;
  //return *x;
}

If you look at the Owner/Borrower API below, then alarm bells will be ringing here as this is adding flab and extra state to otherwise clean classes or values. However, this is more a test to see if I can trick the compiler into tracking something Rust-like, and I was surprised how many cases this catches.

The static int counters report misuse of borrowed variables. This is at the cost here of composing types in more complicated types, which stinks a bit. However, as a foundational idea I find it interesting that one can trick the C++ compiler into semi compile-time checks. A static_assert would be better but that is impossible as the ownership state is mutating during compilation (so, no constexpr). The compromise is that ‘counts’ of bad borrows are computed by the compiler and stored in statics, so you can read out (all?) errors from the binary before running it. (In final production code, all this would be #defed out.)

// Hypothetical Rust-like owner / borrow wrappers in C++
// This wraps types with data which is compiled away in release
// It is not possible to static_assert, so this uses static ints to count errors.
#include <utility>

// Statics to track errors. Ideally these would be static_asserts
// but they depen on Owner::has_been_moved which changes during compilation.
static int owner_already_moved = 0;
static int owner_use_after_move = 0;
static int owner_already_borrowed = 0;

// This method exists to ensure static errors are reported in compiler explorer
int get_fault_count() {
  return owner_already_moved + owner_use_after_move + owner_already_borrowed;
}

// Storage for ownership of a type T.
// Equivalent to mut usage in Rust
// Disallows move by value, instead ownership must be explicitly moved.
template <typename T>
struct Owner {
  Owner(T v) : value(v) {}
  Owner(Owner<T>& ov) = delete;
  Owner(Owner<T>&& ov) {
    if (ov.has_been_moved) {
      owner_already_moved++;
    }
    value = std::move(ov.value);
    ov.has_been_moved = true;
  }

  T& operator*() {
    if (has_been_moved) {
      owner_use_after_move++;
    }
    return value;
  }

  T value;
  bool has_been_moved{false};
};

// Safely borrow a value of type T
// Implicit constuction from Owner of same type to check borrow is safe
template <typename T>
struct Borrower {
  Borrower(Owner<T>& v) : value(v.value) {
    if (v.has_been_moved) {
      owner_already_borrowed++;
    }
  }

  const T& operator*() const {
    return value;
  }

  T value;
};

// Example of function borrowing a value, can only read const ref
static void use(Borrower<int> v) {
  (void)*v;
}

// Example of function taking ownership of value, can mutate via owner ref
static void use_mut(Owner<int> v) {
  *v = 5;
}

int main() {
  // Rather than just 'int', Owner<int> tracks the lifetime of the value
  Owner<int> x{3};

  // Borrowing value before mutating causes no problems
  use(x);

  // Mutating value passes ownership, has_been_moved set on original x
  use_mut(std::move(x));

  // Uncomment for owner_already_borrowed = 1
  //use(x);

  // Uncomment for owner_already_moved = 1
  //use_mut(std::move(x));

  // Uncomment for another owner_already_borrowed++
  //Borrower<int> y = x;

  //Uncomment for owner_use_after_move = 1;
  //return *x;
}

These late nighters are fun puzzles to play with, but it’s hard to tell if it has any value, or if I’ve made an obvious error, or just gone mentally off-grid. Then the next day after the sunk cost of a sleepless night, I’m overly eager to ‘sell’ a very broad idea to people who just want their morning coffee. However, I’m brained-out now, so floating this idea for collaboration, RSVP if this piques your curiosity.

Compiler Explorer link | StackOverflow link

Leave a Reply

Your email address will not be published. Required fields are marked *