I believe that Clang and MSVC are incorrect, and GCC is correct to reject this code. This is an example of the principle that names in different scopes do not overload with each other. I submitted this to Clang as llvm bug 26850, we'll see if they agree.
There is nothing special about operator[]
vs f()
. From [over.sub]:
operator[]
shall be a non-static member function with exactly one parameter. [...] Thus, a subscripting expression x[y]
is interpreted as x.operator[](y)
for a class object x
of type T
if T::operator[](T1)
exists and if the operator is selected as the best match function by the overload
resolution mechanism
So the rules governing the lookup of d[Y()]
are the same as the rules governing d.f(X())
. All the compilers were correct to reject the latter, and should have also rejected the former. Moreover, both Clang and MSVC reject
d.operator[](Y());
where both they accept:
d[Y()];
despite the two having identical meaning. There is no non-member operator[]
, and this is not a function call so there is no argument-dependent lookup either.
What follows is an explanation of why the call should be viewed as ambiguous, despite one of the two inherited member functions seeming like it's a better match.
The rules for member name lookup are defined in [class.member.lookup]. This is already a little difficult to parse, plus it refers to
C
as the object we're looking up in (which in OP is named
D
, whereas
C
is a subobject). We have this notion of
lookup set:
The lookup set for f
in C
, called S(f,C)
, consists of two component sets: the declaration set, a set of
members named f
; and the subobject set, a set of subobjects where declarations of these members (possibly
including using-declarations) were found. In the declaration set, using-declarations are replaced by the set
of designated members that are not hidden or overridden by members of the derived class (7.3.3), and type
declarations (including injected-class-names) are replaced by the types they designate.
The declaration set for operator[]
in D<float>
is empty: there is neither an explicit declaration nor a using-declaration.
Otherwise (i.e., C
does not contain a declaration of f or the resulting declaration set is empty), S(f,C)
is
initially empty. If C
has base classes, calculate the lookup set for f
in each direct base class subobject Bi,
and merge each such lookup set S(f,Bi) in turn into S(f,C)
.
So we look into B<float>
and C<float>
.
The following steps define the result of merging lookup set S(f,Bi)
into the intermediate S(f,C):
— If each of the subobject members of S(f,Bi) is a base class subobject of at least one of the subobject
members of S(f,C), or if S(f,Bi) is empty, S(f,C) is unchanged and the merge is complete. Conversely,
if each of the subobject members of S(f,C) is a base class subobject of at least one of the
subobject members of S(f,Bi), or if S(f,C) is empty, the new S(f,C) is a copy of S(f,Bi).
— Otherwise, if the declaration sets of S(f,Bi) and S(f,C) differ, the merge is ambiguous: the new
S(f,C) is a lookup set with an invalid declaration set and the union of the subobject sets. In subsequent
merges, an invalid declaration set is considered different from any other.
— Otherwise, the new S(f,C) is a lookup set with the shared set of declarations and the union of the
subobject sets.
The result of name lookup for f
in C
is the declaration set of S(f,C)
. If it is an invalid set, the program is
ill-formed. [ Example:
struct A { int x; }; // S(x,A) = { { A::x }, { A } }
struct B { float x; }; // S(x,B) = { { B::x }, { B } }
struct C: public A, public B { }; // S(x,C) = { invalid, { A in C, B in C } }
struct D: public virtual C { }; // S(x,D) = S(x,C)
struct E: public virtual C { char x; }; // S(x,E) = { { E::x }, { E } }
struct F: public D, public E { }; // S(x,F) = S(x,E)
int main() {
F f;
f.x = 0; // OK, lookup finds E::x
}
S(x, F)
is unambiguous because the A
and B
base subobjects of D
are also base subobjects of E
, so S(x,D)
is discarded in the first merge step. —end example ]
So here's what happens. First, we try to merge the empty declaration set of operator[]
in D<float>
with that of B<float>
. This gives us the set {operator[](X)}
.
Next, we merge that with the declaration set of operator[]
in C<float>
. This latter declaration set is {operator[](Y)}
. These merge sets differ, so the merge is ambiguous. Note that overload resolution is not considered here. We are simply looking up the name.
The fix, by the way, is to add using-declarations to D<T>
such that there is no merge step done:
template<typename T> struct D : B<T>, C<T> {
using B<T>::operator[];
using C<T>::operator[];
};