Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding Alias Templates

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?

like image 658
Jonathan Mee Avatar asked Dec 20 '22 04:12

Jonathan Mee


2 Answers

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.

like image 98
TartanLlama Avatar answered Dec 22 '22 10:12

TartanLlama


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.

like image 27
Daniel Frey Avatar answered Dec 22 '22 10:12

Daniel Frey