Topic: Duplicated base class ambiguity resolution via explicit qualification


Author: "Francesco S. Carta" <entuland@gmail.com>
Date: Sun, 4 Oct 2009 13:41:04 CST
Raw View
Hi everybody.

Consider the following:
-------
struct A1{ void foo() {} };
struct A2{ void foo() {} };
struct B1 : A1, A2 {};
struct B2 : A1, A2 {};
struct C : B1, B2 {};

int main() {
   C c;
   c.B1::A1::foo();
}
-------

Is the last instruction well formed?

Thank you for your attention.

Francesco S. Carta

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Francesco S. Carta" <entuland@gmail.com>
Date: Wed, 7 Oct 2009 14:08:30 CST
Raw View
"Francesco S. Carta" <entul...@gmail.com> wrote:
> Hi everybody.
>
> Consider the following:
> -------
> struct A1{ void foo() {} };
> struct A2{ void foo() {} };
> struct B1 : A1, A2 {};
> struct B2 : A1, A2 {};
> struct C : B1, B2 {};
>
> int main() {
>    C c;
>    c.B1::A1::foo();}
>
> -------
>
> Is the last instruction well formed?

I'd like to know the official interpretation of the Standard about
this case.

The wording given in [class.mi] 4 seems clear to me:

"In such lattices, explicit qualification can be used to specify which
subobject is meant."

Where "such lattices" refers to this example:
-------
     class L { public: int next;  /* ... */ };
     class A : public L { /* ... */ };
     class B : public L { /* ... */ };
     class C : public A, public B { void f(); /* ... */ };
-------

Right after the above sentence, the Standard continues with:

"The body of function C::f could refer to the member next of each L
subobject:
     void C::f() { A::next = B::next; }
Without the A:: or B:: qualifiers, the definition of C::f above would
be ill-formed because of ambiguity (class.member.lookup)."

Here is my example again:

-------
struct A1{ void foo() {} };
struct A2{ void foo() {} };
struct B1 : A1, A2 {};
struct B2 : A1, A2 {};
struct C : B1, B2 {};

int main() {
    C c;
    c.B1::A1::foo();
}
-------

Two of the most important/widespread C++ compilers out there reject
the last instruction.

They correctly find foo() in the definition of A1, but since A1 is a
duplicated base class of C (and since they reject the statement as
being ambiguous) they seem to be ignoring the "B1::" part of the
qualification chain.

I'm not so sure I'm interpreting the Standard in the correct way and
I'd like to clarify it with your help.

Thanks for reading,
heave good time,
Francesco S. Carta

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Johannes Schaub (litb)" <schaub-johannes@web.de>
Date: Thu, 8 Oct 2009 14:19:43 CST
Raw View
Francesco S. Carta wrote:

> Hi everybody.
>
> Consider the following:
> -------
> struct A1{ void foo() {} };
> struct A2{ void foo() {} };
> struct B1 : A1, A2 {};
> struct B2 : A1, A2 {};
> struct C : B1, B2 {};
>
> int main() {
>    C c;
>    c.B1::A1::foo();
> }
> -------
>
> Is the last instruction well formed?
>

11.2/5 contains this wording:

> If a class member access operator, including an implicit "this->" is used
to access a nonstatic data member or nonstatic member function, the
reference is ill-formed if the left operand (considered as a pointer in the
"." operator case) cannot be implicitly converted to a pointer to the naming
class of the right operand.

In your case, the naming class is A1 (named by B1::A1), and converting C* to
A1* is ill-formed. You can get around this by doing

 ((B1&)c).A1::foo();


--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Francesco S. Carta" <entuland@gmail.com>
Date: Fri, 9 Oct 2009 10:07:29 CST
Raw View
Johannes Schaub (litb) wrote:
> Francesco S. Carta wrote:
> > Hi everybody.
>
> > Consider the following:
> > -------
> > struct A1{ void foo() {} };
> > struct A2{ void foo() {} };
> > struct B1 : A1, A2 {};
> > struct B2 : A1, A2 {};
> > struct C : B1, B2 {};
>
> > int main() {
> >    C c;
> >    c.B1::A1::foo();
> > }
> > -------
>
> > Is the last instruction well formed?
>
> 11.2/5 contains this wording:
>
> > If a class member access operator, including an implicit "this->" is used
>
> to access a nonstatic data member or nonstatic member function, the
> reference is ill-formed if the left operand (considered as a pointer in the
> "." operator case) cannot be implicitly converted to a pointer to the naming
> class of the right operand.
>
> In your case, the naming class is A1 (named by B1::A1), and converting C* to
> A1* is ill-formed. You can get around this by doing
>
>  ((B1&)c).A1::foo();

