Topic: Relaxing restrictions on covariant return types


Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/28
Raw View
Siemel B. Naran <sbnaran@uiuc.edu> wrote in article
<slrn7s88bf.k9h.sbnaran@localhost.localdomain>...

> On 25 Aug 1999 15:54:49 GMT, Wil Evers <bouncer@dev.null> wrote:
>
> >Even in this small example, a call to Derived::clone() via a
> >pointer to a Base1 cannot have the exact same implementation
> >as a call to Derived::clone() via a pointer to a Base2.  In
> >one of these cases, an extra pointer conversion is required.
>
> It is possible for "Derived * Derived::clone() const" to have
> the same exact implementation as "Base * Base::clone() const".
> If the compiler stores derived objects this way: base part first,
> derived-only part next, morederived-only part next, then
> &Base == &Derived == &MoreDerived.

It seems you missed my point.  In the example I gave, Derived had two base
classes:

 struct Base1 {
  /* ... */
  virtual Base1 *clone() const;
 } ;

 struct Base2 {
  /* ... */
  virtual Base2 *clone() const;
 };

 struct Derived : Base1, Base2 {
  /* ... */
  Derived *clone() const;
 };

For an object of type Derived, the language requires the Base1 and Base2
subobjects to have different addresses, which is why the calling context
matters.  For Derived::clone(), we have three cases:

 (1) Derived::clone() called through a Base1 *
  which is defined to return a Base1 *
 (2) Derived::clone() called through a Base2 *
  which is defined to return a Base2 *
 (3) Derived::clone() called through a Derived *
  which is defined to return a Derived *

Either (1) or (2) may have the exact same implementation as (3), but not
both.  In at least one case, a pointer adjustment is needed.  This is true
for every C++ compiler.

In other words: the current language rules for covariant return types
already contain a precedent in which the compiler must take the calling
context into account and decide whether a conversion is required.  This is
exactly what the compiler would need to do if the restrictions on covariant
return types were to be relaxed.

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Valentin Bonnard <Bonnard.V@wanadoo.fr>
Date: 1999/08/29
Raw View
> the language requires the Base1 and Base2
> subobjects to have different addresses,

Does it ?

--

Valentin Bonnard


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: postmast.root.admi.gov@iname.com (blargg)
Date: 1999/08/30
Raw View
In article <slrn7sjkam.2oe.sbnaran@localhost.localdomain>,
sbnaran@uiuc.edu wrote:

> On 29 Aug 1999 18:03:09 GMT, gbush@my-deja.com <gbush@my-deja.com> wrote:
> > Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:
> >> ???
>
> >>> the language requires the Base1 and Base2
> >>> subobjects to have different addresses,
> >>
> >> Does it ?
> >>
> >Actually, logic requires it. But the standard does mention it in
> >10.1p4; notice the word distinct. Can subobjects have the same address
> >and be distinct?
>
> Base1 and Base2 might have the same address if the compiler does the
> empty base optimization.
>
> struct B1 { };
> struct B2 { };
> struct D : B1, B2 { int d; };
>
> With the empty base optimization, sizeof(D)==sizeof(D::d) which is
> 4 on my system.  The B1 and B2 subobjects don't hold any space.
> So it seems to me that all of these expressions are true:

A compiler could also assign different addresses to the B1 and B2
sub-objects of D (even though it was performing empty base optimization),
perhaps to allow code that assumes that objects of different types will
never have the same address. This would add extra performance overhead
when converting pointers to the sub-objects, though, since a pointer
offset would have to be done when converting to/from a base class pointer.

Just take this simple example to prove to yourself (the poster that
thought the standard required sub-objects have different addresses) that
it's OK to have objects of different types with the same address:

    struct B { };
    struct D : B { };

    D d;

    if ( static_cast<void*> (&d) == static_cast<B*> (&d) )
        std::cout << "same\n";

I expect this to print "same" on most implementations of C++.

Here's a nice example that I bet some compilers that do the empty base
optimization fail on:

    struct B { };

    struct X : B { };
    struct Y : B { };

    struct D : X, Y { };

    D d;
    B& bx = static_cast<X&> (d);
    B& by = static_cast<Y&> (d);

    assert( &bx != &by );

Sure enough, it fails on the compiler I use (though I'm using one release
earlier than the latest release, because it's less-buggy than the current
release :-)

[snip]


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: gbush@my-deja.com
Date: 1999/08/29
Raw View
In article <37C81909.5D@wanadoo.fr>,
  Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:
>
> > the language requires the Base1 and Base2
> > subobjects to have different addresses,
>
> Does it ?
>
Actually, logic requires it. But the standard does mention it in
10.1p4; notice the word distinct. Can subobjects have the same address
and be distinct?

Gene Bushuyev


Sent via Deja.com http://www.deja.com/
Share what you know. Learn what you don't.


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/30
Raw View
On 29 Aug 1999 18:03:09 GMT, gbush@my-deja.com <gbush@my-deja.com> wrote:
> Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:
>> ???

>>> the language requires the Base1 and Base2
>>> subobjects to have different addresses,
>>
>> Does it ?
>>
>Actually, logic requires it. But the standard does mention it in
>10.1p4; notice the word distinct. Can subobjects have the same address
>and be distinct?

Base1 and Base2 might have the same address if the compiler does the
empty base optimization.

struct B1 { };
struct B2 { };
struct D : B1, B2 { int d; };

With the empty base optimization, sizeof(D)==sizeof(D::d) which is
4 on my system.  The B1 and B2 subobjects don't hold any space.
So it seems to me that all of these expressions are true:
(void*)&d == (void*)static_cast<B1*>(&d)
(void*)&d == (void*)static_cast<B2*>(&d)
(void*)static_cast<B1*>(&d) == (void*)static_cast<B2*>(&d)

The was a question recently about doing something like this:
D d; B1& b1=d; B2& b2=d;
std::cout << d << '\n'; // prints something
b1=B1();
std::cout << d << '\n'; // does this print same stuff as above?

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Valentin Bonnard <Bonnard.V@wanadoo.fr>
Date: 1999/08/30
Raw View
gbush@my-deja.com wrote:
>
> In article <37C81909.5D@wanadoo.fr>,
>   Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:
> >
> > > the language requires the Base1 and Base2
> > > subobjects to have different addresses,
> >
> > Does it ?

> Actually, logic requires it.

So please explain the logic !

> But the standard does mention it in
> 10.1p4; notice the word distinct. Can subobjects have the same address
> and be distinct?

Obviously non virtual inheritance implies distinct
subobject. Where are they required to have different
addresses ?

--

Valentin Bonnard


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/30
Raw View

Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote in article
<37C81909.5D@wanadoo.fr>...
>
> > the language requires the Base1 and Base2
> > subobjects to have different addresses,
>
> Does it ?

You're right, of course.  Base class subobjects may have zero size and
occupy no bytes at all.

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]




[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/26
Raw View
On 25 Aug 1999 15:54:49 GMT, Wil Evers <bouncer@dev.null> wrote:

>Even in this small example, a call to Derived::clone() via a pointer to a
>Base1 cannot have the exact same implementation as a call to
>Derived::clone() via a pointer to a Base2.  In one of these cases, an extra
>pointer conversion is required.

It is possible for "Derived * Derived::clone() const" to have the same
exact implementation as "Base * Base::clone() const".  If the compiler
stores derived objects this way: base part first, derived-only part
next, morederived-only part next, then &Base == &Derived == &MoreDerived.

IOW,
   MoreDerived * m=new MoreDerived;
   Derived * d=m;
   Base * b=d;
   assert((void*)b==(void*)m); // will be true
   assert((void*)d==(void*)m); // will be true
   assert((void*)b==(void*)d); // will be true

Hence, Base::clone() and Derived::clone() and MoreDerived::clone() all
have the same exact implementation.


>The question is: why would the example conversion above be harder to
>implement than any other conversion?

If the implementors uses the license that you can store the derived-only
part first then the base-only part, then they will have to use the
thunks.  I don't know of a compiler that does this though.

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/25
Raw View
blargg <postmast.root.admi.gov@iname.com> wrote in article
<user-2308992013540001@aus-as3-091.io.com>...

> Read it again. "implementation cost". That means it costs the
> compiler writers to implement this feature in their compilers.
> The practicalities of the compiler are an issue when designing
> a real language for multiple parties to implement. Hell, if that
> weren't the case, we'd all be using super compilers that
> optimized programs like you wouldn't believe!

Right.  However, we should keep in mind that the currently allowed set of
conversions for covariant return types already requires some hidden magic.


 struct Base1 {
  /* ... */
  virtual Base1 *clone() const;
 } ;

 struct Base2 {
  /* ... */
  virtual Base2 *clone() const;
 };

 struct Derived : Base1, Base2 {
  /* ... */
  Derived *clone() const;
 };

Even in this small example, a call to Derived::clone() via a pointer to a
Base1 cannot have the exact same implementation as a call to
Derived::clone() via a pointer to a Base2.  In one of these cases, an extra
pointer conversion is required.

The question is: why would the example conversion above be harder to
implement than any other conversion?

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]




