Sometimes, I need to work in heapless environments. One really unfortunate thing about the language C++, is that move semantics become very difficult to express and constrain when the heap is unavailable.

Storage Durations

As a brief reminder, all variables in C++ have a storage duration, and there are four storage duration classes: automatic, static, thread and dynamic. Memory with dynamic storage duration is allocated on the heap, of course. All non-global variables have automatic storage duration, unless they are declared static, extern, or thread_local.

The Problem

Imagine, for example, that I have a class that represents a SPI bus, and that I’m writing a driver for a specific ADC device that’s connected to my SPI bus. So far, that may look something like this:

enum class SpiError { /* Enumeration Literals... */ };

class Spi {
public:
  Spi() = default;
  // Destructor _may_ actually do something interesting.
  ~Spi();

  std::expected<std::monostate, SpiError> write(
    std::span<const uint8_t> data,
    uint8_t chip_select);

  // Delete the copy constructor
  Spi(const Spi&) = delete;
  Spi& operator=(const Spi&) = delete;

  // Moving is okay, though.
  Spi(Spi&&) = default;
  Spi& operator=(Spi&&) = default;

private:
  // Instance data...
};

class Adc {
public:
  Adc(Spi* spi, uint8_t chip_select) : spi_{spi}, chip_select_{chip_select} {}
  uint32_t read_channel(uint8_t channel_index);

private:
  Spi* spi_;
  uint8_t chip_select_;
};

We don’t want Adc to own an instance of Spi by value, because we need to share the Spi instance between multiple devices. Spi needs to implement resource locking and ensure that concurrent access to the bus is impossible.

There’s no problem with these classes yet. We can leave the default move and copy constructors–after all, they don’t own any resources, so it’s valid to move an instance of these objects.

…Until I do this:

class ApplicationState {
public:
  ApplicationState() : spi_{}, left_adc_{&spi_}, right_adc_{&spi_} {}

private:
  Spi spi_;
  Adc left_adc_;
  Adc right_adc_;
};

The move semantics of ApplicationState are constrained, but not automatically. When I create a Spi* member variable in the Adc class, I’m introducing a new class invariant on Adc: a borrowed lifetime. This isn’t Rust, but if it were, we would be forced to add a generic lifetime parameter on Adc, like this:

struct Adc<'a> {
  spi: &'a Spi,
  chip_select: u8,
}

So that the lifetime checker could ensure we are free of temporal memory safety issues. C++ has nothing like this. Granted, this kind of thing becomes easier to spot if you’re used to reviewing code for this (or perhaps if you’re a Rust programmer). It becomes harder at scale–when there are many member variables, or when we aren’t already familiar with the invariants on Spi and Adc–for example, if we didn’t write them.

Pinned Places

The issue is that the class invariant is placed on the instance data of Adc. We could make the class Spi immovable, but that’s not really correct. There’s nothing in the type Spi that makes it immovable. We may be able to coerce the C++ type system into helping us create a type that enforces this invariant. The concept of a pinned place may help us here.

We’ll start by introducing our type, Pin:

template<typename T>
concept Value = std::is_same_v<std::remove_reference_t<std::remove_pointer_t<T>>, T>;

template<Value T>
class Pin {
public:
    using reference_type = std::add_lvalue_reference_t<T>;
    using pointer_type = gsl::not_null<T*>;

    template<typename... Args>
    Pin(Args&&... args) : m_value{std::forward<Args>(args)...} {}
    ~Pin() = default;

    // Pinned objects intentionally have non-copyable/non-movable semantics.
    // Strictly speaking, copy semantics ought to be definable if T is copyable,
    // but default-ing them would restrict Pin to copyable types T. Semantically,
    // there is no reason why we should be able to copy a pinned object, so we
    // are safe to delete this.
    Pin(const Pin&) = delete;
    Pin& operator=(const Pin&) = delete;
    Pin(Pin&&) = delete;
    Pin& operator=(Pin&&) = delete;

    reference_type operator*() const noexcept(noexcept(*std::declval<pointer_type>())) { return m_value; }
    pointer_type operator->() noexcept { return &m_value; }

private:
    T m_value;
};

