Emulate borrow checker in C++ with stateful metaprogramming

Constant expressions in C++ are not as constant as you might think. For example, the following program will cause a compiler error.

// example0.cpp
#include "borrow.hpp"

int
main() {
using borrow::ended;

struct $ {
struct Lifetime;
};

static_assert(not ended(${}, 0));

struct $::Lifetime {
struct Ended;
};

static_assert(not ended(${}, 0)); // compiler error here
}

This feature does not seem to be useful at first glance. Thanks to the brilliant friend injection techique discovered by Filip Roséen , stateful metaprogramming has become feasible in C++.

Rust, a promising rival to C++, is famed for its borrow checker. With stateful metaprogramming, we can have the same mechanism implemented at compile-time in C++, though you have to manually specify the beginning and the end of a lifetime.

// example1.cpp
#include <new>
#include <type_traits>
#include <utility>
#include "borrow.hpp"

template<typename T>
struct Container {
char buf[sizeof(T)];

void
write(T&& item) {
T *ptr = static_cast<T *>(static_cast<void *>(buf));
new (ptr) T(::std::move(item));
}

T
read() {
T *ptr = static_cast<T *>(static_cast<void *>(buf));
T item = ::std::move(*ptr);
ptr->~T();
return item;
}

T&
get() {
T *ptr = static_cast<T *>(static_cast<void *>(buf));
return *ptr;
}

T const&
get() const {
T *ptr = static_cast<T *>(static_cast<void *>(buf));
return *ptr;
}
};

template<typename T, typename $,
bool A = ::borrow::available(${}),
typename = typename ::std::enable_if<A>::type>
auto
get(::borrow::BorrowPtr<Container<T>,$> ptr) {
return ::borrow::BorrowPtr<T,$> { (*ptr).get() };
}


int
main() {
auto c = Container<int> {};
c.write(0);

BEGIN_LIFETIME($);
auto p = ::borrow::borrow_mut<$>(c);

BEGIN_LIFETIME($1);
auto p1 = ::borrow::borrow_mut<$1>(p);

auto p2 = get(p1);

*p2 = 1;

END_LIFETIME($1);

*p2; // compiler error here

END_LIFETIME($);
}

A borrow pointer can never be dereferenced after its lifetime has ended.

// mut3.cpp
#include "borrow.hpp"

int
main() {
using ::borrow::borrow;
using ::borrow::borrow_mut;

int value = 1;
BEGIN_LIFETIME($1);
BEGIN_LIFETIME($2);
BEGIN_LIFETIME($3);

auto p1 = borrow_mut<$1>(value);
auto p2 = borrow<$2>(p1);
auto p3 = borrow<$3>(p1);
*p2;
static_assert(::std::is_same<decltype(*p2), int const&>::value);

END_LIFETIME($2);
*p3;
*p2; // compiler error here
END_LIFETIME($3);
END_LIFETIME($1);
}

And here is an example that p2 lives in the lifetime of p1.

// mut4.cpp
#include "borrow.hpp"

int
main() {
using ::borrow::borrow;
using ::borrow::borrow_mut;

int value = 1;
BEGIN_LIFETIME($1);
BEGIN_LIFETIME($2);

auto p1 = borrow_mut<$1>(value);
auto p2 = borrow<$2>(p1);
*p2;
static_assert(::std::is_same<decltype(*p2), int const&>::value);

END_LIFETIME($1);
*p2; // compiler error here
END_LIFETIME($2);
}

There can only be exactly one mutable borrow,

// mut7.cpp
#include "borrow.hpp"

int
main() {
using ::borrow::borrow;
using ::borrow::borrow_mut;

int value = 1;
BEGIN_LIFETIME($1);
BEGIN_LIFETIME($2);
BEGIN_LIFETIME($3);
BEGIN_LIFETIME($4);
BEGIN_LIFETIME($5);

auto p1 = borrow_mut<$1>(value);
auto p2 = borrow_mut<$2>(p1);
END_LIFETIME($2);

auto p3 = borrow<$3>(p1);
END_LIFETIME($3);

auto p4 = borrow_mut<$4>(p1);
auto p5 = borrow<$5>(p1); // compiler error here

END_LIFETIME($1);
END_LIFETIME($4);
END_LIFETIME($5);
}

or multiple immutable borrow, but not both.

// mut8.cpp
#include "borrow.hpp"

int
main() {
using ::borrow::borrow;
using ::borrow::borrow_mut;

int value = 1;
BEGIN_LIFETIME($1);
BEGIN_LIFETIME($2);
BEGIN_LIFETIME($3);
BEGIN_LIFETIME($4);
BEGIN_LIFETIME($5);
BEGIN_LIFETIME($6);
BEGIN_LIFETIME($7);

auto p1 = borrow_mut<$1>(value);
auto p2 = borrow<$2>(p1);
auto p3 = borrow<$3>(p1);
END_LIFETIME($2);
END_LIFETIME($3);

auto p4 = borrow_mut<$4>(p1);
END_LIFETIME($4);

auto p5 = borrow<$5>(p1);
auto p6 = borrow<$6>(p1);
auto p7 = borrow_mut<$7>(p1); // compiler error here

END_LIFETIME($1);
END_LIFETIME($5);
END_LIFETIME($6);
END_LIFETIME($7);
}

And a borrow pointer can never be dereferenced when it was still mutably borrowed.

// mut9.cpp
#include "borrow.hpp"

int
main() {
using ::borrow::borrow;
using ::borrow::borrow_mut;

int value = 1;
BEGIN_LIFETIME($1);
BEGIN_LIFETIME($2);
BEGIN_LIFETIME($3);
BEGIN_LIFETIME($4);

auto p1 = borrow_mut<$1>(value);
*p1;
static_assert(::std::is_same<decltype(*p1), int&>::value);

auto p2 = borrow<$2>(p1);
*p1;
static_assert(::std::is_same<decltype(*p1), int const&>::value);
*p2;
END_LIFETIME($2);

*p1;
static_assert(::std::is_same<decltype(*p1), int&>::value);

auto p3 = borrow_mut<$3>(p1);
*p3;
END_LIFETIME($3);
*p1;
static_assert(::std::is_same<decltype(*p1), int&>::value);

auto p4 = borrow_mut<$4>(p1);
*p4;
*p1; // compiler error here

END_LIFETIME($1);
END_LIFETIME($4);
}
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.