[comp.lang.c++] Faulty Ambiguity Detection with Virtual Bases

sdm@cs.brown.edu (Scott Meyers) (10/30/90)

Normally, C++ doesn't complain about potential ambituity, only ambiguity at
a point of call.  With MI, for example, if a function is inherited through
two paths, the function to be called must normally be disambiguated only
when it is called.  As the ARM puts it (p. 206):

    Only the *use* of a name in an ambiguous way is an error, however;
    combining classes that contain members with the same name in an
    inheritance scheme is not an error.

But consider the following example, where function foo is inherited through
two paths in class Bottom:

    class VBase {
    public:
      virtual void foo() = 0;
    };

    class Middle1: virtual public VBase {
    public:
      virtual void foo();
    };

    class Middle2:  virtual public VBase {
    public:
      virtual void foo();
    };

    class Bottom: public Middle1, public Middle2 {
    };

AT&T cfront 2.0 and Sun's cfront 2.1 (beta) both give the following error:

    "ambiguity.C", line 16: error: virtual base: ambiguous Middle1::foo()
                                                 and Middle2::foo()

On the other hand, g++ 1.37 takes it without complaining.  Furthermore, if
either Middle1 or Middle2 is made a nonvirtual base class, then cfront is
happy, too, even though the ambiguity remains.

    1.  Does anybody feel this is not an error in cfront?

    2.  Can anybody explain to me why it should make a difference whether a
        base class is virtual or not in the above example?


Thanks,

Scott
sdm@cs.brown.edu

shopiro@alice.att.com (Jonathan Shopiro) (10/31/90)

In article <54733@brunix.UUCP>, sdm@cs.brown.edu (Scott Meyers) writes:
> Normally, C++ doesn't complain about potential ambituity, only ambiguity at
> a point of call.  With MI, for example, if a function is inherited through
> two paths, the function to be called must normally be disambiguated only
> when it is called.  As the ARM puts it (p. 206):
> 
>     Only the *use* of a name in an ambiguous way is an error, however;
>     combining classes that contain members with the same name in an
>     inheritance scheme is not an error.
> 
> But consider the following example, where function foo is inherited through
> two paths in class Bottom:
> 
>     class VBase {
>     public:
>       virtual void foo() = 0;
>     };
> 
>     class Middle1: virtual public VBase {
>     public:
>       virtual void foo();
>     };
> 
>     class Middle2:  virtual public VBase {
>     public:
>       virtual void foo();
>     };
> 
>     class Bottom: public Middle1, public Middle2 {
>     };
> 
> AT&T cfront 2.0 and Sun's cfront 2.1 (beta) both give the following error:
> 
>     "ambiguity.C", line 16: error: virtual base: ambiguous Middle1::foo()
>                                                  and Middle2::foo()

The problem here is that the vtbl can't be constructed.  Suppose you
had the code

	VBase*	vbp = new Bottom;	// pointer conversion
	vbp->foo();			// virtual function call

Now the function call must be ambiguous, because you don't know
whether to call Middle1::foo() or Middle2::foo().  But there is no way
that the compiler can object to the call itself, because it can't know
that vbp actually points to an instance of Bottom.  So the error
must be generated at the point of declaration.

> On the other hand, g++ 1.37 takes it without complaining.

But what function gets called?  If it's either Middle1::foo() or Middle2::foo()
I'd call that a bug in g++ 1.37.

>  Furthermore, if
> either Middle1 or Middle2 is made a nonvirtual base class, then cfront is
> happy, too, even though the ambiguity remains.

No, actually the declarations are now okay, but the pointer conversion
above becomes ambiguous, because there are now two instances of VBase
hidden inside a Bottom object.

