Reinventing the wheel, 001 spokes at a time

Ok, let me practice a bit my written English by starting a series on how I plan to reinvent the wheel while actually doing it. I could say that I’m inspired in this by the likes of Casey Muratori, and I do have a great admiration for the guys who actually work their minds on how to make better use of the hardware and how to improve the software we write. I am not in that category, however, I have different goals, and I’m doing it because I always wanted to do this.

So here’s my plan. I realized that in the beginning of my career I was not reusing a lot. I looked a few years ago over an archive of projects I was carrying around as my portfolio, and they are all shameful examples of lack of reuse. I was reimplementing the same data structures all the time, I was re-doing them (and doing a bad job at it) being convinced that by writing them from scratch I’ll have better performance, or better… I don’t know. Realistically, I wasn’t thinking about performance, because I wasn’t doing the very basic gesture of actually measuring. Also, a lot of stuff I was reinventing simply because I didn’t like the APIs of the libraries I was using. Documentation was very lacking in those days (late 90s, early 00s), and it was hard to get a corpus of „the best way to…” because advice was expensive. Think about this - in 2000 you could give 2000 German Marks for a small apartment in Romania, while a book on C++ programming was about 150-200 Marks? (I will not get into the topic of piracy and the photocopied books that were moving around this time, though).

That being said, I was also a stubborn person, and I was really bad at learning from people I didn’t like. And a lot of good advice came from people I didn’t like, putting me in the awkward position of dodging some horrible bullets like most of Microsoft toolsets or Java, but also being stubborn enough to not accept good advice as well.

I guess some things never change. And despite the advice I would offer to myself, I started today writing my own C++ core functionality library because not only am I stubborn but also because in the age of AI we should send a „fuck you” to the productivity guys that want us working all the time for „the most, the bestest, the absolut greatest”. So let’s write a new C++ core library to make all the efficient and productive programmers cringe in their favorite language, which is Rust. Fuck Rust too.

Now, that we’re sending happy thoughts around the world, let’s get back to what matters. Which is, look at me, I’m writing something as dumb as a new library. And we start with the most important part, which is writing a proper smart pointer. And because we like good ideas, let’s make two of them, something that will make any STL developer cringe.

The unique_ptr replacements

If you’re not using smart pointers when writing C++ you’re doing it wrong. If there is one reason why you should switch from C to C++ is to use smart pointers and collections, but I’m starting my story from the smart pointer that is the most basic piece here.

So my first idea was to write a way to allocate and deallocate safely memory. So, obviously, I loved the unique_ptr, and I love its versatility. I will not intend to replicate it’s versatility, though, because I think that this smart pointer in particular has some really nasty defaults that I want to avoid.

So, let’s draw up some new requirements. We have to goal of allocating memory for objects(! take note later) safely. I will replicate the make_unique interface to a certain level, but I think that we don’t want to overload the functionality too much. We just want to allocate an object, deallocate it automatically when it goes out of scope, and make sure that we keep the only reference to it. We want to keep the get and release calls, but we should rarely need these. It’s the sort of call I’d rather not use, but if I need it I want it to be there.

I tried to find a better name because the standard library guys are notoriously bad at good namings. Btw, I played a bit with some C# and I liked some things in there, so I will use the PascalCase for class names (no leading C because I’m not an idiot). I will also use snake_case for functions and variables, and m_snake_case for members. Hopefully, no globals or other stuff like that. So in this case, the name UniquePtr seems the best, since UniquePointer is too long and the other name, HeapAllocated doesn’t really cover the secondary function of unique_ptr which is to offer a possibility to wrap C-style libraries resources. And I think that the best name for this entity is UniqueReferenceToHeapAllocatedMemory or the acronym Urtham which sounds like an awesome villain that repents himself in act III and aids your hero against the forces of evil.

But unique_ptr is quite versatile - versatile enough so that you can proverbially shoot yourself in the foot, as the tradition obliges every C++ developer. A unique_ptr can also cover an allocation of multiple objects that you can access via operator[](size_t), giving you the possibility to have a virtual UniqueArrayPtr. Ha, see what I did there? So yes, this is why the subtitle contains replacements.

