Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

C++0x, confusion with using-declaration in multiple base classes.

71 views
Skip to first unread message

Johannes Schaub

unread,
Apr 16, 2011, 9:47:47 AM4/16/11
to

What should happen for this case:

struct A {
void f();
};

struct B : virtual A {
using A::f;
};

struct C : virtual A {
using A::f;
};

struct D : B, C {
void g() {
f();
}
};

The line of interest is `f()`. Clearly the lookup of `f` according to
`10.2` of the FDIS succeeds and finds `A::f`. However, what candidates will
overload resolution consider? The spec says at `13.3.1p4`:

> For non-conversion functions introduced by a using-declaration into a
derived class, the function is considered to be a member of the derived
class for the purpose of defining the type of the implicit object parameter.

The intent of this is that for a single class, if such a class contains both
own member functions and a using declaration bringing names of base class
functions into scope, that during overload resolution all the function
candidates have the same class type in their implicit object parameter. But
what does this mean for the above example? Will the candidates be the
following?

void F1(B&)
void F2(C&)
// call arguments: (lvalue D)

This appears to be wrong, because we only have one declaration in the lookup
result set according to `10.2p7`. How shall we interpret this?? What should
the practical behavior of C++0x implementations be? Considering the call
ambiguous?


--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

John Shaw

unread,
Apr 18, 2011, 10:02:55 AM4/18/11
to

On Apr 16, 8:47 am, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:

> What should happen for this case:
>
> struct A {
> void f();
> };
>
> struct B : virtual A {
> using A::f;
> };
>
> struct C : virtual A {
> using A::f;
> };
>
> struct D : B, C {
> void g() {
> f();
> }
> };
>
The using-declaration in your classes brings the function f() into
your derived classes as a member function. This means that for
overload resolution, they will not be considered members of the base
class and the call to f() in g() will be ambiguous.

This is a form of name hiding. Since, for overload resolution, the
function is considered a member of the derived class and, therefore,
hides the base class member.

7.3.3 The using declaration

16 “For the purpose of overload resolution, the functions which are
introduced by a using-declaration into a derived class will be treated
as though they were members of the derived class.”

Johannes Schaub

unread,
Apr 18, 2011, 7:47:43 PM4/18/11
to

John Shaw wrote:

>
> On Apr 16, 8:47 am, Johannes Schaub <schaub.johan...@googlemail.com>
> wrote:

