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::towouldn’t acceptsubrangeanyway, because the realranges::toaccepts only containers likevector, not views likesubrangeorspan. 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.)