[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: postmast.root.admi.gov@iname.com (blargg)
Date: 1999/08/24
Raw View
In article <slrn7s384c.mqg.sbnaran@localhost.localdomain>,
sbnaran@uiuc.edu wrote:

> On 23 Aug 99 09:36:04 GMT, Wil Evers <bouncer@dev.null> wrote:
>
> >...but after re-reading section 13.7 of D&E, I now realize that I was
> >wrong.  Here's the relevant quote:
> >
> >       We decided not to relax the rules to allow technically
> >       feasible conversions such as a D to an accessible base
> >       B, a D to an X for which D has a conversion, int* to
> >       void*, double to int, etc.  We felt that the benefits
> >       from allowing such conversions through overriding would
> >       not outweigh the implementation cost and the potential
> >       for confusing users.
>
> From this thread I've learned that there is no runtime cost.
>
> There is a compile time cost because the compiler must construct a
> more elaborate virtual table.  But given that the compiler has to
> already do complicated stuff like parse template template parameters,
> member templates, namespaces, etc, I think the overhead is relatively
> small.

Read it again. "implementation cost". That means it costs the compiler
writers to implement this feature in their compilers. The practicalities
of the compiler are an issue when designing a real language for multiple
parties to implement. Hell, if that weren't the case, we'd all be using
super compilers that optimized programs like you wouldn't believe!
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/23
Raw View
In article <01beeadd$21d37210$d03240c3@twwie>, I wrote...

> So far, I don't think anyone has come up with a sound technical
> reason why the rules for covariant return types couldn't be
> relaxed.  I'm beginning to suspect that not considering any
> other conversions was simply an oversight; I'd sure like some
> historical input from the people who were actually there when
> that decision was made.

...but after re-reading section 13.7 of D&E, I now realize that I was
wrong.  Here's the relevant quote:

 We decided not to relax the rules to allow technically
 feasible conversions such as a D to an accessible base
 B, a D to an X for which D has a conversion, int* to
 void*, double to int, etc.  We felt that the benefits
 from allowing such conversions through overriding would
 not outweigh the implementation cost and the potential
 for confusing users.

I apologize to 'the people who were actually there': they were aware of the
possibility, knew that it could be implemented, but decided not to allow
it.

Of course, at the time (1992) C++ did not have member templates, so the
example that triggered this thread (smart pointer conversions) could not
have surfaced.

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]



---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/23
Raw View
On 23 Aug 99 09:36:04 GMT, Wil Evers <bouncer@dev.null> wrote:

>...but after re-reading section 13.7 of D&E, I now realize that I was
>wrong.  Here's the relevant quote:
>
> We decided not to relax the rules to allow technically
> feasible conversions such as a D to an accessible base
> B, a D to an X for which D has a conversion, int* to
> void*, double to int, etc.  We felt that the benefits
> from allowing such conversions through overriding would
> not outweigh the implementation cost and the potential
> for confusing users.



Author: scorp@btinternet.com (Dave Harris)
Date: 1999/08/18
Raw View
sbnaran@uiuc.edu (Siemel B. Naran) wrote:
> > struct B {
> >  virtual long calc();
> >  // ...
> > };
> >
> > struct D : B {
> >  virtual char calc();
> >   // ILLEGAL, but why?
> > };
>
> I know one way to implement this. I'll show that there is a small
> penalty on every virtual function for turning this feature on, as well
> as a moderate penalty on virtual functions that use the feature.

Just implement it as:

 struct D : B {
  virtual long calc();
  char calc__char() { return static_cast<char>( calc() ); }
 };

Callers of B::calc() are unaffected and calc__char() can be inlined, so
this costs nothing for functions not using the feature and very little for
those which do.

Another more general approach is to rewrite it as:

 struct D : B {
  virtual long calc() { return calc__char(); }
  virtual char calc__char();
 };

This handles the cases where the round-trip conversion char->long->char is
inefficient or impossible - callers of D::calc() are rewritten to call
D::calc__char() directly instead. It costs an extra virtual function call
for callers of B::calc(), but nothing more.

  Dave Harris, Nottingham, UK | "Weave a circle round him thrice,
      brangdon@cix.co.uk      |   And close your eyes with holy dread,
                              |  For he on honey dew hath fed
 http://www.bhresearch.co.uk/ |   And drunk the milk of Paradise."


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/18
Raw View
Siemel B. Naran <sbnaran@uiuc.edu> wrote in article
<slrn7rhkf4.700.sbnaran@localhost.localdomain>...