[snip, so overquoting alarm isn't triggered]


>>
> The using-declaration in your classes brings the function f() into
> your derived classes as a member function. This means that for
> overload resolution, they will not be considered members of the base
> class and the call to f() in g() will be ambiguous.
>

[...]


> 7.3.3 The using declaration
>
> 16 “For the purpose of overload resolution, the functions which are
> introduced by a using-declaration into a derived class will be treated
> as though they were members of the derived class.”
>

In this case, the function would therefor be treated as being a member of
class B and class C. What is the result of such a treatment? I cannot find
the Standard defining this.

Note that the function is considered a member of the base for only the
purpose of overload resolution. Name lookup will still be unambiguous. So,
we do overload resolution with a single declaration, but have to treat the
declared entity to be a member of two classes. That sounds illogical and
sounds like requiring explicit treatment by the Standard's text.

John Shaw

unread,
Apr 22, 2011, 3:07:06 PM4/22/11
to
On Apr 18, 6:47 pm, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:

> Note that the function is considered a member of the base for only the
> purpose of overload resolution. Name lookup will still be unambiguous. So,
> we do overload resolution with a single declaration, but have to treat the
> declared entity to be a member of two classes. That sounds illogical and
> sounds like requiring explicit treatment by the Standard's text.
>

Name lookup occurs before overload resolution. It provides a set of
candidate functions to the overload resolution mechanism. Ambiguity
does not apply to name lookup; only to overload resolution.

Draft N3242

13.3 Overload resolution
1 "... a mechanism for selecting the best function to call given ... a
set of candidate functions ..."

See 13.3.1.1.1 Call to named function
3 "... The name is looked up in the context of the function call
following the normal rules for name lookup in function calls
(3.4) ..."

3.4 Name lookup
1 "... Name lookup may associate more than one declaration with a name
if it finds the name to be a function name; the declarations are said
to form a set of overloaded functions (13.1). Overload resolution
(13.3) takes place after name lookup has succeeded ..."

3.4.1 Unqualified name lookup
1 "... name lookup ends as soon as a declaration is found for the name
…"

struct A { void f(); };
struct B : virtual A { using A::f; };
struct C : virtual A { using A::f; };
struct D : B, C { void g() { f(); } };

>From 7.3.3/16 we know that f() is brought into the scopes of A and B.
That is, the using-declarations in A and B, effectively declares the
member functions B::f() and C::f().

The name lookup associates both of those declarations with the call
f() in D::g() (3.4/1). They are the set of candidate functions being
considered by the overload resolution mechanism (13.3/1).

Because of the using-declarations; A::f() is no longer a candidate
function because “name lookup ends as soon as a declaration is found
for the name” (3.4.1/1). Lookup finds matching declarations B::f() and
C::f(); that is where lookup ends.

Johannes Schaub

unread,
Apr 22, 2011, 11:03:57 PM4/22/11
to

John Shaw wrote:

> On Apr 18, 6:47 pm, Johannes Schaub <schaub.johan...@googlemail.com>
> wrote:
>> Note that the function is considered a member of the base for only the
>> purpose of overload resolution. Name lookup will still be unambiguous.
>> So, we do overload resolution with a single declaration, but have to
>> treat the declared entity to be a member of two classes. That sounds
>> illogical and sounds like requiring explicit treatment by the Standard's
>> text.
>>
>
> Name lookup occurs before overload resolution. It provides a set of
> candidate functions to the overload resolution mechanism. Ambiguity
> does not apply to name lookup; only to overload resolution.
>

That's incorrect.

struct A { };

struct B : A {
void f();
};

struct C : A {
void f();
};

struct D : B, C {

};

void g() { &D::f; }

Name-lookup for D::f is ambiguous.


>> From 7.3.3/16 we know that f() is brought into the scopes of A and B.
> That is, the using-declarations in A and B, effectively declares the
> member functions B::f() and C::f().
>

That's incorrect too. As opposed to the above, the following is fine:

struct A {
void f();
};

struct B : A {
using A::f;
};

struct C : A {
using A::f;
};

struct D : B, C {

};

void g() { &D::f; }

In other words, member name lookup as described by 10.2 only finds A::f.

Francis Glassborow

unread,
Apr 23, 2011, 10:34:59 PM4/23/11
to

As neither piece of code is compilable (GCC baulks at &D::f) I do not
think that you have made your point.

As this area is very hard to understand I think it is particularly
important to ensure that code purporting to demonstrate some aspect of
it actually compiles.

Johannes Schaub

unread,
Apr 24, 2011, 12:25:16 PM4/24/11
to

Francis Glassborow wrote:

>
> On 23/04/2011 04:03, Johannes Schaub wrote:
>>
>> John Shaw wrote:
>>
>>> On Apr 18, 6:47 pm, Johannes Schaub<schaub.johan...@googlemail.com>
>>> wrote:
>>>> Note that the function is considered a member of the base for only the
>>>> purpose of overload resolution. Name lookup will still be unambiguous.
>>>> So, we do overload resolution with a single declaration, but have to
>>>> treat the declared entity to be a member of two classes. That sounds
>>>> illogical and sounds like requiring explicit treatment by the
>>>> Standard's text.
>>>>
>>>
>>> Name lookup occurs before overload resolution. It provides a set of
>>> candidate functions to the overload resolution mechanism. Ambiguity
>>> does not apply to name lookup; only to overload resolution.
>>>
>>
>> That's incorrect.
>>

[code follows. snipped for clarity]


>>
>> Name-lookup for D::f is ambiguous.
>>
>>
>>>> From 7.3.3/16 we know that f() is brought into the scopes of A and B.
>>> That is, the using-declarations in A and B, effectively declares the
>>> member functions B::f() and C::f().
>>>
>>
>> That's incorrect too. As opposed to the above, the following is fine:
>>

[code follows. snipped for clarity]


>>
>> In other words, member name lookup as described by 10.2 only finds A::f.
>>
>>
>
> As neither piece of code is compilable (GCC baulks at &D::f) I do not
> think that you have made your point.
>

GCC is not a complete C++0x compiler. It baulks on this because its class
member name lookup has not yet updated to the C++0x semantics.

I thought I was sufficient clear. Let me try again with a short summary of
the shown snippets: A using declaration in class scope is transparent for
name lookup, and name lookup will yield the target declaration as a result.
It's not as-if there are two non-using declarations in both derived classes.

The spec says at 10.2p3: "In the declaration set, using-declarations are
replaced by the members they designate [...]". In this way, you have these
steps, where (a, b) shall mean: Declaration set a, and subobject set b.

Lookup in D: [no declaration -> (). delegate to base classes B and C]
Lookup in B: [using-declaration -> (A::f, B in D). finished]
Lookup in C: [using-declaration -> (A::f, C in D). finished]
Merge of B into D: (A::f, B in D)
Merge of C into D: (A::f, { B in D, C in D })

Result of lookup: { A::f }

Therefor, only A::f is found, and there is no ambiguity in lookup for it, as
opposed to the first case.

Lookup in D: [no declaration -> (). delegate to base classes B and C]
Lookup in B: [declaration -> (B::f, B in D). finished]
Lookup in C: [declaration -> (C::f, C in D). finished]
Merge of B into D: (B::f, B in D)
Merge of C into D: (invalid set, { B in D, C in D })

Result of lookup: invalid -> Program is ill-formed, lookup ambiguity.

The specification is clear on that part. The part I was asking about is: If
the lookup resul yields A::f, and overload resolution has to define the
implicit object parameter to be of the type "of the derived class", what
does that mean? We have two derived classes that the using-declaration for
it was found in.


> As this area is very hard to understand I think it is particularly
> important to ensure that code purporting to demonstrate some aspect of
> it actually compiles.
>

This is an utopic goal, given that the C++0x working paper isn't even an
official Standard yet, and given that no existing compiler (to my knowledge,
please correct me If that's not true) claim to support that part of the
working paper already.

A. McKenney

unread,
Apr 25, 2011, 3:04:33 AM4/25/11
to
On Apr 22, 11:03 pm, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:

> ... the following is fine:


>
> struct A {
> void f();
> };
>
> struct B : A {
> using A::f;
> };
>
> struct C : A {
> using A::f;
> };
>
> struct D : B, C {
>
> };
>
> void g() { &D::f; }
>
> In other words, member name lookup as described by 10.2 only finds A::f.
>

I don't understand why this is not ambiguous.
A call to B::f() is not the same as C::f(),
since B::f() is operating on the first
A subobject (from B), while C::f() is operating
on the second A subobject.

To make it more obvious, consider a more realistic example:
---------------------------------
#include <iostream>

struct A { int x; A(int i = 0) : x(i) {} int f() { return x; } };
struct B : public A { using A::f; B() : A(1) {} };
struct C : public A { using A::f; C() : A(2) {} };
struct D : public B, C {};

int main() {
A a; std::cout << a.f() << std::endl; // prints out 0
B b; std::cout << b.f() << std::endl; // prints out 1
C c; std::cout << c.f() << std::endl; // prints out 2
// D d; std::cout << d.f() << std::endl; // would print out what?
}
-----------------------------------

The last line doesn't compile under g++, but if it did,
what would you want it to print out?


The case of virtual inheritance is different, since
B::f and C::f are in this case the same code acting
on the same subobject.

But I ask: what does allowing this very special case
add to the language? And is it enough to justify
making the language more complicated? Especially
given that there is a well-known, easy-to-understand,
and quite general way to deal with the ambiguity?

John Shaw

unread,
Apr 25, 2011, 3:01:10 AM4/25/11
to
On Apr 22, 10:03 pm, Johannes Schaub <schaub.johan...@googlemail.com>
wrote:
> John Shaw wrote:
> ...

> > Name lookup occurs before overload resolution. It provides a set of
> > candidate functions to the overload resolution mechanism. Ambiguity
> > does not apply to name lookup; only to overload resolution.
>
> That's incorrect.
>
> struct A { };
>
> struct B : A {
> void f();
> };
>
> struct C : A {
> void f();
> };
>
> struct D : B, C {
>
> };
>
> void g() { &D::f; }
>
> Name-lookup for D::f is ambiguous.

Wrong:

Name lookup only provides the set of viable candidates; it does not
select between them and therefore cannot be ambiguous. Overload
resolution is the mechanism that selects the function with the best
fit – from the set of viable candidates. If there is more than 1
choice and the overload mechanism cannot determine the best fit – then
the result is ambiguous.

3: error: reference to 'f' is ambiguous
7: error: candidates are: void A::f()
7: error: void C::f()
7: error: void B::f()

1. Name lookup found 3 candidates that all matched exactly.
2. Overload resolution determined the result was ambiguous.

> >> From 7.3.3/16 we know that f() is brought into the scopes of A and B.
> > That is, the using-declarations in A and B, effectively declares the
> > member functions B::f() and C::f().
>
> That's incorrect too. As opposed to the above, the following is fine:
>
> struct A {
> void f();
> };
>
> struct B : A {
> using A::f;
> };
>
> struct C : A {
> using A::f;
> };
>
> struct D : B, C {
>
> };
>
> void g() { &D::f; }
>
> In other words, member name lookup as described by 10.2 only finds A::f.

Better check again: I have seen gcc fail to compiler and then report
no errors, even though there were link errors.

3: error: reference to 'f' is ambiguous
7: error: candidates are: void A::f()
7: error: void A::f()
7: error: void A::f()

1. Name lookup found 3 candidates, in 3 different scopes {A, B, C},
that all matched exactly.
2. Overload resolution determined the result was ambiguous.

The candidates shown in the error messages are correct, because the
only difference between them was the scope in which they were found.

The above results are from g++ (GCC) 4.5.2

Johannes Schaub

unread,
Apr 25, 2011, 8:56:46 AM4/25/11
to

A. McKenney wrote:

> On Apr 22, 11:03 pm, Johannes Schaub <schaub.johan...@googlemail.com>
> wrote:
>
>> ... the following is fine:
>>
>> struct A {
>> void f();
>> };
>>
>> struct B : A {
>> using A::f;
>> };
>>
>> struct C : A {
>> using A::f;
>> };
>>
>> struct D : B, C {
>>
>> };
>>
>> void g() { &D::f; }
>>
>> In other words, member name lookup as described by 10.2 only finds A::f.
>>
>
> I don't understand why this is not ambiguous.

In the above, "&D::f" has type "void(A::*)()". It refers to "f" as a member
of "A". An ambiguity would be risen if we would try to convert that pointer
to a "void(D::*)()".

In short, the lookup was unambiguous, because lookup found a single
declaration, A::f. If we would call "D d; d.f()", then surely we would get
an ambiguity, but not because of name lookup; rather, because of 5.2.5p5.

More information can be found in the summary for core DR #39 and in the
extensive discussion of the problem in N1543.

0 new messages