C++26 Reflection gives us universal template parameters

Keenan Horrigan on the std-proposals mailing list pointed out an interesting consequence of C++26 Reflection: it seems to give us “universal template parameters” almost for free.

The STL sometimes tries to pretend we have “universal template parameters” by overloading templates with the same name. For example, we have both std::get<T>(tp) taking a type and std::get<I>(tp) taking an integer; but these are just two completely different signatures of std::get in the same overload set. Likewise in C++23 you can write either rg | ranges::to<std::vector<int>>() (where the argument to to is a type) or rg | ranges::to<std::vector>() (where the argument is a class template). Again this is accomplished with an overload set — roughly like this (modulo I’ve totally mangled the actual job of ranges::to, which is generally not to forward its arguments to To’s constructor):

template<class To, class... Args>  // #1
auto rangish_to(Args&&... args) {
  return To(std::forward<Args>(args)...);
}
template<template<class...> class To, class... Args>  // #2
auto rangish_to(Args&&... args) {
  return To(std::forward<Args>(args)...);
}

int *a, *b;
auto x = rangish_to<std::vector<int>>(a, b);      // OK, #1
auto y = rangish_to<std::vector>(a, b);           // OK, #2

But even this overload set won’t accept a caller like:

auto z = rangish_to<std::ranges::subrange>(a, b); // error

because std::ranges::subrange as a template argument matches neither class To nor template<class...> class To. It’s actually a class template of two type parameters and a constant! (cppreference.)

enum class subrange_kind : bool {
  unsized,
  sized,
};
template<
  input_or_output_iterator I,
  sentinel_for<I> S = I,
  subrange_kind K = sized_sentinel_for<S, I> ? sized : unsized>
class subrange { ~~~~ };

We could accept subrange ad-hoc via a third overload:

template<template<class,class,auto> class To, class... Args>  // #3
auto rangish_to(Args&&... args) {
  return To(std::forward<Args>(args)...);
}

but in general there’s no way to write out the set of all possible kinds of class templates, so we can’t make our rangish_to accept all of them.

You might point out that the real ranges::to wouldn’t accept subrange anyway, because the real ranges::to accepts only containers like vector, not views like subrange or span. However, this deficiency is likely to be DR’ed in the near future thanks to Hewill Kang’s well-received P3544 “ranges::to<view>.”

C++26 Reflection to the rescue! C++26 Reflection is “untyped”: it uses a single type std::meta::info to represent the reflections of every kind of thing in the language. Therefore a template parameter of type meta::info alone can represent std::vector, std::ranges::subrange, std::array, or anything else (std::vector<int>, size_t, 42, std::ignore…) and so we can write a single function template like this (Godbolt):

template<std::meta::info To, class... Args>
auto meta_to(Args&&... args) {
  return typename [:To:](std::forward<Args>(args)...);
}

auto x = meta_to<^^std::vector<int>>(a, b);      // OK
auto y = meta_to<^^std::vector>(a, b);           // OK
auto z = meta_to<^^std::ranges::subrange>(a, b); // OK!

For the cost of two characters ^^ (and a really cryptic error message if you forget the typename keyword), C++26 seems to have given us universal template parameters!

(Does this mean P2989 “Universal template parameters” is obsolete? IMHO, yes. But I can see how one might debate it: those two ^^ characters are kind of ugly.)

Posted 2026-06-07