> On 16 Aug 99 17:53:03 GMT, Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:
>
> Suppose that we have
>    // A.hh
>    struct A { virtual ~A(); virtual A calc() const; };
> and
>    // B.hh
>    struct B : A { B calc() const; };
>
> How will your method handle this case?
>    // f.hh
>    #include "A.hh"
>    inline void f(const A * a) { a->calc(); }
>       // perhaps call B::calc(), convert to A
>       // perhaps call A::calc()
>

As already demonstrated, the implementation of relaxed rules for covariant
return types turns out to be quite simple.  For the example above, the
compiler would generate the following code:

struct A {  //  same as above
  virtual ~A();
  virtual A calc() const ;
};

struct B : A {
 /* final */ A calc() const { return __calc_returning_B(); }
  // handles required conversion

 virtual B __calc_returning__B() const;
  // implemented as the original B::calc() const
};

What we do here is allocate a separate vtbl slot for the case where calc()
returns a B.  We use the original slot to forward the call to the new slot
and subsequently perform the required conversion.

Given a pointer a to B, the following call -

 const B *bp = /* ... */;
 B result = bp->calc();

would be translated to:

 const B *bp = /* ... */;
 B result = bp->__calc_returning_B();

Since the compiler has access to both B's and A's definitions, it can see
that B uses a covariant return type for calc(), so it can statically
determine it should use the vtbl slot for __calc_returning_B().

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]



[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/18
Raw View
Siemel B. Naran <sbnaran@uiuc.edu> wrote in article
<slrn7rj1cu.a9l.sbnaran@localhost.localdomain>...
>
> On 17 Aug 99 11:53:48 GMT, Francis Glassborow
>
> >Before pursuing this any further could some proponent of it post an
> >example where it is desirable in a well designed hierarchy to solve a
> >real problem.
>
> A good question.  Bill posted one usage -- covariant returns on smart
> pointers.  Yes, we need more reasons.  It's going to be hard to come
> up with other examples, too, because most one-arg constructors are
> explicit and the covariant mechanism requires the one-arg constructors
> to be implicit.

Assuming you were referring to me when you wrote 'Bill' :-)

I guess a judgement on the importance of the example I posted depends on
two things:

(1) The perceived value of covariant return types.  As demonstrated in this
thread, for every use of a covariant return type, there is always a
somewhat less convenient workaround.  In other words, C++ has covariant
return types because they're convenient, not because we can't live without
them.  The score was settled the day they were introduced into the
language.

(2) The perceived value of smart pointers.  If you think smart pointer
idioms are important to the future of C++, then you don't want the language
rules to needlessly treat them as second class citizens.  Please note that
we have a precedent in this area: the introduction of member templates
removed most, but not all, of the obstacles for the use of smart pointers.

So far, I don't think anyone has come up with a sound technical reason why
the rules for covariant return types couldn't be relaxed.  I'm beginning to
suspect that not considering any other conversions was simply an oversight;
I'd sure like some historical input from the people who were actually there
when that decision was made.

Personally, I hope the day will come when the use of built-in pointers will
be as old-fashioned as the use of a built-in arrays is now: you're not
supposed to use them - unless you're implementing a way to hide them from
others.

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]



[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/18
Raw View
Francis Glassborow <francis@robinton.demon.co.uk> wrote in article
<L7SnivA0ZIu3Ew2g@robinton.demon.co.uk>...

> If there is any penalty on non-users of this proposal it will need an
> immense benefit to justify it.

I don't think there is any penalty for non-users; please see the reply to
Siemel I just posted.

> I cannot see any possibility of such a benefit.
>
> Before pursuing this any further could some proponent of it post an
> example where it is desirable in a well designed hierarchy to solve a
> real problem.

template <class B>
class controlled_ptr {
public :
 /* ... */
 controlled_ptr(const controlled_ptr& rhs);

 template <class D>
 controlled_ptr(const controlled_ptr<D>& rhs);
  // converts ptr-to-derived to ptr-to-base

 B *operator->() const;
 B &operator*() const;
 /* ...etc... */
};

class Base { /* ... */ };
class Derived : public Base { /* ... */ };

class BaseFactory {
public :
 /* ... */
 virtual controlled_ptr<Base> create() const;
};

class DerivedFactory : public BaseFactory {
public :
 /* ... */
 virtual controlled_ptr<Derived> create() const;
  // again: illegal, by why?
};

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]



