'Ambiguous partial specializations and enable_if_t

This question is due to insane curiosity rather than an actual problem. Consider the following code:

template<typename...>
struct type_list {};

template<typename, typename = void>
struct test_class;

template<typename... T>
struct test_class<type_list<T...>> {
    static constexpr auto value = false;
};

template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
    static constexpr auto value = true;
};

int main() {
    static_assert(!test_class<type_list<double, char>>::value);
    static_assert(test_class<type_list<int>>::value);
}

This fails with the error:

ambiguous partial specializations of 'test_class<type_list>'

If I changed the second specialization to something that doesn't work from a functional point of view, the error would go away:

template<typename T>
struct test_class<type_list<T>> {
    static constexpr auto value = true;
};

Similarly, if I use the alias template void_t, everything works as expected:

template<typename T>
struct test_class<type_list<T>, std::void_t<std::enable_if_t<std::is_same_v<T, int>>>> {
    static constexpr auto value = true;
};

Apart from the ugliness of combining void_t and enable_if_t, this also gets the job done when there is a single type that differs from int, ie for a static_assert(!test_class<type_list<char>>::value) (it wouldn't work in the second case instead, for obvious reasons).

I see why the third case works-ish, since the alias template is literally replaced with void when the condition of the enable_if_t is satisfied and type_list<T> is more specialized than type_list<T...> (right?). However, I would have expected the same also for the following:

template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
    static constexpr auto value = true;
};

At the end of the day, std::enable_if_t<std::is_same_v<T, int>> somehow is void when the condition is statisfied (ok, technically speaking it's typename blabla::type, granted but isn't ::type still void?) and therefore I don't see why it results in an ambiguous call. I'm pretty sure I'm missing something obvious here though and I'm curious to understand it now.

I'd be glad if you could point out the standardese for this and let me know if there exists a nicer solution than combining void_t and enable_if_t eventually.



Solution 1:[1]

Let's start with an extended version of your code

template<typename, typename = void>
struct test_class;

template<typename T>
struct test_class<type_list<T>> {
  static constexpr auto value = false;
};

template<typename... Ts>
struct test_class<type_list<Ts...>> {
  static constexpr auto value = false;
};

template<typename T>
struct test_class<type_list<T>, std::enable_if_t<std::is_same_v<T, int>>> {
  static constexpr auto value = true;
};

that is called with

test_class<type_list<int>>::value

Try it here!

The standard distinguishes between template parameters that are equivalent, ones that are only functionally equivalent and others which are not equivalent [temp.over.link]/5

Two expressions involving template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one-definition rule, except that the tokens used to name the template parameters may differ as long as a token used to name a template parameter in one expression is replaced by another token that names the same template parameter in the other expression. Two unevaluated operands that do not involve template parameters are considered equivalent if two function definitions containing the expressions would satisfy the one-definition rule, except that the tokens used to name types and declarations may differ as long as they name the same entities, and the tokens used to form concept-ids may differ as long as the two template-ids are the same ([temp.type]).

Two potentially-evaluated expressions involving template parameters that are not equivalent are functionally equivalent if, for any given set of template arguments, the evaluation of the expression results in the same value. Two unevaluated operands that are not equivalent are functionally equivalent if, for any given set of template arguments, the expressions perform the same operations in the same order with the same entities.

E.g. std::enable_if_t<std::is_same_v<T, T>> and void are only functionally equivalent: The first term will be evaluated to void for any template argument T. This means according to [temp.over.link]/7 code containing two specialisations <T, void> and <T, std::enable_if_t<std::is_same_v<T, T>> is already ill-formed:

If the validity or meaning of the program depends on whether two constructs are equivalent, and they are functionally equivalent but not equivalent, the program is ill-formed, no diagnostic required.

In the code above std::enable_if_t<std::is_same_v<T, int>> is not even functionally equivalent to any of the other versions as it is in general not equivalent to void.

When now performing partial ordering [temp.func.order] to see which specialisation matches best your call this will result in an ambiguity as test_class is equally specialised [temp.func.order]/6 in both cases (with either Ts={int}, void or T=int, std::enable_if_t<std::is_same_v<T, int>>, both resulting in int, void but you can't deduce them against each other) and therefore the compilation will fail.

On the other hand by wrapping std::enable_if_t with std::void_t, which is nothing more but an alias for void

template <typename T>
using void_t = void;

the partial ordering will succeed as in this case the compiler will already know the type of the last template parameter is void in all cases, choosing test_class<T, std::void_t<std::enable_if_t<std::is_same_v<T,int>>>> with T=int as the most specialised as the non-variadic case (T=int, void) is considered more specialised than the variadic one Ts={int}, void (see e.g. temp.deduct.partial/8 or again this).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1