Logo

Blog


A string class that is only instantiable at compile-time

Do you know this, sometimes you have a class that should only be instantiable at compile-time? By that, you can build several assumptions on such a class. For example, that the data is read-only, not on the local stack, nor dynamic memory.

Using some code to illustrate the situation, if you look at the function signature below, the requirement is that the first parameter key contains data that persist during the entire run-time of the program.

1
void Insert(Literal key, int value);

Let's assume we can achieve that, then that knowledge can lead to efficient designs like this string can be used as a key in a data structure where instead of a copy the string, you can store the pointer to the string since the data will always be valid.

The question is how to achieve this. And know what? The answer is: it depends! Of course! But on what? Well, the C++ standard.

Let's start with the latest version, C++20, before we look at a C++17 solution.

A C++20 and later approach

C++20 comes with the right tool here, the missing piece to the constexpr-world, the consteval keyword. While constexpr implies execution at compile- or run-time, consteval or immediate functions are only callable during compile-time.

With that knowledge, here is a C++20 implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Literal {
  const char* const mData{};
  const size_t      mLen{};

public:
  consteval Literal(const char* data)
  : mData{data}
  , mLen{std::char_traits<char>::length(data)}
  {}

  // imagine the access functions you need
};

Here the new feature consteval is handy. You mark the constructor of the class as consteval to ensure that this constructor is invokable only at compile time. This gives you an object that must be created at compile time. While the object itself might go out of scope, the data that the object refers to doesn't.

Here is how you use call Insert:

1
Insert("Hello", 20);

We're done. That was fast, right? If you still have time or if you're using C++17, have a look at the next section.

A C++17 approach (or earlier)

As wonderful as consteval is, what if you're still at C++17? constexpr alone will not do the trick due to its dual nature someone can invoke a constexpr constructor at compile- and run-time.

There is a trick you can employ to achieve the same result. Since C++11, we have user-defined literals and the corresponding new operator, the literal operator. The great thing about the literal operator is that it only takes compile-time values as input. Essential, strings or numbers. Both inputs must be known at compile time. That matches our requirement. But what about the constructor?

The change you have to make is moving the constructor into the protected or private scope. Which one is up to you. If you additionally make the literal operator for that class a friend, this operator is the only way to instantiate that class. Voila, this is your solution to a class that has the guarantee to be only instantiable at compile-time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Literal {
  const char* const mData{};
  const size_t      mLen{};

  friend constexpr Literal operator""_l(const char* data, size_t len);

  constexpr Literal(const char* data, size_t len)
  : mData{data}
  , mLen{len}
  {}

public:
  // imagine the access functions you need
};

constexpr Literal operator""_l(const char* data, size_t len)
{
  return {data, len};
}

This time you need the UDL when calling Insert:

1
Insert("Hello"_l, 20);

You can mix this approach with C++17s string_view, for example, by adding a conversion operator to a string_view object to the class Literal.

I hope this helps you at some point to sharpen your interfaces while still keeping the footprint of your binary low.

Andreas