[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/16
Raw View
On 12 Aug 1999 15:19:53 GMT, Wil Evers <bouncer@dev.null> wrote:

>Thinking about this, it occurred to me that the requirements for covariant
>return types are perhaps too restrictive.  Instead of just the 'Derived *
>-> Base *' conversion, I believe a wider set of conversions (including
>user-defined ones) could be allowed.  For example, I fail to see why the
>following code should be forbidden:
>
> struct B {
>  virtual long calc();
>  // ...
> };
>
> struct D : B {
>  virtual char calc();
>   // ILLEGAL, but why?
> };

I know one way to implement this.  I'll show that there is a small
penalty on every virtual function for turning this feature on, as well as
a moderate penalty on virtual functions that use the feature.


I. CURRENT C++ WITHOUT THE PROPOSED FEATURE

First, let's recall the structure of the virtual table.  It is basically
an array of C style pointers to functions, kind of like this:
   struct function { ??? (*mf)(???); }; // pointer to some C style function
   typedef function[] virtual-table; // array of such functions
'mf' is a pointer to the member function.

Suppose we have this:
   void f(const A * a) { a->calc(); }
Here's what we do:
   1a. get virtual pointer of 'a', which points to a virtual-table
   1b. obtain the address of the function typeid(*a)::calc()
These are the usual steps.  If the return type of the function in 1b is
always the same as A::calc(), then we just call the function, and we're
done.  But C++ allows covariant return types on pointers.  Now if the
implementation stores derived objects this way: base part, followed by
derived-only part, followed by morederived-only part -- then there's
nothing extra to do because &derived is equal to &base.  But if the
compiler stores the derived-only part first then the base part, then to
convert a derived* to base* it must add an offset.  I don't know of any
compiler that stores objects this way.  So here's the final step:
   1c. call the member function in 1b
In summary, here's how the compiler rewrites the above code:
   void f(const A * a) { ((a->vptr)[1])(); }



II. MINIMIZING THE PENALTY ON USERS WHO DON'T USE THE PROPOSED FEATURE

But with your proposed change, we'll have to add a step 1d that converts
the return type of the member function to the type expected.  If
   struct A     { virtual long  calc() const; virtual ~A(); };
   struct B : A { virtual int   calc() const; };
then we may have to convert an int to a long.

To avoid penalizing virtual functions don't use the feature, we store a
bool that indicates whether we need a conversion at all.  Hence we cut
out step 1c and use instead:
   2a. obtain the bool the indicates whether we need to do a conversion
Followed by:
   3A . if bool==false
   3Aa. call the member function in 1b
Each entry in the virtual-table is now as follows:
   struct function { ??? (*mf)(???); bool convert; }
   typedef function[] virtual-table;

We see that there is a small penalty on every virtual function for
turning this feature on.  The virtual table is slightly larger, and we
have to check the boolean flag to see if we must do a conversion.

To complete the picture, what if we need a conversion?
   3B . if bool==true
   3Ba. call the member function in 1b
   3Bb. convert the return type
Implementing 3Bb is a challenge.



III. DEEP HIERARCHIES: MANY CONVERSIONS POSSIBLE

Conversions are more difficult than appears at first sight.  What if
we have a hierarchy three or more levels deep?
   struct A     { virtual long  calc() const; virtual ~A(); };
   struct B : A { virtual int   calc() const; };
   struct C : B { virtual short calc() const; };
   struct D : C { virtual char  calc() const; };
Here are some illustrative functions.  Note the comments:
  void f1(const A * a) { a->calc(); } // convert char->long
  void f2(const B * b) { b->calc(); } // convert char->int
  int main() { const D d; f1(&d); f2(&d); }

Observe that we don't do char->short->int->long.  Thus we have the
following rule: if C derives from B derives from A, then the return
type of C::calc() must be convertible directly to the return type
of B::calc() as well as A::calc().

Anyway, we see that the proposed feature complicates the way we call any
virtual function, because now the compiler must specify to the virtual
function mechanism what type we expect.  Before, the compiler could say
   void f(const A * a) { ((a->vptr)[1])(); } // quoted from above
Now the compiler must say
   void f(const A * a) { ((a->vptr)[0][1])(); }
                                // ^^^ convert to typeof(A::calc())

