Topic: Multiple inheritence, virtual functions, and ambiguity
Author: rfg@netcom.com (Ronald F. Guilmette)
Date: Tue, 1 Jun 1993 08:03:55 GMT Raw View
In article <solomon.738433165@cs.wisc.edu> solomon@gjetost.cs.wisc.edu (Marvin Solomon) writes:
+When do members with the same name inherited from distinct base classes
+cause an ambiguity? Some time ago, I came up with the following example:
+ extern "C" void printf(char *,...);
+
+ struct A { virtual void f() { printf("A::f\n"); } };
+ struct B { virtual void f() { printf("B::f\n"); } };
+ struct C : public A, public B {};
+
+ main() {
+ C *c = new C;
+ A *a = c;
+ // c->f() is ambiguous
+ a->f(); // prints "A::f" ?
+ }
+
+I spent a good deal of time poring over the ARM trying to settle the issue.
And came away with nothing for your trouble, right?
+Is there a C++ theologian out there that can answer this definitively?
Definitively in terms of what? There is no standard yet, remember.
+Is it being considered by the standards committee?
Not at this time. The committee has not yet even agreed upon a number of
even more basic and rudimentary issues than the one you have raised, e.g.
there is as yet no agreement on whether or not C++ has such standard C
things as sequence points and even the presence (or absence) of lvalues
in the language is still a matter of debate within the committee. Truth
to tell, there is (as of this time) still not even agreement within the
committee as to what the standard will standardize (e.g. "conforming
programs", "conforming processors", both?, neither?).
+Am I simply overlooking some obvious (!) answer somewhere in the ARM?
Nope. It ain't. So don't bother looking for it.
The behavior you are interested in can only be explained in terms of the
"dynamic types" of objects and expressions. To put it quite plainly,
there just is no "calculus of dynamic types" specified in the ARM (or
the the X3J16 working paper). In other words, there is no way for you
(or anyone) to say definitively that this expression or that expression
definitely has this or that dynamic type (according to such-and-such rule)
because the set of rules upon which you might base such a statement simply
have not been written yet (nor do I expect that they will be anytime soon).
Again, I feel compelled to point out that the question you have asked goes
well and far beyond the current state of the art with respect to the
specification of the C++ language. As I have noted, a precise
and definitive answer to your question could only be derived from a
complete set of rules which would tell you exactly what the dynamic type
of any given expression must be at any given point in time. But before
you worry too much about the absence of such a set of rules (from the ARM
and from the current X3J16 working paper) I recommend that you consider
the more fundamental problem that neither the ARM nor the X3J16 WP even
contain a complete set of rules by which one could deduce the *static*
type of any given expression! (Of course, the ANSI/ISO C standard solved
the problem of providing a "calculus of static types" for the C language
long ago, but much of the preceeding work done for the ANSI/ISO C standard
seem to be being ignored by the X3J16 committee... and even if it were not
for *that* problem, you gotta remember that C++ is significantly more
complicated that C, even in terms of static typing... for example, don't
forget about all the complexity introduced by the possibility of the
implicit application of arbitrary user-defined type conversion operators!)
--
// Ron ("Loose Cannon") Guilmette uucp: ...uunet!lupine!segfault!rfg
//
// "On the one hand I knew that programs could have a compelling
// and deep logical beauty, on the other hand I was forced to
// admit that most programs are presented in a way fit for
// mechanical execution, but even if of any beauty at all,
// totally unfit for human appreciation."
// -- Edsger W. Dijkstra
Author: solomon@gjetost.cs.wisc.edu (Marvin Solomon)
Date: Wed, 26 May 1993 16:19:25 GMT Raw View
When do members with the same name inherited from distinct base classes
cause an ambiguity? Some time ago, I came up with the following example:
extern "C" void printf(char *,...);
struct A { virtual void f() { printf("A::f\n"); } };
struct B { virtual void f() { printf("B::f\n"); } };
struct C : public A, public B {};
main() {
C *c = new C;
A *a = c;
// c->f() is ambiguous
a->f(); // prints "A::f" ?
}
I spent a good deal of time poring over the ARM trying to settle the
issue. The problem is that is that the "virtual" concept, both for functions
and base classes, appears to be defined by example and by implied
implementation rather than by any kind of abstract principle. The closest
I could find to a resolution was in Section 7.1.2 on page 105 of the ARM:
"For virtual functions, the dominance rule is what guarantees that the same
function is called independently of the static type of the pointer,
reference or name of the object for which it is called."
The dominance rule is discussed in the context of virtual base classes and
"diamond inheritence", which don't apply here. Nonetheless, this sentence
expresses a clear goal that is violated if a->f() is allowed.
Similarly, Section 10.2 states on page 208,
"That is, the interpretation of the call of a virtual function depends
on the type of the object for which it is called, whereas the
interpretation of a call of a nonvirtual member function depends only on
the type of the pointer or reference denoting that object."
I tried out the example on two handy C++ compilers: Cfront 3.0.1 and
g++ 2.2.2. Cfront declared a->f() to be fine (it prints "A::f").
G++ core dumped. I reported the "bug" both to AT&T and to gnu.
AT&T responded as follows:
> This is in reference to your reported C++ problem regarding dominance
> and ambiguities. We feel that this is a user misunderstanding and that
> cfront's behavior in this matter is correct. When there is no virtual
> inheritance, dominance is not an issue.
So, it's not a bug, it's a feature :-).
G++ 2.3.3 does the "right thing":
bug.c:4: ambiguous virtual function `void B::f ()'
bug.c:3: ambiguating function `void A::f ()' (joined by type `C')
I recently got the following message from Mike Stump:
>
> This problem has been fixed, and should not exist in gcc 2.4.0.
>
> Our behaviour matches ARM and Cfront's.
I'm glad it's "fixed", in the sense of matching Cfront's behavior.
I'm curious, however, where in the ARM he finds support for the
assertion that the "fixed" behavior matches the ARM. I'm not saying it
doesn't, just that after going over all the relevant sections I could find,
I couldn't settle the issue one way or the other.
The "principled" interpretation is that a->f() is resolved based on the
dynamic type of *a, if f() is virtual. In the example cited, *a has type C,
and in C, f() is ambiguous. (Of course, this begs the question of how
to determine if f() is virtual. In the case at hand, however, it clearly
is. G++ 2.3.3 accepts the program if "virtual" is removed from either
A::f or B::f. More on this below.)
The "implementation" interpretation appears to say that name resolution works
"as if" it were done by a particular algorithm. The algorithm seems to
be scattered here and there in the ARM, but I can't find a clear statement
in any one place, and my attempts to reconstruct it seem to come up with
some holes. One could, of course, give a "definition by implementation",
describing a particular implementation strategy (with vtables, etc.),
but that approach has the obvious problem of overspecification.
Here's my best guess at the intended "abstract" algorithm. It implies that
all lookup is done at runtime, but of course, any reasonable implementation
does most of it at compile time.
To resolve p->f(args), where p has type A* and *p is an instance of B:
1. Search A's ancestors for a class C that declares f and dominates
all others.
2. Among the declarations of f in C, find a unique one that matches
the args. Call this the "matching declaration"
3. Verify that that the matching declaration satisfies
access restrictions.
4. If the matching declaration is virtual [see note], see whether
any class D "between" C and B overrides f [see note]. If so, use
the definition in D. Otherwise use definition in C.
Note: It is similarly obscure exactly when a declaration overrides
another one. Section 10.2 says a derived class' f overrides a base
class' virtual f, if the signatures are identical (going into a rather
lengthy digression justifying the requirement that they be identical rather
than merely "matching"), but it is silent about the situation when there are
multiple base classes. Consider, for example,
struct A { void f() { printf("A::f\n"); } };
struct B { virtual void f() { printf("B::f\n"); } };
struct C : public A, public B { void f() { printf("C::f\n"); }};
Does C::f override B::f or hide A::f? The answer would appear to be "both".
Is there a C++ theologian out there that can answer this definitively?
Is it being considered by the standards committee?
Am I simply overlooking some obvious (!) answer somewhere in the ARM?
--
Marvin Solomon, Professor
Computer Sciences Department
University of Wisconsin
1210 W. Dayton St., Madison WI, USA
(608) 263-2844
solomon@cs.wisc.edu
Author: cbergren@comm.mot.com (Craig Bergren)
Date: Thu, 27 May 1993 04:11:31 GMT Raw View
Marvin Solomon (solomon@gjetost.cs.wisc.edu) wrote:
> When do members with the same name inherited from distinct base classes
> cause an ambiguity? Some time ago, I came up with the following example:
When they are at the same level of the class hierarchy. Neither
dominates the other because A and B are on parallel inheritence paths to
C. In your example, A::f() and B::f() will be inherited by C making it
look like C has two f() functions. This is ambiguous.
> "For virtual functions, the dominance rule is what guarantees that the same
> function is called independently of the static type of the pointer,
> reference or name of the object for which it is called."
> The dominance rule is discussed in the context of virtual base classes and
> "diamond inheritence", which don't apply here. Nonetheless, this sentence
> expresses a clear goal that is violated if a->f() is allowed.
In your example, a->f() is not dominated by another f(), therefore it
is the correct f() to call.
If you add a function, f(), to C.
> extern "C" void printf(char *,...);
>
> struct A { virtual void f() { printf("A::f\n"); } };
> struct B { virtual void f() { printf("B::f\n"); } };
> struct C : public A, public B {
void f() { printf("C::f\n"); };
> };
>
> main() {
> C *c = new C;
> A *a = c;
B *b = c;
c->f(); // no longer ambiguous because C::f() dominates A::f and B::f
a->f(); // prints "C::f"
b->f(); // prints "C::f"
> }
> Similarly, Section 10.2 states on page 208,
> "That is, the interpretation of the call of a virtual function depends
> on the type of the object for which it is called, whereas the
> interpretation of a call of a nonvirtual member function depends only on
> the type of the pointer or reference denoting that object."
Again there is no f() in your example which dominates either B::f or
A::f. Therfore the virtual keywords have no effect on anything in
class C. With the addition of C::f(), you will see that this
long sentence from the ARM becomes true.
> Note: It is similarly obscure exactly when a declaration overrides
> another one. Section 10.2 says a derived class' f overrides a base
> class' virtual f, if the signatures are identical (going into a rather
> lengthy digression justifying the requirement that they be identical rather
> than merely "matching"), but it is silent about the situation when there are
> multiple base classes. Consider, for example,
> struct A { void f() { printf("A::f\n"); } };
> struct B { virtual void f() { printf("B::f\n"); } };
> struct C : public A, public B { void f() { printf("C::f\n"); }};
> Does C::f override B::f or hide A::f? The answer would appear to be "both".
Yes it is. In the above example main, a->f() prints "A::f", b->f() and
c->f() both print "C::f".
--
Craig Bergren
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
! The views I hold are not mine, nor those of my employer. !
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Author: maxtal@physics.su.OZ.AU (John Max Skaller)
Date: Thu, 27 May 1993 20:17:40 GMT Raw View
In article <solomon.738433165@cs.wisc.edu> solomon@gjetost.cs.wisc.edu (Marvin Solomon) writes:
>When do members with the same name inherited from distinct base classes
>cause an ambiguity? Some time ago, I came up with the following example:
>
> struct A { virtual void f() { printf("A::f\n"); } };
> struct B { virtual void f() { printf("B::f\n"); } };
> struct C : public A, public B {};
>
> main() {
> C *c = new C;
> A *a = c;
> // c->f() is ambiguous
> a->f(); // prints "A::f" ?
> }
>
>I spent a good deal of time poring over the ARM trying to settle the
>issue.
So did I, and I had a long argument on the committee
reflector about it too :-)
>The problem is that is that the "virtual" concept, both for functions
>and base classes, appears to be defined by example and by implied
>implementation rather than by any kind of abstract principle.
The rules for name lookup and virtual call resolution
are not well defined are they?
>
>I tried out the example on two handy C++ compilers: Cfront 3.0.1 and
>g++ 2.2.2. Cfront declared a->f() to be fine (it prints "A::f").
>G++ core dumped. I reported the "bug" both to AT&T and to gnu.
>AT&T responded as follows:
> > This is in reference to your reported C++ problem regarding dominance
> > and ambiguities. We feel that this is a user misunderstanding and that
> > cfront's behavior in this matter is correct. When there is no virtual
> > inheritance, dominance is not an issue.
>So, it's not a bug, it's a feature :-).
Yes. The way I now understand it is that the
static type of the pointer is used to perform a lookup, then,
if that function happens to be virtual one checks if that function
is dominated .. which it isnt in this case.
>
>The "principled" interpretation is that a->f() is resolved based on the
>dynamic type of *a, if f() is virtual. In the example cited, *a has type C,
>and in C, f() is ambiguous. (Of course, this begs the question of how
>to determine if f() is virtual. In the case at hand, however, it clearly
>is. G++ 2.3.3 accepts the program if "virtual" is removed from either
>A::f or B::f. More on this below.)
That was my interpretation too, but it is not the
intended one. :-(
>
>The "implementation" interpretation appears to say that name resolution works
>"as if" it were done by a particular algorithm. The algorithm seems to
>be scattered here and there in the ARM, but I can't find a clear statement
>in any one place, and my attempts to reconstruct it seem to come up with
>some holes.
Yes.
>Here's my best guess at the intended "abstract" algorithm.
See below.
>multiple base classes. Consider, for example,
>
> struct A { void f() { printf("A::f\n"); } };
> struct B { virtual void f() { printf("B::f\n"); } };
> struct C : public A, public B { void f() { printf("C::f\n"); }};
>
>Does C::f override B::f or hide A::f? The answer would appear to be "both".
>
>Is there a C++ theologian out there that can answer this definitively?
>Is it being considered by the standards committee?
Yes.
>Am I simply overlooking some obvious (!) answer somewhere in the ARM?
No, not in my opinion.
Here is my attempt to reconstruct the result of a long exchange
with Bjarne. I hope I get this right :-) I've ignored
all issues of access and overloading here:
Phase 1.
1) Lookup the name on the subobject graph of the
static type of the pointer.
2) If you find a single object, goto Phase 2.
(For example a static member of two different subobjects
of the same type constitutes a single object)
3) If one name dominates all the others, thats it, goto Phase 2
4) Its ambiguous: error.
Phase 2.
1) If the name is a virtual function look at the
*complete* objects subobject graph and see
if any name dominates that name.
If not, call the function.
2) if so, then if there is a unique dominator,
call that function.
3) its ambiguous: machine crash :-)
The 'machine crash' in step (3) above occurs when you have
this:
struct V { virtual f(); }
struct L : virtual V { f(); }
struct R : virtual V { f(); }
struct D : L, R {}
V* v= new D;
v->f(); // crash
In my opinion, D is abstract and should not be constructed.
That is what Eiffel does. (In effect, D::f is defined as
pure virtual)
--
JOHN (MAX) SKALLER, INTERNET:maxtal@suphys.physics.su.oz.au
Maxtal Pty Ltd, CSERVE:10236.1703
6 MacKay St ASHFIELD, Mem: SA IT/9/22,SC22/WG21
NSW 2131, AUSTRALIA