Strings as Template Parameters in C++: A Compile-Time Guide

Ever wanted to pass a string, like "hello", as a template parameter in C++? If you've tried, you've likely run into a frustrating compiler error. Standard C++ doesn't allow string literals or const char* as template parameters. But with modern C++ features, we can build an elegant solution.

# The Core Problem: Why Can't We Use String Literals?

To understand why we need our StringLiteral class, we first need to look at Non-Type Template Parameters (NTTPs). These aren't new, they've been a fundamental part of C++ since C++98.

You've already used them, probably without thinking much about it. The most common example is std::array.


#include <array>

// T is a TYPE parameter (e.g., int)
// N is a NON-TYPE parameter (e.g., 5)
template<typename T, size_t N>
class std::array;  

// We provide a type 'int' and a value '10'
std::array<int, 10> my_array;
                

For decades, the C++ standard was very strict about what kinds of non-type parameters were allowed in template arguments.. Before C++20, the list was mostly limited to:

  • Integral types (like int, size_t, bool).
  • Enumeration types.
  • Pointers and references to objects or functions with external linkage.

A string literal, like "hello", has internal linkage. This means it's essentially private to its source file (.cpp file). The compiler is free to store it however it wants, and it might even merge identical string literals from different files into a single location. Because a string literal doesn't have a stable, unique address across the entire program (i.e., external linkage), a pointer to it was forbidden as a template argument.

Due to this limitation we couldn't just write MyTemplate<"hello">. The rules of the language forbade it. We were stuck—until C++20 expanded the rules for what kind of types could be used as NTTPs, opening the door for our StringLiteral solution.

# The Solution: C++20 and constexpr

So, how do we solve the linkage problem? We pack the string's content into a simple struct that the compiler can understand at compile time. Two key C++ features make this possible:

  • C++20 - Class Types as NTTPs: This is the big one. C++20 finally relaxed the rules and allowed user-defined types to be used as NTTPs, as long as they are structural types. Think of a structural type as a simple data bucket—a struct with public members, no complex inheritance, and a straightforward layout.

  • C++11 - constexpr: This keyword lets us run code at compile time. We need a constexpr constructor to build our string struct during compilation, effectively "lifting" the string's value from runtime into the type system.

# Crafting the StringLiteral Struct

Here's the struct that puts these ideas into practice. It's a wrapper that holds the string's characters in a way that satisfies the compiler's rules.


#include <cstddef>         // for size_t
#include <string_view>     // for std::string_view

template <size_t N>
struct StringLiteral {
    char value[N]{};

    // The compile-time constructor
    constexpr StringLiteral(const char(&str)[N]) noexcept {
        for (size_t i = 0; i < N; ++i) {
            value[i] = str[i];
        }
    }

    // Handy functions to access the data
    constexpr std::string_view View() const noexcept { return { value, N - 1 }; }
    constexpr size_t Size() const noexcept { return N - 1; }
    // ... other helpers like operator[] ...
};
                

Let's break down the important parts:

  • template <size_t N>: Just like std::array, our struct is templated on the size of the string (including the null terminator \0).

  • char value[N]{};: This is the simple data bucket. It's a plain C-style array that will hold the characters of our string. Because it's a public member of a simple struct, it meets the "structural type" requirement.

  • constexpr StringLiteral(...): This is the magic. It's a constructor that can run at compile time. When you give it a string literal, it loops through every character and copies it into the value array. This whole process happens before our program even runs.

# Results: Strings as Template Parameters

With our StringLiteral struct ready, we can finally do what seemed impossible before. We can pass a string directly as a template argument.

Let's define a function that takes a StringLiteral as a template parameter:


#include <iostream>

template <StringLiteral S>
void logMessage() {
    std::cout << "[LOG]: " << S.View() << std::endl;
}
                

Now, for the magic moment (you'll need a C++20 compiler):


int main() {
    logMessage<"Application Started">();
    logMessage<"Processing Data...">();
}
                

When the compiler sees logMessage<"Application Started">(), it performs a beautiful sequence of compile-time operations:

  • It analyzes the literal "Application Started" and determines its type is const char[20].

  • It uses this information to call our constexpr constructor, creating a StringLiteral<20> object right then and there.

  • This newly created, compile-time object is then used as the template argument S.

All of this is resolved during compilation, giving us zero-overhead, type-safe, compile-time strings.

# A Touch of Convenience: C++17's CTAD

While using StringLiteral as a template argument is clean, defining it as a local variable used to be a bit clunky. You had to explicitly provide the size:


// Before C++17, you had to do this:
StringLiteral<6> myStr{"hello"};                    // Verbose!
                

Thankfully, Class Template Argument Deduction (CTAD), introduced in C++17, simplifies this. CTAD allows the compiler to deduce a class's template arguments from its constructor arguments, just like it does for functions.

Because our constructor StringLiteral(const char(&str)[N]) makes the value of N perfectly clear from the input, we can now write:


// With C++17 CTAD, it's this simple:
StringLiteral myStr{"hello"};                          // The compiler deduces N = 6 automatically!
                

This makes our StringLiteral class feel like a natural, built-in part of the language.

# Conclusion

By standing on the shoulders of giants (the C++ standards committee), we've overcome a classic language limitation. The journey involved combining features from three major C++ releases:

  • constexpr (C++11) gave us the ability to execute code at compile time.

  • CTAD (C++17) gave us a much cleaner and more intuitive syntax.

  • Class Types as NTTPs (C++20) provided the final key, allowing our custom struct to be passed as a template parameter.

This solution perfectly illustrates how modern C++ continues to deliver powerful features that solve long-standing challenges and dramatically improve the quality of life for developers, especially in the world of template metaprogramming.

If you'd like to see the full implementation, this StringLiteral is part of my QkTraits library. Feel free to explore the complete code on GitHub!

StringLiteral in QkTraits on GitHub

# Further Notes for the Curious

  • Before C++20, libraries like Boost.Hana and other template metaprogramming techniques simulated compile-time strings using clever tricks (like parameter packs).

  • Compilers had uneven support at first: Clang and GCC adopted class-type NTTPs quickly, while MSVC lagged behind in early versions.

  • Real-world use cases go beyond logging: reflection systems, compile-time configuration, or constexpr lookup tables often rely on strings as NTTPs.

I'll be covering some of these advanced scenarios in upcoming deep dives.

Stick around if you're curious about where this goes!