You could write (note in the following at least one of Middle[12] is
_not_virtually_derived_ from VBase):

	Bottom*		bp = new Bottom;
	Middle1*	m1p = new Bottom;
	Middle2*	m2p = new Bottom;
	VBase*		vbp;
	bp->foo();		// error: ambiguous call
	vbp = bp;		// error: two instances of VBase
	vbp = m1p;
	vbp->foo();		// calls Middle1::foo()
	vbp = m2p;
	vbp->foo();		// calls Middle2::foo()

>     1.  Does anybody feel this is not an error in cfront?

Apparently yes.

>     2.  Can anybody explain to me why it should make a difference whether a
>         base class is virtual or not in the above example?

I hope the above helps.
-- 
		Jonathan E. Shopiro
		AT&T Bell Laboratories, Warren, NJ  07059-0908
		shopiro@research.att.com   (201) 580-4229

ilanc@microsoft.UUCP (Ilan CARON) (10/31/90)

In article <54733@brunix.UUCP> sdm@cs.brown.edu (Scott Meyers) writes:
>    class VBase {
>    public:
>      virtual void foo() = 0;
>    };
>
>    class Middle1: virtual public VBase {
>    public:
>      virtual void foo();
>    };
>
>    class Middle2:  virtual public VBase {
>    public:
>      virtual void foo();
>    };
>
>    class Bottom: public Middle1, public Middle2 {
>    };
>
>    1.  Does anybody feel this is not an error in cfront?
>
>    2.  Can anybody explain to me why it should make a difference whether a
>        base class is virtual or not in the above example?


[Apologies if this is the nth response... our newsfeed is abysmally slow]