A Pin<T> object wraps an instance of a movable type T in an immovable container. For variables with automatic storage duration, this has the effect of “pinning” them on the stack. The noexcept declaration on the operators is not necessary for this discussion, but it allows the noexcept constraint on this operator to be inherited from the same operator on the type T, which is nice. Similarly, we use gsl::not_null for a little bit of extra safety. We never intend to return a null pointer, so it’s helpful to annotate that. Finally, you may notice that I created the concept Value to ensure that T is not a pointer or reference type. This would make the nested type declarations more complicated, and after all, “pinning” a reference type has no semantic value, so we disallow it.

Now, we need a type that will allow classes to require their callers to uphold the immovable invariant on owned instance data. We’ll call it PinPtr:

template<Value T>
class PinPtr {
public:
    using reference_type = std::add_lvalue_reference_t<T>;
    using pointer_type = gsl::not_null<T*>;

    // TODO: Constructors?

    reference_type operator*() const noexcept(noexcept(*std::declval<pointer_type>())) { return *m_value; }
    pointer_type operator->() const noexcept { return m_value; }

private:
    pointer_type m_value;
};

We’ll come back to the constructors in a moment. We can now rewrite Adc like this, and PinPtr acts just like any smart pointer type:

class Adc {
public:
  Adc(PinPtr<Spi> spi, uint8_t chip_select) : spi_{spi}, chip_select_{chip_select} {}
  uint32_t read_channel(uint8_t channel_index) {
    static constexpr std::array<uint8_t, 2> command = {0x08, 0x00};
    spi_->write(command, chip_select_);
  }

private:
  PinPtr<Spi> spi_;
  uint8_t chip_select_;
};

Adc is still movable, and so is Spi. But the idea is that now, when I write ApplicationState:

class ApplicationState {
public:
  ApplicationState() : spi_{}, left_adc_{spi_}, right_adc_{spi_} {}

private:
  Pin<Spi> spi_;
  Adc left_adc_;
  Adc right_adc_;
};

The move and copy constructors for ApplicationState are automatically deleted!

Constructing a PinPtr

The last thing is to ensure that it’s only possible to construct a PinPtr from a valid Pin<T>. For this, we need to recall how value categories work. Remember that an rvalue is either a temporary value that has no address, or a temporary value that is “expiring”. An lvalue is something that has an address, and is neither of these. How these are represented in the type system, however, may be surprising:

  • T& can only represent an lvalue.
  • T&& can only represent an rvalue.
  • const T& can represent either an lvalue or an rvalue.

The last point is critical! Objects of Pin<T> are only valid as lvalues, and so it’s only valid to construct a PinPtr<T> from a Pin<T>&–this is the only way that we can ensure the pinned object will remain valid after the constructor runs. With this in mind, we can add our constructors:

template<Value T>
class PinPtr {
public:
  template<typename U>
  PinPtr(Pin<U>& pin) : m_value{pin.operator->()} {}
};

We template the constructor to allow polymorphic pointers. We can construct a PinPtr<T> from a Pin<U> if U is a derived class of T. Also recall that defining this constructor implicitly deletes the default constructor, which is critical to enforcing this invariant.

Prevailing wisdom would have us declare the single-arg constructor as explicit, but I think that’s not necessary here. There is one–and only one–way to construct a PinPtr. Requiring an explicit constructor call would not add clarity, and would only add visual noise.

Does It Really Work, Though?

The answer seems to be yes!

class Resource {};

class Borrows {
public:
    Borrows(PinPtr<Resource> resource) : resource_{resource} {}
private:
    PinPtr<Resource> resource_;
};

int main() {
    Resource r;
    Pin<Resource> pinned;

    // These fail to compile:
    // Borrows borrower{Pin<Resource>{Resource{}}};
    // Borrows borrower{&r};
    // Borrows borrower{Resource{}};

    // This is the only way to construct a Borrows:
    Borrows borrower{pinned};
}

Conclusion

It’s not a perfect bolt-on solution for temporal memory safety. In the previous example, PinPtr cannot ensure that the lifetime of pinned outlives the lifetime of borrower. Now that we’re protected from pointer invalidation by move/destruction, however, other temporal memory safety issues are theoretically harder to invoke accidentally, and may be easier to locate during code inspection.

Whether this adds value, though, or visual noise, is up to you. It may feel odd to represent a new semantic value category using a vocabulary type. If that’s the case, this likely isn’t for you! I think it has the potential to prevent a number of nasty UB issues, however, so I think I’m a fan of it! After trying to use it, I’ll give an update to see if that turned out to be the case.