I asked a question that has several references to the code:
template <typename...>
using void_t = void;
I believe I have a generally misunderstand alias templates:
Why wouldn't you just evaluate whatever template parameter you're passing into an alias template in an enable_if_t
or conditional_t
statement?
Is the code above just about doing an enable_if_t
on multiple template parameters at once?
Secondly, I believe that I have a specific misunderstanding of the role of void_t
. This comment states that the C++17 standard defines void_t
. Here's what I don't get:
Isn't void_t
just an arbitrary name? If I still have to define template <typename...> using void_t = void;
wherever I plan to use void_t
what's the point of standardizing an arbitrary name?
I don't think the shown example really shows what void_t
is good for as it only shows one use case, but when you look at
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
it is not so much different from
template<typename T>
struct has_to_string<T,
decltype(std::to_string(std::declval<T>()), void())
>
: std::true_type { };
And for this statement:
The former version is much easier to read and void_t
doesn't require decltype
to work.
I think the advantage in readability is quite small and the second part makes no sense, when decltype
doesn't work, SFINAE kicks in as expected.
One example where void_t
is more useful is the one from the proposal:
// primary template handles types that have no nested ::type member
template< class, class = void_t<> >
struct has_type_member
: std::false_type { };
// specialization recognizes types that do have a nested ::type member
template< class T >
struct has_type_member<T, void_t<typename T::type>>
: std::true_type { }
As you can see, even the primary template uses void_t
to increase the readability as it now matches the specialization. That is not strictly necessary, but I like it. The real power comes when you think about the alternatives. Without void_t
, the specialization is now more complicated:
template< class T >
struct has_type_member<T, decltype(typename T::type, void())>
: std::true_type { }
wouldn't work as T::type
names a type, not an expression. You therefore need
template< class T >
struct has_type_member<T, decltype(std::declval<typename T::type>(), void())>
: std::true_type { }
The whole expression becomes longer, more tricky and it might suffer from edge-cases you forgot to handle. This is where void_t
really helps, the other uses are then just a small improvement and they increase consistency.
In Barry's example from your linked question:
template<typename T, typename = void>
struct has_to_string
: std::false_type { };
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
void_t
is just used to translate the type deduced by decltype
to void
so that it matches the default argument to the primary template definition. The SFINAE is all taken care of by the decltype
expression. You could just as easily do the following:
//use , void() instead of wrapping in void_t
//this uses the comma operator to check the type of the to_string call, then change the type to void
decltype(std::to_string(std::declval<T>()), void())
The former version is much easier to read and void_t
doesn't require decltype
to work.
If void_t
is available in your implementation you don't need to redefine it. When it's standardised it will be available just like any of the other alias templates in the standard.
Think about it this way: if T
is int
, which has a valid std::to_string
overload, deduction will look like this:
has_to_string<int>
-> has_to_string<int,void>
because of the default argument. So lets look for specializations of has_to_string
with those arguments.
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
Okay, that is a partial specialization for some T
and some dependent type. Let's work out that type:
void_t<decltype(std::to_string(std::declval<T>()))>
//std::to_string(int&&) is valid and returns a std::string
void_t<std::string>
//void_t changes types to void
void
Now our specialization looks like this:
template<>
struct has_to_string<int,void>
: std::true_type { };
This matches our instantiation of has_string<int,void>
, so has_to_string<int>
inherits from std::true_type
.
Now think about it when T
is struct Foo{};
. Again, let's work out that dependent type:
void_t<decltype(std::to_string(std::declval<T>()))>
//wait, std::to_string(Foo&&) doesn't exist
//discard that specialization
With that specialization discarded, we fall back to the primary template:
template<typename T, typename = void>
struct has_to_string
: std::false_type { };
So has_to_string<Foo>
inherits from std::false_type
.