5

Below is the pretty short example.

#include <utility>

template<typename T, typename = void>
struct A {};

template<typename T, typename U>
void f(A<std::pair<T,U>>) {}

template<typename U>
void f(A<std::pair<int,U>, std::enable_if_t<std::is_same_v<int,U>>>) {}

int main() {
  A<std::pair<int, int>> x;
  f(x);
}

The error is clear enough

uffa.cpp: In function ‘int main()’:                                                                                                                                                                                                                           
uffa.cpp:22:4: error: call of overloaded ‘f(A<std::pair<int, int> >&)’ is ambiguous                                                                                                                                                                           
   22 |   f(x);                                                                                                                                                                                                                                                
      |   ~^~~                                                                                                                                                                                                                                                 
uffa.cpp:10:6: note: candidate: ‘void f(A<std::pair<_T1, _T2> >) [with T = int; U = int]’                                                                                                                                                                     
   10 | void f(A<std::pair<T,U>>) {}                                                                                                                                                                                                                           
      |      ^                                                                                                                                                                                                                                                 
uffa.cpp:18:6: note: candidate: ‘void f(A<std::pair<int, U>, typename std::enable_if<is_same_v<int, U>, void>::type>) [with U = int; typename std::enable_if<is_same_v<int, U>, void>::type = void]’                                                          
   18 | void f(A<std::pair<int,U>, std::enable_if_t<std::is_same_v<int,U>>>) {}                                                                                                                                                                                
      |      ^

But I don't understand why having int as a fixed template argument in the second overload doesn't make it more specialized. After all, if I remove , std::enable_if_t<std::is_same_v<int,U>> from it, then it is preferred.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 1
    See [`[temp.func.order]`](http://eel.is/c++draft/temp.func.order). This (partial ordering rule) applies, regardless of the fact that you've explicitly specified `int`, because the template functions are synthesized first, and both are viable. – Cody Gray - on strike Jun 05 '21 at 08:31
  • Thanks @CodyGray, I've come across this before and never understood the problem. More specifically it is covered by this example in your link: http://eel.is/c++draft/temp.func.order#example-4. "such type deduction considers only parameters for which there are explicit call arguments, some parameters are ignored" – cmannett85 Jun 05 '21 at 08:38
  • I can happily say, without jeopardizing my non-existent [[tag:language-lawyer]] badge, that I have never come across such a situation in the real world, and if I ever did, that I would prefer to rewrite the code to something more obvious and readable. Templates are fantastic, and I use them to great effect, but memorizing details about overload resolution... meh, ain't nobody got time for that. – Cody Gray - on strike Jun 05 '21 at 08:47
  • @CodyGray so the two definitions of `f` in my snippet are two templates which undergo substitution separately and independently (just like if I delete one of them and compile, and do the same for the other one) and only sheet substitution are they taken both into account to decide which one is to be picked? But then how does removing `enable_if` solve the problem? – Enlico Jun 05 '21 at 09:05
  • By the way, that's not code I've conceived. I don't even understand what it could be useful for. I just want to understand what makes it work or not, purely from the language point of view. – Enlico Jun 05 '21 at 09:06
  • 1
    You aren't using `enable_if` in the normal way for SFINAE; you're actually adding it as a normal parameter, not a template type parameter. If you do something like [this](https://gcc.godbolt.org/z/Y3GK51a4a), I think the result will more closely match your expectations. (Note that I've modified your functions to return `int` rather than `void` so we can see which one got picked by the compiler. That doesn't change anything about the overload resolution or other semantics, since both were changed equally.) – Cody Gray - on strike Jun 05 '21 at 10:26
  • @CodyGray, does the fact that you only commented on second question in my second to last comment mean that the answer to the first question (_sheet_ -> _after_) is _yes_? – Enlico Jun 05 '21 at 17:37
  • I'm not really sure I understood your first question. But I didn't think it needed further explanation, as HolyBlackCat had already posted a correct and easily accessible answer. The only thing he had omitted was a mention of the fact that you were using `enable_if` in an unusual way, which was not consistent with the typical SFINAE tricks, and that that may have been creating some confusion for you. (HolyBlackCat has since updated his answer.) But, basically, yes. the template functions are expanded by the compiler, and then overload resolution is performed. – Cody Gray - on strike Jun 06 '21 at 03:16

1 Answers1

1

Even though this is , I'm going to provide a layman explanation.

Yes, the second overload fixes the first parameter of pair as int, while the first one doesn't.

But, on the other hand, the first overload fixes the second parameter of A as void, while the second one doesn't.

Your functions are equivalent to those:

template <typename T, typename U>
void f(A<std::pair<T, U>, void>) {}

template <typename U>
void f(A<std::pair<int,U>, blah-blah<U>>) {}

So none of them is more specialized than the other.


The code will work if you use more a conventional SFINAE:

template<typename U, std::enable_if_t<std::is_same_v<U, int>, std::nullptr_t> = nullptr>
void f(A<std::pair<int,U>>) {}

Or C++20 concepts:

template <std::same_as<int> U>
void f(A<std::pair<int,U>>) {}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • What about the fact that the first overload fixes something of `std::pair`, which is a template argument to `A`, whereas the second fixes something of `A` itself? I guess the answer is _the two things affect specialization at the same extent_, but what part of the standard says/impies it? Might be a stupid question, but it's easy for me to be a bit paranoid on this language-lawyer stuff. – Enlico Jun 05 '21 at 17:41
  • @Enlico The compiler isn't going to judge which of the diverging specializations is "more significant". For X to be more specialized than Y, it has to be specialized *in the exact same way*, and then some. – HolyBlackCat Jun 05 '21 at 19:05
  • The similar ruling applies everywhere in C++. For class template specializations, for C++20 constraints, etc. – HolyBlackCat Jun 05 '21 at 19:08
  • _it has to be specialized in the exact same way, **and then some**_: can you please clarify? I think my English is stepping in :/ – Enlico Jun 05 '21 at 19:15
  • 1
    @Enlico I'm not a native speaker, so maybe it's not the best wording. Let's say you have `template void foo(A, int, int) {}` and `template void foo(int, A, B) {}`. Then `foo(0, 0, 0)` is ambiguous. But if you change the second one to `foo(A, B, int)`, then there's no ambiguity. You get the idea? – HolyBlackCat Jun 05 '21 at 19:44
  • the quoted code (more conventional SFINAE) does not compile. The `= nullptr` is the problem. Can someone explain how `enable_if_t, std::nullptr_t> = nullptr` can be legal code. To me it looks like "type equal value" which clearly makes no sense. If I remove `= nullptr` then then code compiles, but that specialization is not selected. – ritter Jun 07 '21 at 17:21
  • @ritter [Compiles on my end](https://gcc.godbolt.org/z/nWbafcWj6). This is a non-type template parameter, of type `std::nullptr_t` (assuming condition is true), unnamed, with default argument `nullptr`. I perform SFINAE this way to make sure that the user can't change anything by manually specifying the template argument: they can't specify anything other than `nullptr`, which is the default. – HolyBlackCat Jun 07 '21 at 17:36