11

Consider the following class:

class Foo
{
    public:

    void operator [] (const std::string& s) { }

    void operator [] (std::size_t idx) { }
};

Here, given an instance of Foo f, the expression f[0] is not ambiguous, because the compiler chooses the second overload. Likewise, the expression f["abc"] is not ambiguous, because the compiler chooses the first overload (since a const char* is convertible to an std::string).

So, why is it then, that if we have two Base classes, each with a different overload, that there is suddenly ambiguity?

Suppose we have:

class Base1
{
    public:

    void operator [] (const std::string& s) { }
};

class Base2
{
    public:

    void operator [] (std::size_t idx) { }
};

class Derived : public Base1, public Base2
{ };

Now, if we say:

Derived d;
d[0];

The compiler complains:

    error: request for member ‘operator[]’ is ambiguous
      d[0];
         ^
   note: candidates are: void Base2::operator[](std::size_t)
      void operator [] (std::size_t idx) { }
           ^
   note:                 void Base1::operator[](const string&)
      void operator [] (const std::string& s) { }

Why does the fact that both operator overloads are now in Base classes cause any ambiguity? And is there some way to resolve this?

EDIT: Could this be a compiler bug (I am using GCC 4.8.1)

Channel72
  • 24,139
  • 32
  • 108
  • 180

5 Answers5

9

This is not an issue with overload resolution, but rather with member name lookup, which is defined in 10.2. Consider (as I'd rather not write operator[] everywhere):

struct base1 { void f(int); };
struct base2 { void f(double); };
struct derived : base1, base2 {};
int main() {
   derived d; d.f(0);
}

When lookup for f starts in the postfix expression d.f(0), it will first look into derived and find that f does not resolve to anything at all. 10.2/5 then requires that lookup proceeds to all base classes in parallel, constructing separate lookup sets. In this case, S(f,base1) = { base1::f } and S(f,base2) = { base2::f }. The sets are then merged following the rules in 10.2/6. The first bullet deals with merging when one of the sets is empty or if the lookup for the different sets ended with the same member (consider that it hit a common base). The second bullet is the interesting one, as it applies here

10.2/6 bullet 2

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.

That is, S(f,base1) differs from S(f,base2), so S(f,derived) becomes an invalid declaration set. And lookup fails.

Community
  • 1
  • 1
David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • This contradicts the example in 10.2/13, I believe. Consider especially the call `f(0.0)`. Oh wait nevermind, that has *using-declarations* so it isn't the same. – Ben Voigt Oct 09 '13 at 16:19
  • I was trying to understand those paragraphs for this question, you made it clear, +1. – masoud Oct 09 '13 at 16:23
  • To be clear, you mean that bullet 1 applies to the first merge, replacing `S(f, derived)` with `S(f, base1)`, and then the conflict you describe occurs when merging from `base2`, where `S(f, base2)` is compared with `S(f, derived)`, which now is `S(f, base1)`? – Ben Voigt Oct 09 '13 at 16:25
  • Actually, does 10.2 even apply to the operator? I (and clang) agree with your conclusions concerning a named member function. – Ben Voigt Oct 09 '13 at 17:25
  • Ahh yes, 13.3.1.2/3 defers to 13.3.1.1.1 (in qualified context) which refers to 10.2 – Ben Voigt Oct 09 '13 at 17:27
4

The call is ambiguous because the two operators do not overload. Overloading applies only to names that are defined in the same scope. Base1 and Base2 define two different scopes, so in the derived class the compiler simply sees two identical names that have no connection. As the other answers have said, the way to overcome this is to hoist both names into the derived class with appropriate using declarations; when that is done, the compiler sees the two names in the scope of the definition of the derived class, and applies overload resolution.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • 1
    That's not what the error message says. – Ben Voigt Oct 09 '13 at 15:37
  • @BenVoigt - I don't know what the error message says; I'm just stating what the language definition says. – Pete Becker Oct 09 '13 at 15:39
  • They may not be overloads, yet they are still candidates and overload resolution will be used to select one. – Ben Voigt Oct 09 '13 at 15:41
  • 1
    @PeteBecker: That is interesting. Is it possible for you to tell me/us the reference where it says (or implies) what you said. – Nawaz Oct 09 '13 at 15:41
  • @Nawaz: At the risk of speaking for Pete, overload rules are vastly complex and difficult to cite in isolation. Have you read through that chapter in the standard yourself, looking for this information? – Lightness Races in Orbit Oct 09 '13 at 15:44
  • 2
    Clause 13, [over]/1: "When two or more different declarations are specified for a single name in the same scope, that name is said to be overloaded." Paragraph 3: "When an overloaded function name is used in a call, which overloaded function declaration is being referenced is determined by comparing the types of the arguments ... [t]his function selection process is called overload resolution and is defined in 13.3." – Pete Becker Oct 09 '13 at 15:44
  • @LightnessRacesinOrbit - in this case, it's easy: the opening paragraphs of Clause 13 set out the basic rule for when overloading can be considered. The details don't matter if overload resolution doesn't apply. – Pete Becker Oct 09 '13 at 15:45
  • @PeteBecker: So it seems! How fortuitous, and _rare_. – Lightness Races in Orbit Oct 09 '13 at 15:46
  • 3
    Overload resolution *does* apply. See section 13.3 -- overload resolution applies to the candidate set, which includes names found in multiple scopes, and not just overloads. – Ben Voigt Oct 09 '13 at 15:48
  • @BenVoigt - you're pointing at a long and complicated bunch of text that you say contradicts the clear language at the top of Clause 13. More details needed... `` – Pete Becker Oct 09 '13 at 15:53
  • 3
    Peter while technically they are not *overloads* by that definition, overload resolution **does** apply. Consider a different example: ADL. Functions pulled from completely different namespaces are brought together and overload resolution picks the best among them. Seems like an unfortunate choice of wording in the standard. – David Rodríguez - dribeas Oct 09 '13 at 15:54
  • I guess 13.2 needs a note saying something like "(though overload resolution applies in other ways to non-overloaded functions)". – Lightness Races in Orbit Oct 09 '13 at 15:57
  • 1
    @BenVoigt: the program is ill-formed according to 10.2/6, so no overload resolution whatsoever applies to it. – n. m. could be an AI Oct 09 '13 at 16:00
  • @n.m.: Huh? Nothing in 10.2/6 says anything about being ill-formed. What draft are you using? I'm checking n3690. – Ben Voigt Oct 09 '13 at 16:04
  • @BenVoigt I only have 3337 here. It says about a declaration set being invalid. 10.2/7 says about a program with an invalid set being ill-formed. – n. m. could be an AI Oct 09 '13 at 16:11
  • @n.m.: Why don't we end up in bullet 3 "Otherwise, the new S(f, C) is a lookup set with the shared set of declarations and the union of the subobject sets." What different scenario would lead to that (union)? – Ben Voigt Oct 09 '13 at 16:14
  • @BenVoigt: because we cannot merge Base1::operator[] and Base2::operator[] due to Base1 and Base2 not being base subobjects of each other. – n. m. could be an AI Oct 09 '13 at 16:20
  • @n.n.: That explains why we aren't in bullet #1. Why aren't we in bullet #3? What different scenario would lead to bullet #3? – Ben Voigt Oct 09 '13 at 16:22
  • We will be in bullet #3 if the declaration sets are identical, e.g. we have `struct A:B,C{...` and `B:virtual X{using X::foo()...` and `C:virtual X{using X::foo()...`. Then B and C bring the same X::foo() into the scope of A. – n. m. could be an AI Oct 09 '13 at 16:46
  • @n.m.: That still uses bullet #1. I'm still curious what scenario leads to bullet #3 and the *union* of sets found through subobjects. – Ben Voigt Oct 09 '13 at 16:59
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/38901/discussion-between-n-m-and-ben-voigt) – n. m. could be an AI Oct 09 '13 at 17:08
2
class Derived : public Base1, public Base2
{ 
    public:
    using Base1::operator[];
    using Base2::operator[];
};

