15

Consider the following:

template<typename T>
struct C {};
template<typename T, typename U>
void operator +(C<T>&, U);

struct D: C<D> {};

struct E {};
template<typename T>
void operator +(C<T>&, E);

void F() { D d; E e; d + e; }

This code compiles fine on both GCC-7 and Clang-5. The selected overload for operator + is that of struct E.

Now, if the following change takes place:

/* Put `operator +` inside the class. */
template<typename T>
struct C {
    template<typename U>
    void operator +(U);
};

that is, if operator + is defined inside the class template, instead of outside, then Clang yields ambiguity between both operator +s present in the code. GCC still compiles fine.

Why does this happen? Is this a bug in either GCC or Clang?

2
  • GCC still compiles fine... but selects which overload? Commented Jan 12, 2018 at 5:15
  • @Nawaz: It picks struct E's overload in both cases. Commented Jan 12, 2018 at 16:15

2 Answers 2

7

This is a bug in gcc; specifically, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53499 .

The problem is that gcc is regarding the implicit object parameter of a class template member function as having a dependent type; that is, during function template partial ordering gcc transforms

C<D>::template<class U> void operator+(U);  // #1

into

template<class T, class U> void operator+(C<T>&, U);  // #1a (gcc, wrong)

when it should be transformed into

template<class U> void operator+(C<D>&, U);  // #1b (clang, correct)

We can see that when compared to your

template<class T> void operator+(C<T>&, E);  // #2

#2 is better than the erroneous #1a, but is ambiguous with #1b.

Observe that gcc incorrectly accepts even when C<D> is not a template at all - i.e., when C<D> is a class template full specialization:

template<class> struct C;
struct D;
template<> struct C<D> {
    // ...

This is covered by [temp.func.order]/3, with clarification in the example. Note that again, gcc miscompiles that example, incorrectly rejecting it but for the same reason.

Sign up to request clarification or add additional context in comments.

1 Comment

The last example settles the issue to me.
7

Edit: The original version of this answer said that GCC was correct. I now believe that Clang is correct according to the wording of the standard, but I can see how GCC's interpretation could also be correct.

Let's look at your first example, where the two declarations are:

template<typename T, typename U>
void operator +(C<T>&, U);
template<typename T>
void operator +(C<T>&, E);

Both are viable, but it is obvious that the second template is more specialized than the first. So GCC and Clang both resolve the call to the second template. But let's walk through [temp.func.order] to see why, in the wording of the standard, the second template is more specialized.

The partial ordering rules tell us to replace each type template parameter with a unique synthesized type and then perform deduction against the other template. Under this scheme, the first overload type becomes

void(C<X1>&, X2)

and deduction against the second template fails since the latter only accepts E. The second overload type becomes

void(C<X3>&, E)

and deduction against the first template succeeds (with T = X3 and U = E). Since the deduction succeeded in only one direction, the template that accepted the other's transformed type (the first one) is considered less specialized, and thus, the second overload is chosen as the more specialized one.

When the second overload is moved into class C, both overloads are still found and the overload resolution process should apply in exactly the same way. First, the argument list is constructed for both overloads, and since the first overload is a non-static class member, an implied object parameter is inserted. According to [over.match.funcs], the type of that implied object parameter should be "lvalue reference to C<T>" since the function does not have a ref-qualifier. So the two argument lists are both (C<D>&, E). Since this fails to effect a choice between the two overloads, the partial ordering test kicks in again.

The partial ordering test, described in [temp.func.order], also inserts an implied object parameter:

If only one of the function templates M is a non-static member of some class A, M is considered to have a new first parameter inserted in its function parameter list. Given cv as the cv-qualifiers of M (if any), the new parameter is of type “rvalue reference to cv A” if the optional ref-qualifier of M is && or if M has no ref-qualifier and the first parameter of the other template has rvalue reference type. Otherwise, the new parameter is of type “lvalue reference to cv A”. [ Note: This allows a non-static member to be ordered with respect to a non-member function and for the results to be equivalent to the ordering of two equivalent non-members. — end note ]

This is the step where, presumably, GCC and Clang take different interpretations of the standard.

My take: The member operator+ has already been found in the class C<D>. The template parameter T for the class C is not being deduced; it is known because the name lookup process entered the concrete base class C<D> of D. The actual operator+ that is submitted to partial ordering therefore does not have a free T parameter; it is not void operator+(C<T>&, U), but rather, void operator+(C<D>&, U).

Thus, for the member overload, the transformed function type should not be void(C<X1>&, X2), but rather void(C<D>&, X2). For the non-member overload, the transformed function type is still void(C<X3>&, E) as before. But now we see that void(C<D>&, X2) is not a match for the non-member template void(C<T>&, E) nor is void(C<X3>&, E) a match for the member template void(C<D>&, U). So partial ordering fails, and overload resolution returns an ambiguous result.

GCC's decision to continue to select the non-member overload makes sense if you assume that it is constructing the transformed function type for the member lexically, making it still void(C<X1>&, X2), while Clang substitutes D into the template, leaving only U as a free parameter, before beginning the partial ordering test.

11 Comments

gcc accepts even the case where the member function template is in an explicit specialization, where there can’t possibly be a free template parameter to insert. Definitely a gcc bug.
The irony in this is that, despite GCC being wrong as it seems, it gives the result which is most intuitive according to the user's expectations.
@Alek I don't understand. How is it more intuitive at all?
@JohannesSchaub-litb Because one wouldn't presume putting the operator inside or outside the class definition should make any difference in the resulting overload resolution.
In other cases there are differences aswell between operators inside and outside of classes, so IMO it's not too much worth to try and find a way to "make them behave the same". Consider struct A { A(int) { } void operator+(A) { } }; vs void operator(A, A) { }. For the latter, you can say 10 + A(10), but for the former it's not valid.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.