Lets take something concrete:
#include <utility>
#include <vector>
template <typename ... Ts>
using void_t = void;
template <typename T, typename = void_t<>>
struct is_lt_comparable : std::false_type {};
template <typename T>
struct is_lt_comparable<T, void_t<decltype(std::declval<T>() < std::declval<T>())>> : std::true_type {};
template <typename T>
static constexpr bool is_lt_comparable_v = is_lt_comparable<T>::value;
struct test{};
int main()
{
#define error_message "detection doesn't work correctly"
static_assert(is_lt_comparable_v<int>, error_message);
static_assert(!is_lt_comparable_v<test>, error_message);
static_assert(is_lt_comparable_v<std::vector<int>>, error_message);
}
In the code above, why doesn't first and the last asserts trigger double definition of is_lt_comparable
?
void_t
with any arguments is still void
. As such, the last unnamed parameter for the template is always void
. IIRC type aliases are not considered distinct types, thus my intuition leads me to believe that I'm missing something.
Specifically, given a choice that both declaration are valid, and result in the same type, e.g. in the first one, is_lt_comparable<int, void>
, how does it know which template to instantiate?
You write: is_lt_comparable<int>
. This is what happens.
The primary template is chosen, and the second template argument is inferred, because it's the default. So, you actually have is_lt_comparable<int, void>
.
Now the template specializations are considered, to see if there is a match.
It finds the first (and only) specialization, and because it is a partial specialization, not a full one, it needs to instantiate it too, basically. So you get:
is_lt_comparable<int, void_t<decltype(std::declval<int>() < std::declval<int>())>>
Now, if the <
expression is ill-formed, then the specialization is not considered, and the compiler fall backs to the primary template.
But if it is valid, then the partial specialization becomes: is_lt_comparable<int, void>
. And that is an exact match from the template that we instantiated in 1), so the compiler chooses that one. Formally, this is known as the partial ordering rules.
If you are still confused, consider this:
template<typename> void foo() {}
template<> void foo<int>() {}
If I do foo<int>()
, there also won't be a double definition error, like you said there'd be. The specialization is a better match than the primary, so the compiler doesn't even instantiate the primary template with T = int
(it can't).