Put another way, the virtual-table of class D in current C++ is simply:
   struct { void (*dtor)(D *); char (*calc)(const D *); } vtable:D;
In C++ with the proposed feature, the virtual-table of class D would be
an array of pointers to real virtual-tables:
   struct { vtable * 0; vtable * 1; vtable * 2; vtable * 3 } vtable:D;
And these four real virtual-tables are:
   struct { void (*dtor)(D *); char  (*calc)(const D *); } vtable:D:A; // 0
   struct { void (*dtor)(D *); short (*calc)(const D *); } vtable:D:B; // 1
   struct { void (*dtor)(D *); int   (*calc)(const D *); } vtable:D:C; // 2
   struct { void (*dtor)(D *); long  (*calc)(const D *); } vtable:D:D; // 3
The index numbering scheme (the most base class has index 0, the next
base class has index 1, etc) lets us quickly figure out which real
virtual-table to use.  It seems easy to extend this numbering scheme
to multiple inheritance.

The member vtable:D:D::calc is the original function D::calc.
The other functions are compiler generated.  For example,
vtable:D:B::calc points to the following function:
   int {
      call D::calc and optionally inline the function;
      call int::int(const char&) to convert char->int;
   }

Rule 2: the constructor int::int(const char&) must be implicit or there
must be a conversion char::operator int() const.
Rule 3: if both the conversion constructor and operator conversion exist,
we get a compile time ambiguity error.

Notice that we've got rid off the boolean flag.  But at a cost: we
now have to dereference two virtual-table pointers.  So there is still
a small penalty on every virtual function for turning this feature on.
Doubtless there some who would consider this penalty large.

Also, we end up with many virtual tables, or code bloat.  Unless the
compiler does the optimizations of
   [X] using one real virtual table whenever possible, as in
          struct A     { virtual void f() const; };
          struct B : A { virtual void f() const; };
            // vtable:B:A and vtable:B:B are the same!
   [X] eliminating unused virtual tables.



IV. LINK TIME OPTIMIZATIONS POSSIBLE

At link time, the compiler knows the full class hierarchies.  If it
sees that a certain hierarchy does not use the proposed feature, then
it can use the traditional form of calling virtual functions.  As
this optimization means rewriting the already compiled code at link
time, it is not likely to happen, at least for a while.  Besides, the
optimization is not possible for shared libraries.

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Valentin Bonnard <Bonnard.V@wanadoo.fr>
Date: 1999/08/16
Raw View
Siemel B. Naran wrote:
>
> On 12 Aug 1999 15:19:53 GMT, Wil Evers <bouncer@dev.null> wrote:
>
> >Thinking about this, it occurred to me that the requirements for covariant
> >return types are perhaps too restrictive.  Instead of just the 'Derived *
> >-> Base *' conversion, I believe a wider set of conversions (including
> >user-defined ones) could be allowed.  For example, I fail to see why the
> >following code should be forbidden:
> >
> >       struct B {
> >               virtual long calc();
> >               // ...
> >       };
> >
> >       struct D : B {
> >               virtual char calc();
> >                       //      ILLEGAL, but why?
> >       };
>
> I know one way to implement this.
[ snip long discution of a complicated way to do that ]

That's too much for me, I just can't follow you.

My prefered method is the one which keeps all existing
calling conventions without changes. (That one I understand.)

It's probably too easy for you (not interresting enough).

But why would anyone use the simple method when it's
simplier to use the more complicated one ? (The Shadoks,
badly translated)

--

Valentin Bonnard
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/17
Raw View
On 16 Aug 99 17:53:03 GMT, Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote:

>My prefered method is the one which keeps all existing
>calling conventions without changes. (That one I understand.)

How does this method work?  If there is a simpler method, then of course
we should seriously consider it.

Suppose that we have
   // A.hh
   struct A { virtual ~A(); virtual A calc() const; };
and
   // B.hh
   struct B : A { B calc() const; };

How will your method handle this case?
   // f.hh
   #include "A.hh"
   inline void f(const A * a) { a->calc(); }
      // perhaps call B::calc(), convert to A
      // perhaps call A::calc()

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: Francis Glassborow <francis@robinton.demon.co.uk>
Date: 1999/08/17
Raw View
In article <slrn7rb6e4.m9.sbnaran@localhost.localdomain>, Siemel B.
Naran <sbnaran@uiuc.edu> writes
>I know one way to implement this.  I'll show that there is a small
>penalty on every virtual function for turning this feature on, as well as
>a moderate penalty on virtual functions that use the feature.
>
If there is any penalty on non-users of this proposal it will need an
immense benefit to justify it.  I cannot see any possibility of such a
benefit.