This definition is in error since there is only *one* virtual function
table slot in VBase's vft for function foo() -- hence when creating an
instance of Bottom (which has only *one* embedded instance of VBase
because it's derived from virtually), the compiler can't know which
overriding function, Middle1::foo() or Middle2::foo(), to prefer.  C++
has no rules for resolving this particular ambiguity (note that
dominance is irrelevant here).  (Hence instances of Bottom can't be created,
hence the definition is in error).

On the other hand, the non-virtual case is fine.  There's no problem when
creating an instance of Bottom: the vft of the Vbase part of Middle1 gets
initialized with Middle1::foo(), similarly the Vbase part of Middle2.

Of course, a *reference* to foo() through an instance of Bottom needs
to be disambiguated: as in, a_bottom.Middle1::foo().

--ilan caron (uunet!microsoft!ilanc)

P.S. Yeah, I just *love* virtual inheritance...

jgro@lia (Jeremy Grodberg) (10/31/90)

In article <54733@brunix.UUCP< sdm@cs.brown.edu (Scott Meyers) writes:
<Normally, C++ doesn't complain about potential ambituity, only ambiguity at
<a point of call.  [...] But consider the following example, where function
<foo is inherited through two paths in class Bottom:
<
<    class VBase {
<    public:
<      virtual void foo() = 0;
<    };
<
<    class Middle1: virtual public VBase {
<    public:
<      virtual void foo();
<    };
<
<    class Middle2:  virtual public VBase {
<    public:
<      virtual void foo();
<    };
<
<    class Bottom: public Middle1, public Middle2 {
<    };
<
<AT&T cfront 2.0 and Sun's cfront 2.1 (beta) both give the following error:
<
<    "ambiguity.C", line 16: error: virtual base: ambiguous Middle1::foo()
<                                                 and Middle2::foo()
<
<On the other hand, g++ 1.37 takes it without complaining.  Furthermore, if
<either Middle1 or Middle2 is made a nonvirtual base class, then cfront is
<happy, too, even though the ambiguity remains.
<

The ARM, on page 235, says "To avoid ambiguous function definitions, all
redefinitions of a virtual function from a virtual base class must occur
on a single path through the inheritance structure."  This explains the
behavior of CFront.  However, this quote is taken from the commentary on
how virtual base classes with virtual functions could be implemented,
not from any section which is part of the ANSI base document, so it has
no formal status as defining the language.  The section that is part of
the formal specification does not address this issue at all (as far as I
can find).

In short, the reason CFront complains is because there is only one
pointer slot for a virtual function of a virtual base, and the compiler
can't put both Middle1::foo() and Middle2::foo() in the one slot.

g++ either uses a different mechanism for virtual function tables
(doubtful), fills the table's slot with a pointer to a function which 
returns a run-time error message (reasonable), or just puts one of the two 
functions in the table (a bug).

-- 
Jeremy Grodberg      "I don't feel witty today.  Don't bug me."
jgro@lia.com          

sdm@cs.brown.edu (Scott Meyers) (10/31/90)

In article <54733@brunix.UUCP> I wrote:
|  class VBase {
|  public:
|    virtual void foo() = 0;
|  };
|
|  class Middle1: virtual public VBase {
|  public:
|    virtual void foo();
|  };
|
|  class Middle2:  virtual public VBase {
|  public:
|    virtual void foo();
|  };
|
|  class Bottom: public Middle1, public Middle2 {
|  };

In article <58641@microsoft.UUCP> ilanc@microsoft.UUCP (Ilan CARON) writes:
|  This definition is in error since there is only *one* virtual function
|  table slot in VBase's vft for function foo() -- hence when creating an
|  instance of Bottom (which has only *one* embedded instance of VBase
|  because it's derived from virtually), the compiler can't know which
|  overriding function, Middle1::foo() or Middle2::foo(), to prefer.  C++
|  has no rules for resolving this particular ambiguity (note that
|  dominance is irrelevant here).  (Hence instances of Bottom can't be created,
|  hence the definition is in error).

What you say is true, but I claim this is a rationalization for a compiler
error, not an explanation as to why the code itself is wrong.  (Perhaps the
code is wrong, but I'd like to see a reference to the ARM where it says
it's wrong.)  

VBase::foo is pure virtual, which means it should never be called through
the vtable; the ARM explicitly says the program behavior is undefined if
that ever happens (p. 215).  (In cfront 2.0 such a call would result in a
runtime error.)  So where foo's slot in VBase's vtable points makes no
difference -- the pointer should never be used (which means it could be
optimized out of existence).  It also means that there can never be an
object of type VBase, only objects of classes derived from VBase, and foo
must be defined in those classes, so VBase::foo can't be called via a
reference/pointer to a VBAse.

When a Bottom object is created, the vtables for both Middle1 and Middle2
are used, and then there's no problem with space in the vtable.  And of
course the ambiguity on a call to foo still exists.

So I'm unconvinced, I still think this is a compiler error.  Am I still
overlooking something?

Scott
sdm@cs.brown.edu

rfg@NCD.COM (Ron Guilmette) (10/31/90)

In article <54733@brunix.UUCP> sdm@cs.brown.edu (Scott Meyers) writes:
<Normally, C++ doesn't complain about potential ambituity, only ambiguity at
<a point of call.  With MI, for example, if a function is inherited through
<two paths, the function to be called must normally be disambiguated only
<when it is called.  As the ARM puts it (p. 206):
<
<    Only the *use* of a name in an ambiguous way is an error, however;
<    combining classes that contain members with the same name in an
<    inheritance scheme is not an error.

I have always felt that this was a big mistake in the language design.
Why is it ever a good idea to let people build time bombs which just
sit around waiting to go off until somebody first touches them?

I feel that it would be much better to have compilers tell class authors
whenever they have created (unwittingly) the *possibility* of ambiguity.

Now somebody may argue that one might obtain a class A from one source of
code, and a class B from another source, and then one should be able to
write one's own class C, derived from both A and B, and have it pass
compilation in all cases (so that the author of C will not be bothered
unnecessarily with superfluous errors about inherited members of C
which never actually get used bu C's clients).  In my opinion, such an
argument is just bunk.  This is just a way of supporting and reinforcing
laziness on the part of the author of class C (who could easily fix all
conflicts by adding code within C and without touching either A or B).

<But consider the following example, where function foo is inherited through
<two paths in class Bottom:

[... text deleted...]

<AT&T cfront 2.0 and Sun's cfront 2.1 (beta) both give the following error:
<
<    "ambiguity.C", line 16: error: virtual base: ambiguous Middle1::foo()
<                                                 and Middle2::foo()
<
<On the other hand, g++ 1.37 takes it without complaining.  Furthermore, if
<either Middle1 or Middle2 is made a nonvirtual base class, then cfront is
<happy, too, even though the ambiguity remains.
<
<    1.  Does anybody feel this is not an error in cfront?

Yes.  At the very least, cfront should try to be more consistant about
what it defines as erroneous.

<    2.  Can anybody explain to me why it should make a difference whether a
<        base class is virtual or not in the above example?

I certainly can't.  Perhaps the authors of cfront will step forward and
make a statement.

-- 

// Ron Guilmette  -  C++ Entomologist
// Internet: rfg@ncd.com      uucp: ...uunet!lupine!rfg
// Motto:  If it sticks, force it.  If it breaks, it needed replacing anyway.

diamond@tkou02.enet.dec.com (diamond@tkovoa) (11/02/90)

In article <2351@lupine.NCD.COM> rfg@NCD.COM (Ron Guilmette) writes:
<In article <54733@brunix.UUCP> sdm@cs.brown.edu (Scott Meyers) writes:
<<With MI, for example, if a function is inherited through
<<two paths, the function to be called must normally be disambiguated only
<<when it is called.  As the ARM puts it (p. 206):  [deleted]
<
<I have always felt that this was a big mistake in the language design.
<Why is it ever a good idea to let people build time bombs which just
<sit around waiting to go off until somebody first touches them?

People can build time bombs no matter what you do.  Programmers who wish to
be safe will disambiguate every use of an inherited component at the point
where it is used, whether or not dismbiguation is necessary (yet).

<I feel that it would be much better to have compilers tell class authors
<whenever they have created (unwittingly) the *possibility* of ambiguity.

A warning (switchable on or off) might be reasonable.  This would be less
helpful than you imagine because:
<Now somebody may argue that one might obtain a class A from one source of
<code, and a class B from another source, and then one should be able to
<write one's own class C, derived from both A and B, and have it pass
<compilation in all cases (so that the author of C will not be bothered
<unnecessarily with superfluous errors about inherited members of C
<which never actually get used bu C's clients).  In my opinion, such an
<argument is just bunk.  This is just a way of supporting and reinforcing
<laziness on the part of the author of class C (who could easily fix all
<conflicts by adding code within C and without touching either A or B).
Suppose your A vendor adds a new feature to A which you don't use, and
your B vendor adds a new feature to B with the same name?  Suppose B and
C both come from the second vendor, C uses a feature of B and C uses other
features of A (i.e. the second vendor is also a client of the first vendor),
and the first vendor adds a feature to A with the same name?  Do you wait
for the second vendor to mail you a revised C?  Or do you hope that they
have already (previously unnecessarily) disambiguated all uses of inherited
features, and that you yourself have done the same in your classes?

-- 
Norman Diamond, Nihon DEC    diamond@tkov50.enet.dec.com
                                    (tkou02 is scheduled for demolition)
We steer like a sports car:  I use opinions; the company uses the rack.

petergo@microsoft.UUCP (Peter GOLDE) (11/03/90)

>What you say is true, but I claim this is a rationalization for a compiler
>error, not an explanation as to why the code itself is wrong.  (Perhaps the
>code is wrong, but I'd like to see a reference to the ARM where it says
>it's wrong.)  

p. 235: To avoid ambiguous function definitions, all redefinitions
of a virtual function from a virtual base must occur on a single
path through the inheritance structure.

---
Peter Golde                         petergo%microsoft@uunet.uu.net 
"Motion gives the illusion of progress"