Braden++

A string literal as a template argument

But first, a preamble

This is my backstory before the recipe in this metaphorical cookbook.

I was first introduced to the idea of parser combinators in a CppCon talk not about parser combinators, at least not as the main feature. Ben Deane and Jason Turner’s famous “constexpr ALL the Things!” talk opened my eyes to the magic that’s possible with parser combinators, and I wanted to do it too! I tried and failed to replicate what they had, then gave it up.

Another magical idea, expression templates, was introduced to me in Joel Falcou’s two-part talk “Expression Templates: Past, Present, Future”. Then I was determined to use expression templates in a 2nd attempt at creating parser combinators. Maybe this time I’ll succeed by using a more complex method than before! (sarcasm of course)

I tried again, and gave up again. This stuff is hard, and I wasn’t very experienced.

Then in summer 2022 I decided to give it another try. It’s a fun challenge, for some definition of “fun” at least. With this attempt, I got it. I don’t know what was the secret sauce that made a difference, but I want to talk about it here. I’m too excited not to share!


A string, but make it static

Thanks for indulging me above.

I wanted to create an ergonomic interface where a string literal can be passed as a template argument. This 3rd key piece of magic was introduced to me by Hana Dusikova’s CTRE library.

To pass a string literal as a template argument, the parameter must be another type that’s implicitly constructed from a string literal, to be used as a non-type template parameter (NTTP). Simple enough. But this type also needs to satisfy certain requirements so it can be used as an NTTP. Put simply, it needs to be constexpr constructible and its data members must all be public. This is a simplification, but it’s all that matters for our purposes here. Note that this only works in C++20 and beyond.

To get started, we know that a string is conceptually just an array of chars, so let’s create a struct to store an array of chars. Depending on the size of the string literal, the array needs to hold a different number of chars, so we’ll template this class on a std::size_t parameter. I’m calling it StaticString.

template <std::size_t N>
struct StaticString
{
    std::array<char, N> data = {};
};

This is a good start. As-is, this type can already be used as an NTTP, even without adding constructors and other functions. But that’s not what I’m looking for. I want automatic conversion from a string literal to a StaticString.


Adding functionality

Next we need to create a constructor that accepts a string literal, which is just syntax sugar for a C array of chars. In C++ you can’t pass a C array as a parameter by value, but you can take a C array by reference. Let’s do that. And remember to make it constexpr.

template <std::size_t N>
struct StaticString
{
    std::array<char, N + 1> data = {};

    constexpr StaticString(const char (&input)[N + 1])
    {
        std::ranges::copy_n(input, N + 1, data.begin());
    }
};

I would love to use std::memcpy() here instead, but that function isn’t constexpr. So it’s either a raw for loop or a std::ranges solution. I chose std::ranges. I’ll eventually need other constructors and other functions, but I’ll mention those when they’re needed in later articles.

One more thing the type needs: deduction guides, so that we don’t need to specify the template parameter.

template <std::size_t N>
StaticString(const char(&)[N]) -> StaticString<N - 1>;

I want to ensure that, for example, StaticString("abc") creates a StaticString<3>. It’s always important to remember that the string literal "abc" has a size of 4, not 3, because of the null terminator.

Alright, let’s use it!


One final piece of magic

template <StaticString str>
void foo()
{
    for (char c : str.data)
        std::cout << c;
    std::cout << "\n";
};

int main()
{
    foo<"abc">();  // > abc
    foo<"wxyz">(); // > wxyz
}

Of course you would never write this function in real life, but it’s a demonstration of functioning code.

Here, we use StaticString as the NTTP, instead of StaticString<N> with a value for N. That means foo() is templated on every string literal, and not just the string literals of one particular size. And even better, we don’t need to specify the size at the call site! I would love to know which part of the C++ standards document allows it. I’m sure it’s somewhere in [temp.arg.nontype], but I’m not well-versed enough in standardese yet.

I’ve been using this type as a foundation to create other types where all the data is stored in the type itself, instead of using data members. I’ll talk about compile time and run time performance at some other point. For now, it’s fun and I’ve been enjoying the process.

I won’t claim that my library is the best in any metric, but it’s exciting to be able to call it my own. I want to document every aspect of my library in this blog, from the ground up, starting with this humble first blog post. I hope you’ll come along with me.

Here is the code written in this article. Subsequent articles will continue to build on it, writing a library from the ground up.