Make the inherits explicit, so there is no need for the compiler to 'select a base'.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • 2
    Why is that needed? The compiler can clearly see the overloads. – Nawaz Oct 09 '13 at 15:33
  • @Nawaz - they're **not** overloads because they are not defined in the same scope. – Pete Becker Oct 09 '13 at 15:37
  • @Nawaz It seems this time, clang++ is _less_ restrictive and rightly so. See e.g. an 'opposite' case here: [using and overloading a template member function of a base class?](http://stackoverflow.com/questions/18861514/using-and-overloading-a-template-member-function-of-a-base-class) – sehe Oct 09 '13 at 15:37
  • @Pete: They are members of the candidate set. – Ben Voigt Oct 09 '13 at 15:42
2

TL;DR: Although both functions are in the candidate set, the candidate set is also invalid, making the program ill-formed. See dribeas's answer for details on that.


Both functions are clearly viable, since:

f((size_t)0)

and

f((const char*)0)

are legal and both conversion sequences are implicit.

Originally, the two candidates were not ambiguous because one was a better conversion than the other. The compiler chose the one that required only an integral promotion. Since an integral promotion was "better" than the other conversion sequence, it won.

Now, both candidates require a pointer upcast. Now the conversion sequence involving an upcast and integral promotion is no longer clearly better. So the compiler cannot choose and it reports ambiguity. (Note: I think the conversion sequence without user-defined conversion should still be better, and that candidate f(Base2* implicit, size_t) should still win... but it is MUCH more complicated now, because of the overload resolution rules involving conversions of multiple arguments.)

The "using" declaration allows the this pointer to be passed with an identity conversion instead of an upcast, so again one conversion sequence is just the integral promotion, which IS better.


From section 13.3.1:

The set of candidate functions can contain both member and non-member functions to be resolved against the same argument list. So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called. For the purposes of overload resolution, both static and non-static member functions have an implicit object parameter, but constructors do not.

Similarly, when appropriate, the context can construct an argument list that contains an implied object argument to denote the object to be operated on. Since arguments and parameters are associated by position within their respective lists, the convention is that the implicit object parameter, if present, is always the first parameter and the implied object argument, if present, is always the first argument.

and

During overload resolution, the implied object argument is indistinguishable from other arguments. The implicit object parameter, however, retains its identity since conversions on the corresponding argument shall obey these additional rules:

  • no temporary object can be introduced to hold the argument for the implicit object parameter; and

  • no user-defined conversions can be applied to achieve a type match with it.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • "Both overloads" No, in the second case, there are _no_ overloads. – Lightness Races in Orbit Oct 09 '13 at 15:42
  • 1
    @LightnessRacesinOrbit: Ok, *candidates*. Which cause *overload resolution*. See 13.3 – Ben Voigt Oct 09 '13 at 15:43
  • This seems to be the correct answer. @PeteBecker's answer is correct (we are dealing with *candidates* not *overloads*), but it doesn't explain why the Multiple Inheritance version is ambiguous, since the compiler still *sees* both candidates – Channel72 Oct 09 '13 at 15:46
  • @Channel72: well no, Pete's first sentence is wrong. Mere absence of overloads does not imply ambiguity. – Ben Voigt Oct 09 '13 at 15:47
  • 1
    This is a totally incorrect explanation. – n. m. could be an AI Oct 09 '13 at 15:49
  • But should the first not be preferred over the second? The second conversion sequence requires the user defined conversion from `const char*` to `std::string`, while the first one is just a promotion. – David Rodríguez - dribeas Oct 09 '13 at 15:49
  • @DavidRodríguez-dribeas: I confess to being a little confused about that... however we're talking about two different arguments, and rankings can't be compared across arguments -- to be a unique "best", it has to have as good or better sequence on all arguments, and strictly better on at least one. I think the sequence on the `this` argument should be "as good" however. – Ben Voigt Oct 09 '13 at 15:51
  • @BenVoigt: The `this` argument is implicit and I'm not convinced that it is treated as any other argument in this regard. – Lightness Races in Orbit Oct 09 '13 at 15:55
  • @LightnessRacesinOrbit: Yes it is, 13.3.1 describes the "implicit object parameter" and how it is used in overload resolution. – Ben Voigt Oct 09 '13 at 15:57
  • @BenVoigt: It doesn't do so to my satisfaction. I don't consider that passage to be at all clear that the `this` argument is handled in this way. – Lightness Races in Orbit Oct 09 '13 at 15:58
  • @LightnessRacesinOrbit: Pasted in another paragraph. That better? – Ben Voigt Oct 09 '13 at 16:01
  • @BenVoigt: Oh, much better. Satisfied now. Thanks. – Lightness Races in Orbit Oct 09 '13 at 16:02
  • Try to change `const string&` parameter to `const Base1&` and see what happens. – n. m. could be an AI Oct 09 '13 at 16:08
  • @Nawaz: "convinced with its reasoning" -- that's your choice, but the reasoning is still incorrect. – n. m. could be an AI Oct 09 '13 at 16:09
  • @BenVoigt: though, I'm totally convinced with overload resolution thing, I'm not totally convinced with the conversion sequence. I think `f(Base2*, size_t)` is [still a better match](http://coliru.stacked-crooked.com/a/fba3c8beb6584d58). – Nawaz Oct 09 '13 at 16:18
  • 2
    The answer is **wrong**. This is not a problem of overload resolution, but rather lookup. Provided an answer explaining it (or attempting to), but basically when the member is not found in the derived type, it is looked in the different bases and the results are merged. The merge, which is not an *union* yields an invalid set and fails. Overload resolution never gets a chance – David Rodríguez - dribeas Oct 09 '13 at 16:19
  • @DavidRodríguez-dribeas: In what scenario would bullet point #3 be invoked, leading to a union? – Ben Voigt Oct 09 '13 at 16:20
  • Change one of the functions to something clearly not viable. Compile with gcc. Does your explanation apply to that case? – n. m. could be an AI Oct 09 '13 at 16:54
  • 1
    @n.m.: Already tried that, but didn't know whether to trust clang or gcc. – Ben Voigt Oct 09 '13 at 16:58
0

Have you tried explicitly saying that Derived exposes both?

class Derived : public Base1, public Base2
{
public:
    using Base1::operator[];
    using Base2::operator[];
};

I have no idea whether it may work, I only have Visual here.

Medinoc
  • 6,577
  • 20
  • 42