Extending constant template parameter support by customizing std::meta::reflect_constant

Document #: P4340R0 [Latest] [Status]
Date: 2026-08-11
Project: Programming Language C++
Audience: EWG
Reply-to: Barry Revzin
<>

1 Introduction

This is a follow-up to [P2484R0] (Extending class types as non-type template parameters) and [P3380R1] (Extending support for class types as non-type template parameters) (the latter of which starts with a useful reading list), and is a new solution to that problem building upon three insights:

The goal here is to allow types to opt-in to being usable as constant template parameters in a way that’s forward-looking to containers as well. This proposal will also make the following standard library types usable: std::optional, std::expected, std::variant, std::tuple, std::string_view, std::span, most types in <chrono>, std::complex, the comparison categories, std::reference_wrapper, std::inplace_vector, and std::bitset. Additionally, string literals will also become usable.

2 Design

One of the many consteval functions added to the standard library by [P2996R13] (Reflection for C++26) was std::meta::reflect_constant(v). This is a very interesting function in that, if v has class type, this returns a reflection representing the template parameter object that is template-argument-equivalent to v.

That is, for all class types that are today usable as constant template parameters, this comparison holds (let’s agree to ignore that I’m not using std::addressof):

template <auto V>
constexpr void const* addr = &V;

static_assert(addr<E> == &[: std::meta::reflect_constant(E) :]);

Ultimately, the whole problem of how to opt an arbitrary type with non-public subobjects into being usable as a constant template parameter is about this question of how to produce the template parameter object. What if we cut out the middle-man and simply allowed (forced) types to directly produce that template parameter object?

Here is a very simple example:

class Widget {
    // private, so not usable by default
    int data_;

public:
    constexpr Widget(int d) : data_(d) { }

    consteval auto reflect_constant() const -> std::meta::info;
};

template <int D> constexpr auto impl = Widget(D);

consteval auto Widget::reflect_constant() const -> std::meta::info {
    return substitute(^^impl, {std::meta::reflect_constant(data_)});
}

Widget is defining how to produce a template parameter object: reflect_constant returns a reflection representing a static storage duration object. It is up to Widget to figure out how to do that, but once it does so, we have already solved all the other problems in this space:

std::meta::reflect_constant(v) will then be extended to first invoke v.reflect_constant() for class types that opt in before falling back to the normal C++20 structural rules.

2.1 Normalizing

Let’s go through two examples where we do something non-trivial: we want to do some normalization on the way in, so that we have fewer or more predictable specializations. Previous papers in this space brought up two examples:

Let’s start with Fraction. We need to make sure that we serialize it in lowest terms, so the arguments we’re substitute()-ing will have to be reduced:

struct Frac {
    int numer;
    int denom;
    consteval auto reflect_constant() const -> std::meta::info;
};
template <int N, int D> constexpr auto interned = Frac{N, D};

consteval auto Frac::reflect_constant() const -> std::meta::info {
    int g = std::gcd(numer, denom);
    return substitute(^^interned, {
        std::meta::reflect_constant(numer / g),
        std::meta::reflect_constant(denom / g),
    });
}

And the result we will end up with is:

template <auto V> inline constexpr auto const& ref = V;

// the template parameter object is {1, 2}, that's all we see
static_assert(ref<Frac{2, 4}>.numer == 1);
static_assert(ref<Frac{2, 4}>.denom == 2);

// there is only one template parameter object, so these
// alternate representations have the same address
static_assert(&ref<Frac{2, 4}> == &ref<Frac{3, 6}>);

Similarly, the SmallString container I showed in [P3380R1] wanted to ensure that regardless of what path you get to the same “value” that you only see the real value, and not any trailing characters. We can do that too:

class SmallString {
    char data_[32];
    int length_ = 0; // always <= 32

public:
    SmallString() = default;

    constexpr SmallString(std::initializer_list<char> elems)
        : data_(), length_(elems.size())
    {
        std::copy(elems.begin(), elems.end(), data_);
    }

    constexpr auto data() const -> char const* {
        return data_;
    }

    constexpr auto push_back(char c) -> void {
        assert(length_ < 31);
        data_[length_] = c;
        ++length_;
    }