The job seems fairly easy and the unique_ptr is relatively easy to understand in the STL codebase, so we can steal shamelessly a few ideas.

The implementation

A simple implementation for a smart pointer would be, of course, to implement the proper flow elements:

  • we can construct a UniquePtr with a preallocated pointer (inside what we’ll call the make_ptr call). This allows us to take care of pointers that are not created by our application directly, things like FILE* if we really need to. Although I think that we should adopt a different strategy for wrapping things like FILE* (like having a File class, as even the designers of the C standard library intended).
  • we should make sure that on the destructor we deallocate the pointer. Of course, this should be noexcept, like all destructors in the world.
  • we should implement assignment operators - and make sure that no copies are allowed.
  • we should implement the special operators like -> and *, so we can have expressions like ptr->foo() and pass the *ptr as a (preferably const) reference to some function.
  • get and release are also pretty straight forward.

Now, the cutest part about the UniquePtr is that it could be used with custom deleters. At least for the time being, I opted for custom deleters without state, so you can only pass there a Functor class that has no other state associated with it. It’s enough for the time being to support a simple DefaultDeleter that only calls delete on the pointer.

The same goes for the UniqueArrayPtr, however, we should be a bit more strict here. So the changes from UniquePtr would be that we will no longer offer -> and *. These operations don’t make sense for multiple allocations (even if those allocations are a number of 1). Instead, we’ll offer an indexer with operator[](size_t) that should satisfy our needs of accessing such an array. Of course, the access through the index can be nicely checked because we can add the m_size field to the UniqueArrayPtr. A custom deleter can be passed as well when building these things.

The code can be found here. I’m not sure for how long, as this is the first step in a larger project. But I will paste the code at the current version below as well. I think that there are a few things that I can say about the piece of code I wrote here. It’s clear and clean. It has a few more features than I intend to use, so in time I might actually remove some things.

The deleter story

I’m not really happy with the Deleter that I wrote for this. I’m not sure how I will use this in the future, and I think that it’s good to abstract this away in a separate call that will be inlined. I can see one or two uses, but maybe in the future I’ll just drop it. The result will be source compatible with anything that doesn’t use a custom deleter, but if I have something using that custom deleter then there’s no point in using it, right?

There’s another thing that only the future will tell me - if I need to actually have an instance of the Deleter inside the smart pointer. This can be done easily right now with no memory penalty on the class. For example

template <typename T>
struct DefaultDeleter {
  DefaultDeleter() noexcept = default;
  void operator()(T* ptr) { delete ptr; }
};
template <typename T, class Deleter = DefaultDeleter<T>>
class UniquePtr {
  T* m_ptr = nullptr;
  Deleter m_deleter;
public:
  constexpr UniquePtr(T* ptr = nullptr, Deleter deleter={}) : m_ptr(ptr), m_deleter(deleter) {}
  ...

The size of this is the same as the size of a T*, because the DefaultDeleter<T> has no state of its own, and it can be optimized away. This would allow me to pass some state as well, if necessary - an allocator, for example, or a lambda function (that usually carries some state). However, since I don’t need this feature for real for the time being, it will have to do without that Deleter instance. We can get away without that deleter on the UniqueArrayPtr as well.

In a sense, I’m taking advantage of one of the design issues of C++ - relying on global state to be able to perform the deallocation without other information. This will not work if the allocator/deleter will have its own state, for example the new-ish pmr (polymorphic_allocator).

This brings me to some other problem.

Allocate space for future instantiation

If I allocate many items at once, one thing that bothers me is that constructors are called for all the objects. Which might be quite slow especially if you have complex initializers that don’t set everything to 0. So I might actually want to just allocate the memory and create things later in-place with placement-new. This will definitely be the strategy for the kl::Array (not vector, you heathens!).

For the time being I don’t need this feature, but I’m taking it into account. In all honesty, I’ll think about it after I have the next step of my wheel reinvention - a reference counted pointer. But since I don’t need to work myself to exhaustion for your corporate overlords, that will happen when I’ll feel like continuing this effort.

If you want to comment about it, hmu on Mastodon, or Bluesky. I don’t social media much these days, but I might even see a message thrown at me on Xitter or Facebook. But don’t bother that much, Mastodon and Bsky should be enough. Or don’t bother at all, I might not care about your opinion.

Finally, the code

#pragma once
#include <kl/klexcept.hpp>

namespace kl {

template <typename T>
struct DefaultDeleter {
  DefaultDeleter() noexcept = default;
  void operator()(T* ptr) { delete ptr; }
};

template <typename T>
struct DefaultMultiDeleter {
  DefaultMultiDeleter() noexcept = default;
  void operator()(T* ptr) { delete[] ptr; }
};

template <typename T, class Deleter = DefaultDeleter<T>>
class UniquePtr {
  T* m_ptr = nullptr;

public:
  constexpr UniquePtr(T* ptr = nullptr) : m_ptr(ptr) {}
  constexpr UniquePtr(UniquePtr&& ptr) { m_ptr = ptr.release(); }
  UniquePtr(const UniquePtr& ptr) = delete;
  constexpr UniquePtr& operator=(UniquePtr&& ptr) {
    if (this != &ptr) {
      reset();
      m_ptr = ptr.m_ptr;
      ptr.m_ptr = nullptr;
    }
    return *this;
  }
  UniquePtr& operator=(const UniquePtr& ptr) = delete;
  constexpr UniquePtr& operator=(nullptr_t ptr) {
    reset();
    return *this;
  }

