43

The rules for picking which class template specialization is preferred involve rewriting the specializations into function templates and determining which function template is more specialized via the ordering rules for function templates [temp.class.order]. Consider this example, then:

#include <iostream>

template <class T> struct voider { using type = void; };
template <class T> using void_t = typename voider<T>::type;

template <class T, class U> struct A { };

template <class T> int foo(A<T, void_t<T>> ) { return 1; }
template <class T> int foo(A<T*, void> )     { return 2; }

int main() {
    std::cout << foo(A<int*, void>{});
}

Both gcc and clang print 2 here. This makes sense with some previous examples - deducing against a non-deduced context (void against void_t<T>) is just ignored, so deducing <T, void_t<T>> against <X*, void> succeeds but deducing <T*, void> against <Y, void_t<Y>> fails in both arguments. Fine.

Now consider this generalization:

#include <iostream>

template <class T> struct voider { using type = void; };
template <class T> using void_t = typename voider<T>::type;

template <int I> struct int_ { static constexpr int value = I; };

template <class T, class U> struct A      : int_<0> { };
template <class T> struct A<T, void_t<T>> : int_<1> { };
template <class T> struct A<T*, void>     : int_<2> { };

int main() {
    std::cout << A<int*, void>::value << '\n';
}

Both clang and gcc report this specialization as ambiguous, between 1 and 2. But why? The synthesized function templates aren't ambiguous. What's the difference between these two cases?

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    Paging @bogdan. – Barry Feb 23 '17 at 13:25
  • I'm not expert, but I think it has to do with specializing for both `void` and `void_t`. This [works](http://ideone.com/91pVA5) as expected. – xinaiz Feb 23 '17 at 14:18
  • @BlackMoses That doesn't do any partial ordering. If there's only one specialization that matches, it's just picked. – Barry Feb 23 '17 at 14:31
  • 3
    Interesting, looks like a bug and VC++ also reports ambiguity. According to [14.5.5.2](http://eel.is/c++draft/temp.class.spec#temp.class.order) those classes should be transformed into `template void f(A>)` and `template void f(A)` and second should be picked just like in your first example – mpiatek Feb 23 '17 at 15:42
  • 3
    I'm here :-). The first thing that comes to mind is that it's the same problem as the one I wrote about [in this answer](https://stackoverflow.com/a/31735144/4326278), but I'll give it some more thought in a bit. – bogdan Feb 23 '17 at 17:58
  • @bogdan It's one thing to say one compiler gets something wrong (especially when it's msvc), but I dunno how comfortable I am with "well, uh, you're all wrong guys... " Although that's kind of what it seems like? – Barry Feb 23 '17 at 23:37
  • @Barry I'm not comfortable saying that either, but I think it's pretty clear that the current state of affairs is inconsistent - the standard says that two constructs should behave in the same way and they don't. I've been looking through Clang's latest sources and there are some significant changes since that old answer of mine (all those links are now wrong, my fault) but still not enough to resolve this issue. – bogdan Feb 24 '17 at 01:04
  • Anyway, looking through those sources I realized something: consider the primary template `A` and the partial specialization `A>`. The two corresponding function templates used for partial ordering are unordered (deduction succeeds both ways), so the partial specialization is not more specialized than the primary template, which makes it invalid. So making it work consistently would require a change to the partial ordering algorithm itself. – bogdan Feb 24 '17 at 01:05
  • @bogdan Yeah but we don't order between the primary and the one specialization. We just pick it. – Barry Feb 24 '17 at 03:28
  • @Barry except for [\[temp.class.spec\]/8.2](https://timsong-cpp.github.io/cppwp/temp.class.spec#8.2). – T.C. Feb 24 '17 at 05:35
  • @T.C. Ah, you're right. So how does `void_t` work? The compilers just all happen to agree that it does? – Barry Feb 24 '17 at 13:59
  • It looks like this non-standard inconsistency between partial ordering of function templates and class template partial specializations is what makes it work currently. And all 4 main compilers somehow managed an ad-hoc agreement on this (I can imagine four guys around a table over a round of beers during a "late evening session" in, say, Kona). And except a few of us over here no one seems too bothered by this. Potatoswatter opened a thread on his question on std-discussion; no answers if I recall correctly. – bogdan Feb 24 '17 at 15:29
  • @bogdan Forget about partial ordering, we don't even have an actual specification for SFINAE in partial specializations :) – T.C. Feb 24 '17 at 15:49
  • 1
    @Bogdan The "unordered" issue with `void_t` is CWG 2173. – T.C. Feb 24 '17 at 15:57
  • @T.C. SFINAE? Who needs that? :-) Well spotted for the CWG issue. The proposed solution is interesting. It would make `A>` more specialized than `A`, but maybe that's what we want? Given that non-deduced contexts are frequently used for SFINAE, choose non-deduced if enabled, fall back to the other one otherwise? – bogdan Feb 24 '17 at 18:34
  • @T.C. umm, CWG 2173 referred to 1315 that in turn was about the old [temp.class.spec]8.1, that is, it was not about partial ordering of non deduced contexts as such. In fact, the final wording in latest c++17 draft does not mention non deduced contexts at all – Massimiliano Janes Oct 17 '17 at 14:52
  • @bogdan following [temp.func.order], A> *is* more specialized than A, why do you believe it's not ? – Massimiliano Janes Oct 17 '17 at 14:52
  • @MassimilianoJanes Just to make sure we're on the same page: we're talking about OP's `void_t`, not `std::void_t`. During partial ordering, you cannot say that `voider::type` is `void` for a synthesized type `X`. In general, `voider` could have partial or explicit specializations with `type` defined differently. The partial ordering algorithm aims to work uniformly with *any* type `X`, as it tries to make general statements about templates, not about particular specializations, so `voider::type` is just another unknown type in this context. – bogdan Oct 18 '17 at 00:08
  • @bogdan, but does this apply to the transformed template mentioned in [temp.func.order] ? that's the point ... do you mean that my partial ordering process synthesis in my answer is wrong ? if yes, why ? – Massimiliano Janes Oct 18 '17 at 06:50
  • @bogdan, for example, try adding a specialization `template struct voider { using type = int; };` before the cout line above, the program compiles for me, proving that the ( at least my ) compiler does NOT partial order treating the undeduced context as a general unknown type, supporting my reading of the std. – Massimiliano Janes Oct 18 '17 at 06:54
  • 1
    @MassimilianoJanes In your examples, you're pinning the synthesized types to concrete, well known types, so you're not modelling the general algorithm. If you want to add a relevant `voider` specialization to the last example in your answer, add `template<> struct voider { using type = U0_; };`. This will change the result in your example, but not in the algorithm that the compiler actually uses for partial ordering. To test the compilers' algorithm, you could use an example like [this one](https://wandbox.org/permlink/4xwatuWr2wUtOOYM); I've included some explanations in code comments. – bogdan Oct 18 '17 at 09:38
  • @bogdan sorry if I insist, when you say "So, given the rewrite in [temp.class.order], function template #1 should be more specialized than #0,so this call should print 1, right?" you're not right, because that call does not follow the same partial ordering rules. Regarding the concrete types these are only used on the righ-hand-side of each deduciton pass, so it's still the general algorithm; otherwise, how do you interpret [temp.func.order].3 ? (re ...synthesize a unique type...) – Massimiliano Janes Oct 18 '17 at 10:13
  • @MassimilianoJanes Why do you think not? Picking a class template specialization involves doing the rewrite that bogdan illustrated, then doing partial ordering on the function templates. The rules are the same. – Barry Oct 18 '17 at 11:56
  • @Barry, rules are not the same due to [temp.deduct.partial]3.3; now, bogdan (in the other SO question) claims it doesn’t matter, I claim it does (sadly it’s hard to argument extensively in comments). The problem lies in the detailed partial ordering process... I can be wrong of course, but I had no better explanation as of now – Massimiliano Janes Oct 18 '17 at 12:01
  • @Barry, anyway I'l put my answer on hold until I get to a more solid reasoning – Massimiliano Janes Oct 18 '17 at 12:20
  • 1
    @MassimilianoJanes Yes, in general there is of course a difference between using the function types as `P` and `A` versus using just the function parameter types. However, for the rewritten function types used for ordering partial specializations, what is the practical difference for deduction? The return types are `void`, they don't change anything. The parameter lists are made up of one parameter each, not a reference, not cv-qualified - deduction starts with the function type and reaches the stage where we compare the parameter types right away. No difference that I can see. – bogdan Oct 18 '17 at 16:25
  • 1
    @MassimilianoJanes "Synthesize a unique type" yields types about which we don't really know anything. We don't know what `voider::type` is. For the concrete types, we do. To get more space for argumentation, you can open a chat room and link to it from here. – bogdan Oct 18 '17 at 16:38
  • @bogdan, I'm not referring to the deduction of the function type as such, but the function type given to the partial ordering process; I mean, as I read it [temp.class.spec.match] says that "matching specializations" should be considered for patial orderining as of [temp.class.order], that is *including* the result of the matching ( hence making nondeduced context 'more' deduced, so to speak ): after all, this is how compilers seem behaving to me ... – Massimiliano Janes Oct 20 '17 at 10:11
  • @bogdan, as far as I can tell, it's a similar reasoning to the Richard Corden's answer in your linked post... – Massimiliano Janes Oct 20 '17 at 10:15
  • @MassimilianoJanes I'm not sure if I understand correctly what you mean by "including the result of the matching". Do you mean that the specializations that would be instantiated for the given template arguments are somehow used for partial ordering? I'm pretty sure that's not how it works; partial ordering works on templates (partial specializations are templates themselves). It has to work (and yield the same results) when no actual template arguments are available for matching, for example, when checking the condition in [temp.class.spec]/9.2. – bogdan Oct 20 '17 at 15:49
  • @MassimilianoJanes "Matching specializations" in the bullets in [temp.class.spec.match]/1 is referring to partial specializations; it also says "the instantiation is generated from that specialization" - that only makes sense if it's referring to partial specializations. Not the most rigorous wording, but I think the context makes it clear enough. – bogdan Oct 20 '17 at 15:52
  • Hi all - this is a very interesting question and I'd love to know why this is happening. But can someone please explain to me why replacing the alias void with T, makes this program work as it should? Code here: http://rextester.com/DSIPHE40637 – Constantinos Glynos Jan 16 '18 at 17:28

1 Answers1

7

Clang is being GCC-compatible (and compatible with existing code that depends on both of these behaviors).

Consider [temp.deduct.type]p1:

[...] an attempt is made to find template argument values (a type for a type parameter, a value for a non-type parameter, or a template for a template parameter) that will make P, after substitution of the deduced values (call it the deduced A), compatible with A.

The crux of the issue is what "compatible" means here.

When partially ordering function templates, Clang merely deduces in both directions; if deduction succeeds in one direction but not the other, it assumes that means the result will be "compatible", and uses that as the ordering result.

When partially ordering class template partial specializations, however, Clang interprets "compatible" as meaning "the same". Therefore it only considers one partial specialization to be more specialized than another if substituting the deduced arguments from one of them into the other would reproduce the original partial specialization.

Changing either of these two to match the other breaks substantial amounts of real code. :(

Richard Smith
  • 13,696
  • 56
  • 78