void_t
is a hack.
How template class specialization pattern matching works is that the primary template determines the kind (type or value) of each argument, and the defaults:
template<class A, class B=A, class C=void>
struct whatever {};
For you to invoke whatever<?...>
, you must match the valid arguments to the primary specialization of whatever<?...>
.
After that test is passed, the various specializations are pattern matched against. So suppose you wanted to have a specialization for pointers.
template<class T>
struct whatever<T*, T*, void> {
the template<?...>
argument list here isn't matched against: that just provides a list of "free variables". the pattern matching comes within the <?...>
template argument list after the whatever
class name. Each argument is matched in turn against the arguments sent to whatever
, and from that pattern matching the "free variables" (class T
above) are determined.
The trick is that you can put expressions in here that do not pattern match, but rely on other pattern matches, and then produce a new type:
template<class T>
struct whatever<T*, typename std::add_const<T>::type*, void> {
the second argument is a dependent type (of the template add_const
), so cannot be pattern matched against. In general, the result of some_template<T>::type
can be a Turing complete non-injective computation: the C++ standard does not require that to be inverted, fortunately for compiler writers.
The compiler doesn't try -- instead, it tries to determine T
from other template arguments, and then substitutes T
in for that argument, and checks that it matches the types passed to whatever
.
The trick here is that substitution failure (in the immediate context) doesn't generate an error1. If std::add_const<T>
did not have a field called ::type
, instead of generating an error it would instead say "well, this pattern doesn't match". It would discard this specialization as being a viable one from the set of candidates.
This feature was added way back in the early days of C++ because template functions would greedily match even basic types, and attempts to use derived types (say, iterator::value_type
) would fail if you passed an int
as iterator
. With this feature, the fact that int
has no ::value_type
would mean "this isn't a valid overload of some template function", rather than a syntax error during overload resolution. In effect, template functions where nearly useless without it if there was any overloading going on, so they added SFINAE -- substitution failure is not an error.
Once it showed up, people started abusing it. void_t
attempts to exploit it.
template<class T>
struct whatever<T*, T*, void_t<decltype(std::declval<T>().hello_world())>> {
void_t
takes its type argument(s), and discards them, and produces void
in a dependent context (thus blocking the expression from being used to deduce T
). It does so in a way that first evaluates the type arguments2, which means that if the type produced causes a failure in the immediate context, you get a substitution failure.
A point of this is that the type produced doesn't matter once you feed it through void_t
. We might not even know what type of t.hello_world()
should be when we define the primary specialization of whatever<?...>
. So we discard it and turn it into void
. In order for the specialization to match, the type must match. So we set the type to void
in the primary specialization, and in the specializations we do a test then pass it to void_t
to throw away the type result.
We can also use std::enable_if_t<?>
with a compile-time bool
expression, which also produces the type void
if and only if that bool
is true (and otherwise fails in the immediate context).
In a sense, void_t
maps valid type expressions to void
, and invalid type expressions to a substitution failure. enable_if_t
maps true
to void
, and false
to a substitution failure. In both cases, you need the void
type in the specialization to "consume" the result and match properly.
Both are highly useful in SFINAE code, which is a powerful feature that lets you make decisions about which specialization to use based off something other than simple pattern matching.
1 Last I checked, this requirement was not explicitly in the standard! The SFINAE rules applied to template function substitution failures, and everyone (including compiler writers and standard writers) just assumed it applied to template class substitution failures. Of course, the standard is hard enough to read I probably just missed it.
2 At one point, various compilers disagreed with what template<class...>using void_t=void;
should do exactly. Some would fail if the expression passed was a substitution failure: others would note that the expression would be discarded, and discard it before checking for substitution failure. This was clarified in a defect report.