  constexpr ~UniquePtr() { reset(); }

  constexpr T* operator->() {
    if (m_ptr == nullptr) [[unlikely]] {
      throw RuntimeError("Null reference");
    }
    return m_ptr;
  }

  constexpr T& operator*() {
    if (m_ptr == nullptr) [[unlikely]] {
      throw RuntimeError("Null reference");
    }
    return *m_ptr;
  }

  constexpr T* get() const { return m_ptr; }
  constexpr T* release() {
    auto ptr = m_ptr;
    m_ptr = nullptr;
    return ptr;
  }

  constexpr void reset() {
    if (m_ptr != nullptr) [[likely]] {
      Deleter()(m_ptr);
      m_ptr = nullptr;
    }
  }
};

template <typename T, class Deleter = DefaultMultiDeleter<T>>
class UniqueArrayPtr {
  T* m_ptr = nullptr;
  size_t m_size = 0;

public:
  constexpr UniqueArrayPtr() noexcept = default;
  constexpr UniqueArrayPtr(T* ptr, size_t size) : m_ptr(ptr), m_size(size) {}
  constexpr UniqueArrayPtr(UniqueArrayPtr&& ptr) {
    m_size = ptr.m_size;
    m_ptr = ptr.release();
  }
  UniqueArrayPtr(const UniqueArrayPtr& ptr) = delete;
  constexpr UniqueArrayPtr& operator=(UniqueArrayPtr&& ptr) {
    if (this != &ptr) {
      reset();
      m_size = ptr.m_size;
      m_ptr = ptr.release();
    }
    return *this;
  }
  UniqueArrayPtr& operator=(const UniqueArrayPtr& ptr) = delete;
  constexpr UniqueArrayPtr& operator=(nullptr_t ptr) {
    reset();
    return *this;
  }

  constexpr ~UniqueArrayPtr() { reset(); }

  constexpr T& operator[](size_t index) {
    if (m_ptr == nullptr) [[unlikely]] {
      throw RuntimeError("Null reference");
    }
    if (index >= m_size) [[unlikely]] {
      throw RuntimeError("Out of range");
    }
    return *m_ptr;
  }

  constexpr T* get() const { return m_ptr; }
  size_t size() const { return m_size; }
  constexpr T* release() {
    auto ptr = m_ptr;
    m_ptr = nullptr;
    m_size = 0;
    return ptr;
  }

  constexpr void reset() {
    if (m_ptr != nullptr) [[likely]] {
      Deleter()(m_ptr);
      m_ptr = nullptr;
      m_size = 0;
    }
  }
};

template <typename T, typename... Args>
constexpr UniquePtr<T> make_ptr(Args&&... args) {
  return UniquePtr<T>(new T(std::forward<Args>(args)...));
}

template <typename T>
constexpr UniqueArrayPtr<T> make_array_ptr(size_t size) {
  return UniqueArrayPtr<T>(new T[size], size);
}

} // namespace kl