Before pursuing this any further could some proponent of it post an
example where it is desirable in a well designed hierarchy to solve a
real problem.


Francis Glassborow      Journal Editor, Association of C & C++ Users
64 Southfield Rd
Oxford OX4 1PA          +44(0)1865 246490
All opinions are mine and do not represent those of any organisation
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: sbnaran@uiuc.edu (Siemel B. Naran)
Date: 1999/08/17
Raw View
On 17 Aug 99 11:53:48 GMT, Francis Glassborow

>Before pursuing this any further could some proponent of it post an
>example where it is desirable in a well designed hierarchy to solve a
>real problem.

A good question.  Bill posted one usage -- covariant returns on smart
pointers.  Yes, we need more reasons.  It's going to be hard to come
up with other examples, too, because most one-arg constructors are
explicit and the covariant mechanism requires the one-arg constructors
to be implicit.

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/17
Raw View
Edward Jason Riedy <ejr@lotus.CS.Berkeley.EDU> wrote in article
<7p2t88$fgd$1@nntprelay.berkeley.edu>...

[ Example user-defined evil_ptr snipped ]

> The compiler knows the semantics of pointers.  The compiler does
> not know the semantics user objects.  Trying to restrict the
> template trickery would lead to a horrible, nigh unimplementable
> mess.

Sure, any badly written user-defined conversion is dangerous, especially
when invoked implicitly.  However, I fail to see why such a 'killer
conversion' would be more dangerous in the context of a covariant return
than it already is in the situations where it is currently allowed.

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/13
Raw View
Valentin Bonnard <Bonnard.V@wanadoo.fr> wrote in article
<37B3019B.2C6D@wanadoo.fr>...

> Wil Evers wrote:
>
> > For example, I fail to see why the following code should be forbidden:
> >
> >         struct B {
> >                 virtual long calc();
> >                 // ...
> >         };
> >
> >         struct D : B {
> >                 virtual char calc();
> >                         //      ILLEGAL, but why?
> >         };
>
> You can rewrite it as:
>
>         struct B {
>                 virtual long calc();
>         };
>
>         struct D : B {
>                 virtual long calc() { return calc_char(); }
>                 virtual char calc_char();
>         };
>
>
> Note that this is a general technique.

Of course, but following that line of reasoning, we might as well get rid
of covariant return types completely :-)

  class B {
    virtual B *B_clone() const { return new B(*this); }
  public :
    // ...
    B *clone() const { return B_clone(); }
  };

  class D : public B {
    /* final */ virtual B *B_clone() const { return D_clone(); }
    virtual D *D_clone() const { return new D(*this); }
  public :
    D *clone() const { return D_clone(); }
  };

Presumably, covariant return types were introduced into C++ to save us from
having to write stuff like this, that is, as a convencience - which is
precisely why I don't understand the restrictions placed on them.  It seems
to me that a covariant return of some smart pointer type is just as useful
as a covariant return of a built-in pointer type.

> > does anyone see a technical reason why relaxing the rules this way
> > should not be allowed?
>
> The technical reason is that arbitrary code should be
> executed to do the convertion has opposed to adding
> an offset.
>
> OTOH, when properly implemented, the offset is only added
> when it is needed. The same would go for generalised
> covariant return types.
>
> In particular, see the above rewrite.

I agree.  I believe the examples above nicely demonstrate that
language-level support for a wider set of conversions would be trivial to
implement.

> What are the requirements BTW ? That there is a valid
> unambiguous conversion sequence from DerFuncReturnType to
> BaseFuncReturnType ?

I'm no language lawyer, but yes, that sounds like what I'm after.

Thanks,

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: ejr@lotus.CS.Berkeley.EDU (Edward Jason Riedy)
Date: 1999/08/14
Raw View
And Wil Evers writes:
 -
 -  struct B {
 -   virtual smart_ptr<B> clone() const;
 -   // ...
 -  };
 -
 -  struct D : B {
 -   virtual smart_ptr<D> clone() const;
 -    // ERROR: different return type
 -   // ...
 -  };

template <typename C>
struct smart_ptr : public evil_ptr <C, Some_Bool_Trait<C> >
{
  // Appropriate stuff, not overriding evil_ptr...
};