Hi Johannes, thanks for your reply.

According to the clause you cited, the example given by the Standard
in [class.mi] 3 seems comparable to my example:

-------
      class L { public: int next; };
      class A : public L { };
      class B : public L { };
      class C : public A, public B { void f(); };
-------

In the above Standard's example it isn't possible to implicitly
convert a C to an L, because L is a duplicated base class of C,
nonetheless it is possible to access the 'next' member of a specific L
subobject of C by using explicit qualification, just like the Standard
itself exemplifies:

-------
      void C::f() { A::next = B::next; }
-------

As above, in my example:

-------
struct A1{ void foo() {} };
struct A2{ void foo() {} };
struct B1 : A1, A2 {};
struct B2 : A1, A2 {};
struct C : B1, B2 {};

int main() {
     C c;
     c.B1::A1::foo();
}
-------

The C class cannot be implicitly converted to an A1 class, for the
same reason as above, but here the qualification is meant to resolve
two ambiguities:
- foo() being a member of both A1 and A2;
- A1 being a duplicated base of C;

I wasn't able to find any reference to such a limitation of what the
qualification can resolve or not, and so far I believe that the last
statement of my example should be considered as well-formed, but maybe
I'm still missing some other clause which makes clear that my example
is ill-formed (likely so).

Regarding the mentioned Standard's example, I understand that the
clause you cited could refer not to an implicit conversion from C to
L, but to the implicit conversion from C to A or from C to B, called
for by the explicitly used qualification.

Once such an implicit conversion is done, the name 'next' isn't
ambiguous anymore because there is only one 'next' in A's group of
names (the same applies for B).

This still puzzles me: again referring to the Standard's example, if I
change the definition of C::f() to:

-------
  void C::f() { A::L::next = B::L::next; }
-------

then compilers[*] reject it as just they do for the last statement of
my example, while those qualification chains should be simply
considered as redundant.

All these considerations of mine come from several explicit examples
given by B. Stroustrup in TC++PL 3rd ed., but I know that this book
predates the current Standard and maybe things have changed about
these issues.

I must point out that this is something onto which I've stumbled upon
while testing compilers for available features, it isn't something I
need in a strict sense, and even in such a case, this problem is
easily worked around, as you have shown.

Thanks a lot for your attention.

Francesco S. Carta

[*] here "compilers" stands for "two of the most important/widespread C
++ compilers". I still didn't report these issues to their
implementers because I'm still not sure if they're allowed to reject
such statements or not.

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Johannes Schaub (litb)" <schaub-johannes@web.de>
Date: Fri, 9 Oct 2009 16:51:35 CST
Raw View
Francesco S. Carta wrote:

> Johannes Schaub (litb) wrote:
>> Francesco S. Carta wrote:
> This still puzzles me: again referring to the Standard's example, if I
> change the definition of C::f() to:
>
> -------
>   void C::f() { A::L::next = B::L::next; }
> -------
>
> then compilers[*] reject it as just they do for the last statement of
> my example, while those qualification chains should be simply
> considered as redundant.
>
I'm sorry for the double post - but i forgot an important piece, i think. If
you say A::L::, then this doesn't say "walk the inheritance graph over A to
L", but it just says "find the member declaration in A::L::", without
remembering that we walked over "A" for the pupose of determining a sub-
object. You would have the same effect by saying "::L::next" instead in this
example.

I said in my other post that name lookup keeps track of base class sub-
objects, but i think that was a bit confusing. It doesn't keep track of that
you named "L" over "A::" of course. It just keeps track of sub-objects of
"L" itself while looking up "next" within it (since L has no bases, we don't
have base class subobjects in it), so keeping the subobjects that name-
lookup used wouldn't help in our case anyway, i think. Hope this helps.


--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Johannes Schaub (litb)" <schaub-johannes@web.de>
Date: Fri, 9 Oct 2009 16:51:19 CST
Raw View
Francesco S. Carta wrote:

> Johannes Schaub (litb) wrote:
>> Francesco S. Carta wrote:
>> > Hi everybody.
>>
>> > Consider the following:
>> > -------
>> > struct A1{ void foo() {} };
>> > struct A2{ void foo() {} };
>> > struct B1 : A1, A2 {};
>> > struct B2 : A1, A2 {};
>> > struct C : B1, B2 {};
>>
>> > int main() {
>> >    C c;
>> >    c.B1::A1::foo();
>> > }
>> > -------
>>
>> > Is the last instruction well formed?
>>
>> 11.2/5 contains this wording:
>>
>> > If a class member access operator, including an implicit "this->" is
>> > used
>>
>> to access a nonstatic data member or nonstatic member function, the
>> reference is ill-formed if the left operand (considered as a pointer in
>> the "." operator case) cannot be implicitly converted to a pointer to the
>> naming class of the right operand.
>>
>> In your case, the naming class is A1 (named by B1::A1), and converting C*
>> to A1* is ill-formed. You can get around this by doing
>>
>>  ((B1&)c).A1::foo();
>
> Hi Johannes, thanks for your reply.
>
> According to the clause you cited, the example given by the Standard
> in [class.mi] 3 seems comparable to my example:
>
> -------
>       class L { public: int next; };
>       class A : public L { };
>       class B : public L { };
>       class C : public A, public B { void f(); };
> -------
>
> In the above Standard's example it isn't possible to implicitly
> convert a C to an L, because L is a duplicated base class of C,
> nonetheless it is possible to access the 'next' member of a specific L
> subobject of C by using explicit qualification, just like the Standard
> itself exemplifies:
>
> -------
>       void C::f() { A::next = B::next; }
> -------
>
In this case, the naming class is A and B respectively, and the implicit
class member access is

(*this).A::next
(*this).B::next

respectively. C* can be converted to A* and B* respectively, so this example
is well formed. "Naming class" is defined at 11.2/4

> Regarding the mentioned Standard's example, I understand that the
> clause you cited could refer not to an implicit conversion from C to
> L, but to the implicit conversion from C to A or from C to B, called
> for by the explicitly used qualification.
>
> Once such an implicit conversion is done, the name 'next' isn't
> ambiguous anymore because there is only one 'next' in A's group of
> names (the same applies for B).
>
> This still puzzles me: again referring to the Standard's example, if I
> change the definition of C::f() to:
>
> -------
>   void C::f() { A::L::next = B::L::next; }
> -------
>
> then compilers[*] reject it as just they do for the last statement of
> my example, while those qualification chains should be simply
> considered as redundant.
>
I can't give a definitive rationale since i don't know it, but i suspect the
reason is to be able to unambiguously refer to a single base class sub-
object's member during evaluation of a (possibly implicit) member-access.

Since name-lookup just yields a declaration and there is just a single
declaration of "next", A::next and A::L::next respectively will both
unambiguously yield

L::next

as the result of name-lookup. But to be able to determine what sub-object is
actually referenced, name-lookup alone can't satisfy that. For this, the
object-expression (interpreted as a pointer) is implicitly converted to the
type referred by the nested-name-specifier of the right side. So, to "A*" or
"L*" respectively.

The result of that will give the address of the sub-object containing the
member and should be an unambiguous. So, if you happened to use "A::L::" as
the nested-name-specifier, it's ambiguous, and if you just use "A::" it's
not.

I couldn't find wording that explicitly states this mechanism, however.
During name-lookup, the sub-object is carried along (to determine name
hiding), but i think it's then thrown away since it's not part of name-
lookup result. I wonder whether there is some paragraph stating this
mechanism explicitly?

--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]