    constexpr auto pop_back() -> void {
        assert(length_ > 0);
        --length_;
    }

    constexpr auto operator[](size_t idx) const -> char {
      return data_[idx];
    }

    consteval auto reflect_constant() const -> std::meta::info;
};

template <char... Cs>
inline constexpr SmallString impl = {Cs...};

consteval auto SmallString::reflect_constant() const -> std::meta::info {
    std::vector<std::meta::info> elems;
    for (size_t i = 0; i != length_; ++i) {
        elems.push_back(std::meta::reflect_constant(data_[i]));
    }
    return substitute(^^impl, elems);
}

We simply only serialize data[0:length] and not all of data[0:32]. The result is that the trailing data gets dropped and we end up with the same object:

constexpr auto f() -> SmallString {
    auto s = SmallString();
    s.push_back('x');
    return s;
}

constexpr auto g() -> SmallString {
    auto s = f();
    s.push_back('y');
    s.pop_back();
    return s;
}

static_assert(&ref<f()> == &ref<g()>);

// It doesn't matter that g()[1] == 'y', normalizing through the
// template parameter object gives us all trailing nulls
static_assert(ref<g()>[1] == '\0');

As a result, this example just isn’t an ODR issue, because bad<f()> and bad<g()> both evaluate the same way — they return 1.

template <SmallString S>
constexpr auto bad() -> int {
  if constexpr (S.data()[1] == 'y') {
    return 0;
  } else {
    return 1;
  }
}

2.2 Defaulting reflect_constant

While it’s important to be able to support the kind of arbitrary selection of serialization and normalization that I’ve shown in the previous three examples, in practice, for a large number of types, this kind of genericity is unnecessary. In fact, a lot of time, all we want to do is have a simple member-wise (or really subobject-wise) template-argument-equivalence.

That’s the C++20 rule we have today, except that rule requires that all the subobjects be public. That’s necessary, since we can’t otherwise distinguish between std::string_view and std::string, and those would definitely want to have very different rules around equivalence (the former comparing pointers, the latter comparing contents). But what if I want to both have private members and also have member-wise equivalence?

Take string_view. It’s straightforward enough to implement reflect_constant in a way that does what we want:

template <class T, auto... Vals>
constexpr auto impl = T(Vals...);

class string_view {
    char const* ptr_;
    size_t len_;

public:
    constexpr string_view(char const*, size_t);

    consteval auto reflect_constant() const -> std::meta::info {
        return substitute(^^impl, {
            ^^string_view,
            std::meta::reflect_constant(ptr_),
            std::meta::reflect_constant(len_),
        });
    }
};

This works fine, and we already have the constructor that we need to support this. But this is a lot of ceremony for what is basically the default behavior: serialize and then deserialize all of my members. Since that is a very reasonable default behavior, we should just allow default-ing this. This does mirror very nicely the way that other defaulted functions behave:

class string_view {
    char const* ptr_;
    size_t len_;

public:
    constexpr string_view(char const*, size_t);

    consteval auto reflect_constant() const -> std::meta::info = default;
};

We’ll want defaulting behavior for reflect_constant to be similar to defaulting behavior for other functions, like the copy constructor and the comparison operators: for dependent types, if one of the subobjects isn’t usable as a constant template parameter (whether it’s not C++20 structural or doesn’t customize reflect_constant), the resulting customization is deleted rather than being ill-formed. That means that for a class template like std::tuple, we can simply default the implementation and not have to worry about providing suitable constraints.

Note that a consequence of being able to default this function is that you should also be able to delete it, which means that a type could (if so desired) simply opt out of being usable as a constant template parameter.

Also, the actual implementation details of defaulting reflect_constant would differ from those of explicitly providing it. If it is user-provided, the implementation will of course invoke and use that result as the template parameter object. But if it is defaulted, then effectively the implementation just tracks an extra bit on the type that tells it to ignore the usual access rule for C++20 class types as non-type template parameters. It wouldn’t have to change the rules for template-argument-equivalence for that case.

2.3 Concrete Details

Currently, the rule is that f<x> and f<y> are the same specialization when x and y are template-argument-equivalent. For which the relevant rule from 13.6 Type equivalence [temp.type] for class types is:

2 Two values are template-argument-equivalent if they are of the same type and

  • (2.1) […]
  • (2.10) they are of union type and either they both have no active member or they have the same active member and their active members are template-argument-equivalent, or
  • (2.11) […], or
  • (2.12) they are of class type and their corresponding direct subobjects and reference members are template-argument-equivalent.

This proposal extends the those two bullets such that two values x and y of class type T (including union type) are template-argument equivalent if T provides a non-default reflect_constant customization and x.reflect_constant() == y.reflect_constant() (i.e. are reflections representing the same object).

In order for that to be viable, we need to impose strict (albeit obvious) restrictions on the return type of reflect_constant. For a class type T, T::reflect_constant must return a reflection representing either an object or a variable. Letting: O be either that object or the object defined by that variable:

reflect_constant must also be deterministic (same value in, same object out) and idempotent (given a value x, x.reflect_constant() and [: x.reflect_constant() :].reflect_constant() must be the same object). Idempotence we can actually check as part of the implementation, in the same way that template variable construction does an extra copy right now.

We’ll have the same rules for the shape of the customization point for defaulting reasons as we have for defaulting the comparison operators. That is, it must be:

Note that a type with mutable members is non-structural, regardless of any customization point.

2.4 Standard Library Extensions

There is one standard library type that I’m aware of that would actually provide a bespoke implementation of reflect_constant (see below), but there are quite a few for whom defaulting is the correct behavior. For the class templates below, defaulting will do the right thing by opting in those types whose subobjects are usable as constant template parameters and not opting in those types whose subobjects are not usable (note that std::pair and std::array are absent from this list, but that is only because they are already usable as constant template parameters):

For all of these types and class templates, adding the single line:

consteval auto reflect_constant() const -> std::meta::info = default;

Does the right thing and opts those types (or suitable specializations of those class templates) into being used as constant template parameters, with the correct semantics.

The only standard library type that we would add a non-defaulted reflect_constant to is std::inplace_vector<T, N>. Its customization point would look the same as std::vector’s (defined below).

2.5 Future Extensions with Containers

Notably absent from the above list are allocating containers, most significantly std::vector<T> and std::string. That’s because we do not yet have non-transient allocation and so cannot support them yet.

However, once we do have non-transient allocation, this paper provides a design that lets us easily provide constant template parameter support for them. Here is vector for instance:

template <class T, auto& R>
inline constexpr auto impl = T(std::from_range, R);

template <class T>
class vector {
public:
    consteval auto reflect_constant() const -> std::meta::info
        requires std::meta::is_structural_type(^^T)
    {
        return substitute(^^impl, {
            ^^vector,
            std::meta::reflect_constant_array(*this),
        });
    }
};

Note that this implicitly normalizes capacity — as long as the elements are the same, we will choose the same specialization of impl, which means we will end up with the same object (which will have whatever capacity it has).

If for some reason, you actually do want capacity to participate in all of this, that’s easy too — just also pass in capacity() as an additional serialization argument.

2.6 String Literals

One other type to cover not only isn’t in the standard library, but it isn’t even a class type: string literals. While char const* is usable as a template parameter, we cannot currently pass a string literal as a template argument.

The reason for this is that we do not ensure that "hello" is the same exact pointer across all translation units (and we will not start doing this now), and the result is that given a class template like:

template <char const* P>
struct X { };

There is no guarantee that X<"hello"> is the same type in every translation unit.

Now, [P0424R2] (String literals as non-type template parameters) already proposed the solution to this problem. Except that paper was dropped in favor of the more general [P0732R2] (Class Types in Non-Type Template Parameters) in the C++20 timeframe. But we still cannot pass in string literals, so I’m resurrecting the ideas behind the original paper.

In modern C++26 reflection terms, the way we need this to work is that when we pass a string literal as a template argument:

X<"hello">

That it needs to evaluate instead as:

X<[: std::meta::reflect_constant_string("hello") :]>

And this now ensures that, across all translation units, X<"hello"> is the same type. This is really the only way to ensure this property, and is the requirement for this to be allowed to work, so we should do it.

Note that it’s not quite that simple, because:

constexpr char const* a = "holdup";
constexpr char const* b = a + 3;
constexpr char const* c = "dup";

X<b> dup1;
X<c> dup2;

Do dup1 and dup2 have the same type? I would argue no, they do not. The transformation I expect is more like this:

template <char... Cs>
inline constexpr char const lit[] = {Cs..., '\0'};

X<lit<'h', 'o', 'l', 'd', 'u', 'p'> + 3> dup1;
X<lit<'d', 'u', 'p'>> dup2;

That is, just because the string contents of a + 3 and c are the same ("dup"), the pointers themselves are different, and template-argument-equivalence should preserve that. This is essential for another issue to bring up…

2.7 Normalization II

I talked about normalization earlier from the perspective of wanting to mutate an object down to its canonical representation, and only use that canonical representation as a template parameter object.

But there’s another perspective of normalization to consider: simple aggregation. What happens here:

struct Stuff {
    std::string_view s;
    int x;
};

template <Stuff S> int f();

int r = f<Stuff{.s="hello"sv, .x=1}>();

Stuff has all-public members, all of whom are usable as constant template parameters, so this should just work without Stuff having to do anything. But in order for this to work:

This all needs to basically… just happen as part of the template parameter object construction process. Otherwise composition completely breaks down.

This is also why the string literal rule has to have the form I described above. Imagine if std::string_view’s storage looked like this:

class string_view {
    char const* begin_;
    char const* end_;
};

In the above initialization, string_view("hello"sv) would have two pointers into the same string literal — so that needs to transform into (p, p + 5) where p points to a static storage duration array. It cannot turn into two pointers into different arrays. That would be horribly broken.

2.8 Naming

The name I’m choosing for the customization point here is reflect_constant, since we’re basically customizing std::meta::reflect_constant. There is precedent for using regular identifiers in language customization points already (begin, end, get, plus all the coroutine stuff). But with = default, it’s possible that this might just be a bit too much — and we’ll want to use a name that is decidedly not just an identifier.

The first paper in my constant template parameter trilogy, [P2848R0] (std::is_uniqued), used the name operator template(). Richard Smith also suggests the name operator<>(). Both are perfectly fine, and express what they do reasonably enough. Here are what those three (well, four, if I show reflect_constant both with and without an explicit return type) might look like in the wild in their typical usage (i.e. defaulted):

class string_view {
  char const* ptr_;
  size_t size_;

public:
  consteval auto reflect_constant() const -> std::meta::info = default;
  consteval auto reflect_constant() const = default;
  consteval operator template() const = default;
  consteval operator<>() const = default;
}

In the original paper, operator template() was not invokable, which was something that some people didn’t like, since it meant that you had to contrive some other way to test the function directly if that’s what you wanted to do. In this case, operator template() would still be a customization point for std::meta::reflect_constant(), so there would still be an easy way to test the implication directly.

So those options exist as well, if preferred. I have a mild preference for sticking with reflect_constant, simply because it matches the reflection function name, but only mild, and would be quite happy with any name whatsoever as long as it means we get the feature.

2.9 Implementation Experience

This has been implemented in my fork of Clang, and you can see it on compiler explorer.

3 References

[P0424R2] Louis Dionne, Hana Dusíková. 2017-11-14. String literals as non-type template parameters.
https://wg21.link/p0424r2
[P0732R2] Jeff Snyder, Louis Dionne. 2018-06-06. Class Types in Non-Type Template Parameters.
https://wg21.link/p0732r2
[P2484R0] Richard Smith. 2021-11-17. Extending class types as non-type template parameters.
https://wg21.link/p2484r0
[P2848R0] Arthur O’Dwyer, Enrico Mauro. 2023-04-24. std::is_uniqued.
https://wg21.link/p2848r0
[P2996R13] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, and Dan Katz. 2025-06-18. Reflection for C++26.
https://wg21.link/p2996r13
[P3380R1] Barry Revzin. 2024-12-17. Extending support for class types as non-type template parameters.
https://wg21.link/p3380r1
[P3491R3] Wyatt Childers, Peter Dimov, Barry Revzin, and Daveed Vandevoorde. 2025-06-20. define_static_{string,object,array}.
https://wg21.link/p349143