Templates[temp]
Strings as C++ template parameters with C++20 fixed_string
- Section
- Templates [temp]
- Standard
- C++20
- Compilers
- MSVC 19.44, Clang 19.1
- Published
- Length
- 11 min read
In this answer9 clauses
- 1Why
Name<"id">is rejected - 2C++20: class types as template parameters
- 3A fixed_string that deduces its length
- 4Using the string at compile time
- 5A string literal operator:
"user_id"_name - 6Why std::string and std::string_view can’t be template parameters
- 7Before C++20: named arrays and character packs
- 8C++26: std::define_static_string
- 9What it costs
Abstract
A string literal can't be the argument of a const char* template parameter in any standard, but since C++20 it can initialize a class-type parameter, so wrap the characters in a fixed_string whose constructor takes const char (&)[N] and declare the parameter as template <fixed_string S>. Two arguments with the same characters are then the same argument, which the named static arrays of C++17 never gave you.

Name<"id"> does not compile when Name takes a const char*, in any standard. Since C++20 it compiles when the parameter is a class type that copies the characters: write a small fixed_string whose constructor takes const char (&)[N], declare the parameter as template <fixed_string S>, and class template argument deduction works out N from the literal. The whole class is a dozen lines, shown below.
This guide is written for C++20, the first standard that accepts class types as non-type template parameters (P0732R2, with the rules reworked by P1907R1). Before C++20 the choices were a named array with static storage, which C++17 made easier to declare, or a character pack split out of the literal. C++26 adds std::, which turns a string into a pointer that a const char* parameter accepts. Class types as template parameters need GCC 9, Clang 12 or Visual Studio 2019 16.6 (the C++20 table of Since When has the row and its sources). Every sample was compiled with MSVC 19.44 (/std:c++20 /EHsc /W4 /permissive-) and Clang 19.1 (-std=c++20 -Wall -Wextra, on Microsoft’s standard library) on one Windows x64 machine, without optimization unless the text says otherwise, and the C++17 sample also under C++17; the outputs and errors shown are what those builds produced there. Errors marked in comments were reported by both compilers unless the text names one, and every output is what both printed.
What changed: a string reaches a template as the address of a named array, as the literal itself, constexpr char id[] = "id";Name<"id">, through a class-type parameter.
since C++20
Why Name<"id"> is rejected
template <const char* S>
struct Name {};
Name<"id"> n; // error: a string literal can't be a template argument— end example]
The rule is in [temp.arg.nontype] of the working draft: a pointer or reference template argument must not point to a string literal object, a temporary, the result of typeid, __func__, or a subobject of any of those. Pointer arguments are compared by address, and [lex.string] calls string literal objects “potentially non-unique”: whether two "id" literals are the same object is unspecified. A literal argument would not even say which type it names. Clang 19 reports “pointer to subobject of string literal is not allowed in a template argument”; MSVC 19.44 reports error C2762, an invalid expression as a template argument.
C++20: class types as template parameters
Since C++20 a non-type template parameter can have a class type, as long as the class is a structural type. For a class, cppreference’s template parameters page and the draft’s [temp.param] give the same three conditions:
- it is a literal class type, so it can be created in a constant expression;
- every base class and non-static data member is public and not
mutable; - every base and member is itself structural: a scalar, an lvalue reference, a structural class, or an array of those.
The argument is converted to the parameter’s type at compile time, and inside the template the parameter names a template parameter object: an object of type const T with static storage duration, one per distinct value. Two class-type arguments are the same argument when their members are, member by member, so an array member is compared element by element. That is exactly what a string needs. (The C++26 draft calls them constant template parameters.)
A fixed_string that deduces its length
#include <algorithm>
#include <cstddef>
#include <string_view>
#include <type_traits>
template <std::size_t N>
struct fixed_string {
char data[N] = {}; // N counts the terminating '\0'
constexpr fixed_string(const char (&s)[N]) {
std::copy_n(s, N, data);
}
constexpr std::size_t size() const { return N - 1; }
constexpr std::string_view view() const { return {data, N - 1}; }
};
template <fixed_string S>
struct Name {
static constexpr std::string_view value = S.view();
};
Name<"id"> a; // S is a fixed_string<3> holding "id"
static_assert(Name<"id">::value == "id");
static_assert(std::is_same_v<Name<"id">, Name<"id">>);
static_assert(!std::is_same_v<Name<"id">, Name<"ID">>);— end example]
What each part does:
template <fixed_string S>names the class template with no arguments, and C++20 deduces those arguments at each use:"id"has typeconst char[3], the constructor’sconst char (&)[N]matches it, and the implicit deduction guide makesSafixed_string<3>. No hand-written guide is needed.- The constructor is not
explicit, because the literal has to convert tofixed_stringimplicitly. datastays public. Make it private and the class stops being structural: both compilers then rejectName<"id">, MSVC with C2993 (“is not a valid type for non-type template parameter”) and Clang with “is not a structural type”.std::copy_nisconstexprsince C++20; a plain loop works as well.view()returns astd::string_viewinto the template parameter object, which has static storage, so the view cannot dangle.
The last two static_asserts are the point of the exercise: the same characters give the same type, and different characters give different types even at the same length.
Using the string at compile time
Inside the template, S is a constant like any other. You can branch on it with if constexpr, use it as a key, or check it in the declaration with a constraint:
template <fixed_string S>
requires (S.size() > 0 && S.size() <= 32)
struct Name {
static constexpr std::string_view value = S.view();
};
Name<"id"> ok;
// Name<""> empty; // error: constraints not satisfied— end example]
The requires clause turns a bad string into an error at the use: Clang 19 reports “constraints not satisfied for class template ‘Name’ [with S = fixed_string<1>{”“}]”, and MSVC 19.44 reports C7602. C++20 concepts: how to write and use them covers the other forms a constraint can take.
A string in a type also makes a compile-time key. Here a record holds fields named by strings, and get<"id"> finds a field by its name:
#include <iostream>
#include <string>
template <fixed_string Key, class T>
struct field {
T value;
};
template <class... Fields>
struct record : Fields... {};
template <fixed_string Key, class T>
constexpr T& get(field<Key, T>& f) { return f.value; }
int main() {
record<field<"id", int>, field<"name", std::string>> user{
{42}, {"Ada"}};
get<"id">(user) += 1;
std::cout << get<"id">(user) << ' ' << get<"name">(user) << '\n';
// prints: 43 Ada
// get<"email">(user); // error: no field named "email"
}— end example]
get<"id"> fixes Key, and deduction finds T by matching field<"id", T> against the base classes of record. A name that isn’t there fails to compile with “no matching function” on both compilers. A template can go further and parse its string when it is instantiated, building a type from what it finds: a small language inside C++, the same job expression templates do with operators instead of text.
A string literal operator: "user_id"_name
C++20 also lets a literal operator template take a class-type parameter, which cppreference calls a string literal operator template. The compiler passes the literal as the template argument, so the suffix can return a value whose type carries the string:
template <fixed_string S>
constexpr Name<S> operator""_name() { return {}; }
auto n = "user_id"_name; // Name<"user_id">
static_assert(std::is_same_v<decltype(n), Name<"user_id">>);— end example]
Name here is the unconstrained one from the first sample. The suffix suits places that want a value rather than a type, such as a function argument.
Why std::string and std::string_view can’t be template parameters
#include <string>
#include <string_view>
template <std::string S> // error: not a structural type
struct A {};
template <std::string_view S> // error: not a structural type
struct B {};— end example]
Both compilers reject both, and give the reason: in Microsoft’s STL, which both used here, the classes have data members that aren’t public. Clang 19 says “is not a structural type because it has a non-static data member that is not public”; MSVC 19.44 says the member “is not a public, non-mutable, non-static data member”.
Two further reasons would remain even if the members were public. A std::string that owns heap memory can’t be a constant: C++20 lets a constant expression allocate only if it frees the memory before the evaluation ends. With Microsoft’s STL here, constexpr std:: compiled on both compilers, because the characters fit in the string object itself, and a string too long for that buffer was rejected; whether even a short one works depends on the library. A std::string_view is a pointer and a length, so two views of the same text would be compared by address, and a view of a literal would point into a string literal object, which [temp.arg.nontype] forbids. fixed_string avoids both by copying the characters into the object.
If you’d rather not write a class, std::array<char, N> is structural, and std::to_array (C++20) builds one from a literal:
#include <array>
template <std::array A> // C++20: std::array<char, N> is structural
struct Bytes {
static constexpr std::size_t size = A.size();
};
Bytes<std::to_array("id")> b; // A is std::array<char, 3>— end example]
The price is spelling std::to_array at every use and having no view() of your own.
Before C++20: named arrays and character packs
Until C++20 the portable way was to give the string a name and pass the array, which decays to a pointer:
#include <iostream>
#include <type_traits>
template <const char* S>
struct Name {
static constexpr const char* value = S;
};
constexpr char user_id[] = "user_id"; // namespace scope: has linkage
Name<user_id> a; // C++11 and later
constexpr char id1[] = "id";
constexpr char id2[] = "id";
static_assert(!std::is_same<Name<id1>, Name<id2>>::value,
"compared by address, not by contents");
void f() {
static constexpr char local[] = "local"; // no linkage
Name<local> b; // C++17 and later
std::cout << b.value << '\n';
}
int main() {
std::cout << a.value << '\n'; // prints: user_id
f(); // prints: local
}— end example]
C++11 and C++14 require the array to have linkage, which a constexpr array at namespace scope has (internal linkage). C++17 dropped that requirement, so a function-local static array works too: Clang 19 rejects Name<local> under -std=c++14 (“refers to object ‘local’ that does not have linkage”), while MSVC 19.44 accepts it even under /std:c++14. The cost is in the static_assert: id1 and id2 hold the same characters and still make two different types, because the arguments are addresses. Every string also needs a name outside the template before it can be used.
The other pre-C++20 approach holds the characters one by one in a pack, template <char... Cs>. The standard hands a literal to a character pack only for numeric literals (the numeric literal operator template, as in 123_x). For a string literal, this form is an extension:
template <char... Cs>
struct chars {};
// Not standard C++: a character pack for a string literal.
template <class Char, Char... Cs>
constexpr chars<Cs...> operator""_chars() { return {}; }
auto s = "id"_chars; // chars<'i', 'd'>, where it is accepted— end example]
Clang 19 accepts it with the warning “string literal operator templates are a GNU extension”; MSVC 19.44 rejects it with C3686. Portable code had to build the pack with a macro, typically one that indexed the literal up to a fixed maximum length. In C++20 code, fixed_string replaces all of these.
C++26: std::define_static_string
Neither compiler used for this guide implements it, so this section has no sample and nothing in it was compiled here. P3491R3 (the paper) added three functions that are now in the C++26 working draft as [meta.define.static]: std::, std:: and std::, declared in <meta> in namespace std. define_static_string runs at compile time, takes a range of characters and returns a const char* to a null-terminated array with static storage duration. That array is not a string literal, so the pointer is a valid argument for a const char* parameter. The paper’s own example: given template <const char* P> struct C {};, C<define_static_string("yay")> is valid and C<"nope"> is still ill-formed.
Since When has no row for it yet: Microsoft’s conformance page has no C++26 language rows, so the lookup’s C++26 table holds library features only. Look for P3491R3, and for the reflection paper it builds on, P2996, on your compiler’s status page. A standard fixed string class has also been proposed, std:: in P3094 (revision 6, January 2025); the committee’s paper tracker files it under C++29 and marks it as needing revision, so it is not part of C++26. Until one of them reaches your compiler, the dozen lines of fixed_string above are the answer.
What it costs
- One instantiation per string. Every template that takes the string, and every template it passes the string on to, is instantiated again for each distinct value, with one template parameter object each. A few dozen keys cost little; for thousands, measure your build times and binary size rather than guess.
- The characters are in the type’s name. On both compilers here, the
typeidname ofName<"user_id">isstruct Name<struct fixed_string<8>{char{117,, and Clang spells the argument115, 101, 114, 95, 105, 100, 0}}> fixed_string<8>{"user_id"}in its diagnostics. The characters end up in symbol names and error messages. - You may not need a template parameter at all. To check a string at compile time without changing a type, a
constevalconstructor is enough, with one function for every string.std::formatchecks its format string this way, through theconstevalconstructor of its format-string parameter type,std::format_string(P2216R3, which the committee applied to C++20 as a defect report).
#include <iostream>
#include <string_view>
struct key {
std::string_view text;
consteval key(const char* s) : text(s) {
if (text.empty() || text.size() > 32)
throw "key must be 1 to 32 characters";
}
};
void lookup(key k) { std::cout << k.text << '\n'; }
int main() {
lookup("user_id"); // checked at compile time, one function
// lookup(""); // error: not a constant expression
}— end example]
The throw never runs at run time: if constant evaluation reaches it, the call is not a constant expression, so lookup("") fails to compile (MSVC 19.44 C7595, Clang 19 “call to consteval function ‘key::key’ is not a constant expression”). To see what the compiler makes of any of these samples, paste them into Compiler Explorer or C++ Insights, both on Toolchain, and for more generic code see Templates.
Questions
Why can’t a string literal be a template argument?
The standard forbids a pointer or reference template argument that points to a string literal object ([temp.arg.nontype]). Pointer arguments are compared by address, and whether two identical literals are one object or two is unspecified, so a literal would not reliably name one type. Since C++20, pass the literal to a class-type parameter such as fixed_string, which copies the characters.
Can std::string be a non-type template parameter?
No. A template parameter needs a structural type, and std::string has private members; both MSVC 19.44 and Clang 19 reject it with that reason. It also owns heap memory, which C++20 lets a constant expression allocate only if it frees it before the evaluation ends. Use a fixed_string parameter and convert to std::string where you need one.
Can std::string_view be a template parameter?
No, for the same reason: its members are private, so it is not a structural type. Even if it were, it holds a pointer, so two views of the same text would compare by address, and it could not point into a string literal. Keep a fixed_string as the parameter and give it a member function that returns a std::string_view.
Which compilers support class types as template parameters?
GCC 9, Clang 12 and Visual Studio 2019 16.6, according to the vendors’ status pages as collected in Since When; build with -std=c++20, or /std:c++20 from Visual Studio 2019 16.11 (/std:c++latest before it). Clang lists the follow-up paper P1907R1 as partial, and Clang 19 still reports __cpp_nontype_template_args as 201411L, while MSVC 19.44 reports 201911L. Every working sample in this guide compiled on both.
Sources
- Template parameters and template argumentscppreference.com
- [temp.arg.nontype] Constant template argumentsC++ standard working draft
- User-defined literalscppreference.com
- P3491R3: define_static_{string,object,array}WG21,