template <typename C, bool>
struct evil_ptr
{
  C* my_ptr;
  C* operator->() { return my_ptr; }
  // constructor, etc.
};

// specialization
template <typename C, false>
struct evil_ptr
{
  // no operator->, or worse, one that returns garbage...
};

The compiler knows the semantics of pointers.  The compiler does
not know the semantics user objects.  Trying to restrict the
template trickery would lead to a horrible, nigh unimplementable
mess.  And I wouldn't appreciate a library writer's forcing me
always to use that library writer's smart_ptr, anyways.

Jason


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: "Wil Evers" <bouncer@dev.null>
Date: 1999/08/12
Raw View
Hi,

I have a suggestion for the next round of C++ standardization :-).

As we all know, using member templates, it is possible to define a smart
pointer template that supports a conversion from smart-pointer-to-derived
to smart-pointer-to-base:

 template <class Base>
 class smart_ptr {
 public :
  //...
  template <class Derived>
  smart_ptr(smart_ptr<Derived>& rhs);
 };

With this conversion, the smart_ptr template almost always works as
expected, and it can be used to encourage a programming style in which most
of the burden of managing polymorphic, heap-allocated objects is left to
the compiler.  However, smart pointers can't be used as covariant return
types:

 struct B {
  virtual smart_ptr<B> clone() const;
  // ...
 };

 struct D : B {
  virtual smart_ptr<D> clone() const;
   // ERROR: different return type
  // ...
 };

This is a shame, because it forces us to use built-in pointers, which is
error-prone, or to fall back on one of the workarounds from the days when
covariant return types were not available, which are all ugly.

Thinking about this, it occurred to me that the requirements for covariant
return types are perhaps too restrictive.  Instead of just the 'Derived *
-> Base *' conversion, I believe a wider set of conversions (including
user-defined ones) could be allowed.  For example, I fail to see why the
following code should be forbidden:

 struct B {
  virtual long calc();
  // ...
 };

 struct D : B {
  virtual char calc();
   // ILLEGAL, but why?
 };

To me, this code makes perfect sense.  The values D::calc() returns are a
subset of the values B::calc() returns, so there will be no surprises for
users accessing a D object with a B *.  On the other hand, users who access
that some object with a D * can take advantage of knowing that D::calc()'s
return value is in the char range.

Obviously, some extra code must be generated to make sure that the
character value returned by D::calc() is converted to a long when it's
called through B's interface, but in principle, this is not that different
from the code that is generated for a covariant return of a derived pointer
as a pointer to a virtual base class.  In the case of a covariant smart
pointer return, the compiler would simply generate some code that invokes
the user-defined conversion constructor.

Any takers?  In particular, does anyone see a technical reason why relaxing
the rules this way should not be allowed?

Thanks,

- Wil

Wil Evers, DOOSYS IT Consultants, Maarssen, Holland
[Wil underscore Evers at doosys dot com]



[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: Valentin Bonnard <Bonnard.V@wanadoo.fr>
Date: 1999/08/12
Raw View
Wil Evers wrote:

> I have a suggestion for the next round of C++ standardization :-).

Interresting

> Thinking about this, it occurred to me that the requirements for covariant
> return types are perhaps too restrictive.  Instead of just the 'Derived *
> -> Base *' conversion, I believe a wider set of conversions (including
> user-defined ones) could be allowed.  For example, I fail to see why the
> following code should be forbidden:
>
>         struct B {
>                 virtual long calc();
>                 // ...
>         };
>
>         struct D : B {
>                 virtual char calc();
>                         //      ILLEGAL, but why?
>         };

You can rewrite it as:

        struct B {
                virtual long calc();
        };

        struct D : B {
                virtual long calc() { return calc_char(); }
                virtual char calc_char();
        };

        struct D2 : D {
                virtual long calc() { return calc_bool(); }
                virtual char calc_char() { return calc_bool(); }
                virtual char calc_bool();
        };

Note that this is a general technique.

> To me, this code makes perfect sense.

No: a char is a letter; a long is an integer.

> Any takers?  In particular, does anyone see a technical reason why relaxing
> the rules this way should not be allowed?

The technical reason is that arbitrary code should be
executed to do the convertion has opposed to adding
an offset.

OTOH, when properly implemented, the offset is only added
when it is needed. The same would go for generalised
covariant return types.

In particular, see the above rewrite.

What are the requirements BTW ? That there is a valid
unambiguous conversion sequence from DerFuncReturnType to
BaseFuncReturnType ?

--

Valentin Bonnard


[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]