Author: "Francesco S. Carta" <entuland@gmail.com>
Date: Sat, 10 Oct 2009 12:26:46 CST
Raw View
On 10 Ott, 00:51, "Johannes Schaub (litb)" <schaub-johan...@web.de>
wrote:
> Francesco S. Carta wrote:
> > Johannes Schaub (litb) wrote:
> >> Francesco S. Carta wrote:
> >> > Hi everybody.
>
> >> > Consider the following:
> >> > -------
> >> > struct A1{ void foo() {} };
> >> > struct A2{ void foo() {} };
> >> > struct B1 : A1, A2 {};
> >> > struct B2 : A1, A2 {};
> >> > struct C : B1, B2 {};
>
> >> > int main() {
> >> >    C c;
> >> >    c.B1::A1::foo();
> >> > }
> >> > -------
>
> >> > Is the last instruction well formed?
>
> >> 11.2/5 contains this wording:
>
> >> > If a class member access operator, including an implicit "this->" is
> >> > used
>
> >> to access a nonstatic data member or nonstatic member function, the
> >> reference is ill-formed if the left operand (considered as a pointer in
> >> the "." operator case) cannot be implicitly converted to a pointer to the
> >> naming class of the right operand.
>
> >> In your case, the naming class is A1 (named by B1::A1), and converting C*
> >> to A1* is ill-formed. You can get around this by doing
>
> >>  ((B1&)c).A1::foo();
>
> > Hi Johannes, thanks for your reply.
>
> > According to the clause you cited, the example given by the Standard
> > in [class.mi] 3 seems comparable to my example:
>
> > -------
> >       class L { public: int next; };
> >       class A : public L { };
> >       class B : public L { };
> >       class C : public A, public B { void f(); };
> > -------
>
> > In the above Standard's example it isn't possible to implicitly
> > convert a C to an L, because L is a duplicated base class of C,
> > nonetheless it is possible to access the 'next' member of a specific L
> > subobject of C by using explicit qualification, just like the Standard
> > itself exemplifies:
>
> > -------
> >       void C::f() { A::next = B::next; }
> > -------
>
> In this case, the naming class is A and B respectively, and the implicit
> class member access is
>
> (*this).A::next
> (*this).B::next
>
> respectively. C* can be converted to A* and B* respectively, so this example
> is well formed. "Naming class" is defined at 11.2/4
>
>
>
> > Regarding the mentioned Standard's example, I understand that the
> > clause you cited could refer not to an implicit conversion from C to
> > L, but to the implicit conversion from C to A or from C to B, called
> > for by the explicitly used qualification.
>
> > Once such an implicit conversion is done, the name 'next' isn't
> > ambiguous anymore because there is only one 'next' in A's group of
> > names (the same applies for B).
>
> > This still puzzles me: again referring to the Standard's example, if I
> > change the definition of C::f() to:
>
> > -------
> >   void C::f() { A::L::next = B::L::next; }
> > -------
>
> > then compilers[*] reject it as just they do for the last statement of
> > my example, while those qualification chains should be simply
> > considered as redundant.
>
> I can't give a definitive rationale since i don't know it, but i suspect the
> reason is to be able to unambiguously refer to a single base class sub-
> object's member during evaluation of a (possibly implicit) member-access.
>
> Since name-lookup just yields a declaration and there is just a single
> declaration of "next", A::next and A::L::next respectively will both
> unambiguously yield
>
> L::next
>
> as the result of name-lookup. But to be able to determine what sub-object is
> actually referenced, name-lookup alone can't satisfy that. For this, the
> object-expression (interpreted as a pointer) is implicitly converted to the
> type referred by the nested-name-specifier of the right side. So, to "A*" or
> "L*" respectively.
>
> The result of that will give the address of the sub-object containing the
> member and should be an unambiguous. So, if you happened to use "A::L::" as
> the nested-name-specifier, it's ambiguous, and if you just use "A::" it's
> not.
>
> I couldn't find wording that explicitly states this mechanism, however.
> During name-lookup, the sub-object is carried along (to determine name
> hiding), but i think it's then thrown away since it's not part of name-
> lookup result. I wonder whether there is some paragraph stating this
> mechanism explicitly?

I have read over and over several parts, and finally I believe that
the paragraph you pointed out in first place (11.2 [class.access.base]
4) is enough to tell that the last statement of my example is ill-
formed, just as using "A::L::next" in that Standard's example would
be.

The Standard doesn't seem in need to define the mechanism used by
implementations in order to resolve duplicated base classes, as far as
requirements are met; the fact that the object must be implicitly
convertible to the naming class of the requested member is enough to
tell that the naming class must be directly and uniquely identified,
hence, if the naming class is a duplicated base of that object, that
call is ill-formed.

It would be nice if implementations could allow redundant
qualifications in such cases, and it would be even nicer if a multiple
ambiguity such as the one of my example could be resolved only by
climbing the qualification chain, but that would mean changing the
wording in 11.2/4, and I'm not going to propose it.

Thanks a lot for your time Johannes, and thanks everybody for reading.

All the best,
Francesco S. Carta


--
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@netlab.cs.rpi.edu]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://www.comeaucomputing.com/csc/faq.html                      ]