Topic: an optimization I claim is illegal


Author: Dick Menninger <Dick.Menninger@daytonoh.attgis.com>
Date: 1995/11/28
Raw View
> ==========Fergus Henderson, 11/21/95==========

> If you changed the line

>        D1 d1 (7);

> to

>      D1 &d1 = *(D1 *) malloc(sizeof(D1) + sizeof(D2));
>      new (&d1) D1 (7);

> then the storage would be definitely large enough to hold either
> a D1 or a D2 and would be correctly aligned to hold any object,
> so this problem wouldn't arise.

Although it would be overkill as far as allocating
enough storage, how this insures correct alignment
less clear, at least to me.  Is there a constraint on
where in an object image the "this" pointer points?
Or is it that the low address of a memory block
is supposed to be passed when this form of new
is used (my memory is fuzzy on this)?  I had the
impression that the "this" pointer could have an
environment specific (or even variable between classes)
relationship to the object instance.  Certainly some
hardware architectures would favor a centrist "this"
pointer for some sizes of objects (small signed offsets
imbedded in instructions dereferencing "this").

Good Day
Dick
Dick.Menninger@DaytonOH.ATTGIS.COM
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: fjh@munta.cs.mu.OZ.AU (Fergus Henderson)
Date: 1995/11/28
Raw View
Dick Menninger <Dick.Menninger@daytonoh.attgis.com> writes:

>> ==========Fergus Henderson, 11/21/95==========
>
>> If you changed the line
>>      D1 d1 (7);
>> to
>>      D1 &d1 = *(D1 *) malloc(sizeof(D1) + sizeof(D2));
>>      new (&d1) D1 (7);
>
>> then the storage would be definitely large enough to hold either
>> a D1 or a D2 and would be correctly aligned to hold any object,
>> so this problem wouldn't arise.
>
>Although it would be overkill as far as allocating
>enough storage, how this insures correct alignment
>less clear, at least to me.  Is there a constraint on
>where in an object image the "this" pointer points?

The draft says that the value of the `this' pointer is the address of
the object (9.4.2 [class.this]), that an object is composed of a
contiguous sequence of bytes (1.6 [intro.object]), and that each byte
of storage has a unique address (1.5 [intro.memory]). However, as
far as I can tell it doesn't define what it means by the address of an object.
So I suppose it would be reasonable for an implementation to define
the address of an object of a certain type to be the address of the
N-th byte of storage for that object for some N != 0.

>I had the
>impression that the "this" pointer could have an
>environment specific (or even variable between classes)
>relationship to the object instance.  Certainly some
>hardware architectures would favor a centrist "this"
>pointer for some sizes of objects (small signed offsets
>imbedded in instructions dereferencing "this").

My reading of the draft confirms your impression.

Nevertheless, the code does ensure correct alignment.
The reason is that there is an implicit conversion to `void *'
in the call to placement new.  This implicit conversion is defined
to return the address of the start of the storage of the object,
i.e. the address of the first byte (4.10/2 [conv.ptr]).

--
Fergus Henderson              WWW: http://www.cs.mu.oz.au/~fjh
fjh@cs.mu.oz.au               PGP: finger fjh@128.250.37.3
I will have little or no net access from Nov 30 until Dec 25.
Please email me a copy of any follow-ups.
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: clamage@Eng.Sun.COM (Steve Clamage)
Date: 1995/11/28
Raw View
Dick Menninger <Dick.Menninger@daytonoh.attgis.com> writes:

>> ==========Fergus Henderson, 11/21/95==========
>
>> If you changed the line
>
>>        D1 d1 (7);
>
>> to
>
>>      D1 &d1 = *(D1 *) malloc(sizeof(D1) + sizeof(D2));
>>      new (&d1) D1 (7);
>
>> then the storage would be definitely large enough to hold either
>> a D1 or a D2 and would be correctly aligned to hold any object,
>> so this problem wouldn't arise.

>Although it would be overkill as far as allocating
>enough storage, how this insures correct alignment
>less clear, at least to me.

malloc is required to return storage suitably aligned for any
object of that size. In order for the technique above not to
work, it would have to be the case that some smaller object could
always require stricter alignment than any larger object. Since
a larger object might contain the smaller as a subobject, I think
this would always lead to a contradiction. It seems to me you would
have to work hard to make such a perverse implementation even if
it *could* somehow conform to the standard.

>Is there a constraint on
>where in an object image the "this" pointer points?

No.

>Or is it that the low address of a memory block
>is supposed to be passed when this form of new
>is used (my memory is fuzzy on this)?  I had the
>impression that the "this" pointer could have an
>environment specific (or even variable between classes)
>relationship to the object instance.

Some implementations have "this" pointing to a location other
than the starting address of the object. For example,
there is some advantage in having the vtable pointer at a
negative offset, although there are disadvantages too.

If "this" does not point at the starting address, the compiler
has to take care to do pointer adjustments in the face of
conversion to and from void*, and with the use of placement new.

That is, the above code has some conversions to and from void*,
and the compiler would adjust the pointer value correspondingly.
--
Steve Clamage, stephen.clamage@eng.sun.com

---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: John Max Skaller <maxtal@suphys.physics.su.oz.au>
Date: 1995/11/19
Raw View
bgibbons@taligent.com (Bill Gibbons) wrote:
>In article <48gmat$i43@engnews1.eng.sun.com>, clamage@Eng.Sun.COM wrote:
>
>> The key point is whether you can do that during execution of a member
>> function on behalf of the original object.
>>
>> I think the answer is no.
>
>I beg to differ.  I believe the issue of destroying an object from within
>a member's execution has been resolved, and the answer was that it's OK:
>
>    void A::f() { delete this; }  // OK

This is unfortunate, because it may force _unloading_ of cached
addresses before calling a member function. For example on the 486:

; function g() { f(); }

     LES BX, object_address ; load ES:BX with 'this' pointer
     CALL f                 ; call member function
     RET

This machine code may LOOK OK, but it ISNT. Why? Because an
interrupt may come flying in after the call to f and before the
return. The machine registers are pushed onto the stack
in the interrupt. After the interrupt has done its job
it pops the register back. WOOPS!!! A segment register
gets loaded with an invalid address. A general protection
fault ensues.

This is NOT a hyposthetical example, it happens right now
(under Borland 4.5), and it happened to a friend of mine
who had to debug someone else's code.

ES must be loaded with a valid address
at all times, 0 will do. But in general there's no way to tell
when deallocating memory whether a register contains a reference
to that memory (other than an address comparison on the segment
register in the deallocator ??).

So IF the 'this' pointer is to be cached across calls to
unknown functions, the compiler has to assume the object
is not deleted.

The argument against changing the type is similar -- IF
the virtual table pointer is to be cached in a register,
it can't be allowed to be changed until the (lexical)
end of the block of code for which it was loaded.
Otherwise the wrong offset may be used in looking up
a subsequent virtual function call or pointer
offset adjustment.

[Perhaps a test will fix this:

    LES BX, vtable1
    CALL f           ; any function
    MOV AX,ES
    CMP vtable1,AX
    JEQ label
    LES BX, vtable1 ; reload vtable pointer :-(((((
label:
]

Does the compiler _really_ have to generate this horrid code?

Note that on the 486 loading segment registers is VERY EXPENSIVE.

>Here's a somewhat more robust version of the example:
>
>  virtual void Derived::f() {
>    if (--n == 0) {
>      void *mem = dynamic_cast<void*>(this);  // start of complete object
>      this->~Derived();
>      new (mem) Base;
>    }
>  }
>
>I agree that this is pretty ugly, but I think the current working paper
>makes it well-formed.

A great pity if this is the case, a major optimisation opportunity
appears to be lost on the most common processor.

[Perhaps I'm wrong though: it's hard to be certain there isn't
a clever way around this problem]


--
John Max Skaller               voice: 61-2-566-2189
81 Glebe Point Rd              fax:   61-2-660-0850
GLEBE NSW 2037                 email: maxtal@suphys.physics.oz.au
AUSTRALIA                      email: skaller@maxtal.com.au




---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: John Max Skaller <maxtal@suphys.physics.su.oz.au>
Date: 1995/11/19
Raw View
clamage@Eng.Sun.COM (Steve Clamage) wrote:
>
>In article h00@engnews1.eng.sun.com, clamage@Eng.Sun.COM (Steve Clamage) writes:
>
>The question is whether an object can change type during execution of a
>member function other than during construction or destruction.
>
>I think the answer is no.

I _hope_ the answer is no -- both to allow the
optimisation, and to make programs easier to read.

--
John Max Skaller               voice: 61-2-566-2189
81 Glebe Point Rd              fax:   61-2-660-0850
GLEBE NSW 2037                 email: maxtal@suphys.physics.oz.au
AUSTRALIA                      email: skaller@maxtal.com.au




---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: clamage@taumet.eng.sun.com (Steve Clamage)
Date: 1995/11/20
Raw View
James Kanze (kanze@gabi-soft.fr) wrote:
>Alex Martelli (martelli@cadlab.it) wrote:
>
>: In a debate over on comp.compilers, there was some discussion of
>: whether it would be legal for a compiler to optimize the following
>: snippet of code...:
>
>: class P {
>: public: virtual void f();
>:     // etc
>: };
>
>: // with a P* p
>:     for(i=0;i<1000;i++)
>:  p->f();
>
>: by hoisting out of the loop the virtual lookup for f()'s address.
>

[code removed, including Derived class deriving from Base class]

>
>There's one obvious problem.  The address of Base in Derived is not
>guaranteed to be the same as the address of Derived.


Tim Hollebeek also wrote:

[snip again]

> I think it's pretty clear the answer is no:
>
> [class.dtor] #11:
>
> "...  Once a destructor is invoked for an object, the object no longer
>  exists; ..."
>
> Now, it is legal to use a (T*) as a (void*) even after the T's life
> has ended, however:
>
> [basic.life] #7:
>
> "If, after the lifetime of an object has ended and while the storage
> which the object occupied still exists, a new object is created at the
> storage location which the original object occupied, a pointer that
> pointed to the original object will automatically refer to the new
> object and, once the lifetime of the new object has started, can be
> used to manipulate the new object, if:
>
> [...]
>
> ... the new object is of THE SAME TYPE as the original object ...
>
> [...]
>

What is meant by the SAME TYPE in the piece of the Draft quoted ?  Would
the following code be legal:

-- start here --

#include <iostream.h>
#include <defalloc.h>

struct Base
{
    int i;
    Base (int val) : i(val) {};
    Base () : i(i) {};
    virtual ~Base () {};
    virtual void f () { cout << "Base" << endl; };
};

struct D2;

struct D1 : public Base
{
    D1 (int val) : Base(val) {};
 D1 () : Base () {};
    virtual ~D1 () {};
    virtual void f ();
};

struct D2 : public Base
{
    D2 (int val) : Base(val) {};
 D2 () : Base () {};
    virtual ~D2 () {};
    virtual void f ();
};

void D1::f () { cout << "D1" << endl; new (this) D2 (); }
void D2::f () { cout << "D2" << endl; new (this) D1 (); }

int main (void)
{
    D1 d1 (7);
    Base *pb = &d1;
    for (int i = 0; i < 5; i++)
        pb->f();
 return 0;
}

-- end here --

The output on g++ is 'D1 D2 D1 D2 D1'. The type as seen by the loop
in main does not change, the layout of the types D1 and D2 are the
same, they have the same parents and the same function. Why would it
be illegal in this case ?  I do not ask this for purely theoretical
reason: I do use this (in a somewhat more involved ways) in a design
of networked pointers.


[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]






Author: fjh@munta.cs.mu.OZ.AU (Fergus Henderson)
Date: 1995/11/21
Raw View
martelli@cadlab.it (Alex Martelli) writes:

>In a debate over on comp.compilers, there was some discussion of
>whether it would be legal for a compiler to optimize the following
>snippet of code...:
>
>class P {
>public: virtual void f();
>    // etc
>};
>
>// with a P* p
>    for(i=0;i<1000;i++)
> p->f();
>
>by hoisting out of the loop the virtual lookup for f()'s address.

Yes, I saw that debate.  I believe that the optimization is legal
according to the current draft standard.  See section 3.8 [basic.life],
"Object Lifetime".  The committee was quite deliberate in its intent to
make sure that this optimizations and other similar optimizations were
legal.

>#include <iostream>
>
>class Base {
>public: virtual void f() {cout<<"Base ";}
>};
>class Derived: public Base {
>    int countdown;
>public: Derived(int n=2):countdown(n) {}
>    virtual void f();
>};
>int main()
>{
>    Base *p=new Derived;
>    for(int i=0;i<4;i++)
>      p->f();
>    cout<<endl;
>    return 0;
>}
>
>and, separately compiled perhaps,
>
>#include <iostream>
>#include <new>
>// above-seen declarations of Base and Derived, then:
>    virtual void Derived::f() {
> cout<<"Derived ";
> if(--n==0) {
>     this->~Derived();
>     new(this) Base;
> }
>    }
>
>Isn't this a strictly conformant, well-defined program,

No.  According to 3.8, it has undefined behaviour.  It violates 3.8/6.
The pointer `p' is dereferenced after the lifetime of the object
it points to has ended (since the storage has been reused).

|   3.8  Object Lifetime                                      [basic.life]
| 5 A  program  may  end the lifetime of any object by reusing the storage
|   which the object occupies or by explicitly calling the destructor  for
|   an  object  of  a  class  type  with a non-trivial destructor.
[...]
| 6 After the lifetime of an object has ended and while the storage  which
|   the  object  occupied still exists, any pointer to the original object
|   can be used but only in limited ways.  Such a pointer still points  to
|   valid  storage and using the pointer as a pointer to the storage where
|   the object was located, as if the pointer were of type void*, is well-
|   defined.   However,  using the pointer to refer to the original object
|   is no longer valid.  In particular, such a pointer cannot be  derefer-
|   enced; for a non-POD class type T, a pointer of type T* that points to
|   the  original  object  cannot  be  the  operand   of   a   static_cast
|   (_expr.static.cast_) (except when the conversion is to void* or char*)
|   and cannot be the operand of a dynamic_cast (_expr.dynamic.cast_);  if
|   T  is  a class with a non-trivial destructor, such a pointer cannot be
|   used as the operand of a delete-expression.  [Example:
|           struct B {
|                   virtual void f();
|                   void mutate();
|                   virtual ~B();
|           };
|
|           struct D1 : B { void f(); };
|           struct D2 : B { void f(); };
|           void B::mutate() {
|                   new (this) D2;  // reuses storage - ends the lifetime of '*th
| is'
|                   f();            // undefined behavior
|                   ... = this;     // ok, 'this' points to valid memory
|           }
|
|           void g() {
|                   void* p = malloc(sizeof(D1) + sizeof(D2));
|                   B* pb = new (p) D1;
|                   pb->mutate();
|                   &pb;            // ok: pb points to valid memory
|                   void* q = pb;   // ok: pb points to valid memory
|                   pb->f();        // undefined behavior, lifetime of *pb has en
| ded
|           }
|    --end example]

--
Fergus Henderson              WWW: http://www.cs.mu.oz.au/~fjh
fjh@cs.mu.oz.au               PGP: finger fjh@128.250.37.3

[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]






Author: Dick Menninger <Dick.Menninger@daytonoh.attgis.com>
Date: 1995/11/22
Raw View
> ==========Somebody said, 11/20/95==========


> What is meant by the SAME TYPE in the piece of the Draft
quoted ?  Would
> the following code be legal:
>
> -- start here --

  #include <iostream.h>
  #include <defalloc.h>

   struct Base
   {
       int i;
       Base (int val) : i(val) {};
       Base () : i(i) {};
       virtual ~Base () {};
       virtual void f () { cout << "Base" << endl; };
   };

   struct D2;

   struct D1 : public Base
   {
       D1 (int val) : Base(val) {};
        D1 () : Base () {};
       virtual ~D1 () {};
       virtual void f ();
   };

   struct D2 : public Base
   {
       D2 (int val) : Base(val) {};
        D2 () : Base () {};
       virtual ~D2 () {};
       virtual void f ();
   };

   void D1::f () { cout << "D1" << endl; new (this) D2 (); }
   void D2::f () { cout << "D2" << endl; new (this) D1 (); }

   int main (void)
   {
       D1 d1 (7);
       Base *pb = &d1;
       for (int i = 0; i < 5; i++)
           pb->f();
        return 0;
   }

> -- end here --
>
> The output on g++ is 'D1 D2 D1 D2 D1'. The type as seen by the loop
> in main does not change, the layout of the types D1 and D2 are the
> same, they have the same parents and the same function. Why would it
> be illegal in this case ?  I do not ask this for purely theoretical
> reason: I do use this (in a somewhat more involved ways) in a design
> of networked pointers.

Wow.  This code raises several questions.  As coded,
the "object" goes through several ctors as it changes
"personality" without ever a dtor called. Is that legal?
Is that reasonable?  That seems to make this a different
problem as you overwrite a living instance through which
the function being executed was called instead of
first destroying it and then creating a new one over
the ashes of the old (or am I confusing two threads).
Is there any requirement that the actual underlying
images be the same size? Can't the compiler could have
other info hidden in the image that could even affect
the offset of this in the memory block used and still
be a conforming compiler?  Couldn't that info even be
stuff that could go insane if you did this (list linkage
for various debugging support)?

Good Day
Dick
Dick.Menninger@DaytonOH.ATTGIS.COM
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: fjh@munta.cs.mu.OZ.AU (Fergus Henderson)
Date: 1995/11/22
Raw View
Dick Menninger <Dick.Menninger@daytonoh.attgis.com> writes:

>> ==========Somebody said, 11/20/95==========
>
>> What is meant by the SAME TYPE in the piece of the Draft quoted ?
>> Would the following code be legal:

No, the code has undefined behaviour.
The types `D1' and `D2' are not the same.
(The code also has undefined behaviour for other reasons - see below.)

>   struct Base
>   {
>       ...
>       virtual void f () { cout << "Base" << endl; };
>   };
>
>   struct D1 : public Base ...
>   struct D2 : public Base ...
>
>   void D1::f () { cout << "D1" << endl; new (this) D2 (); }
>   void D2::f () { cout << "D2" << endl; new (this) D1 (); }
>
>   int main (void)
>   {
>       D1 d1 (7);
>       Base *pb = &d1;
>       for (int i = 0; i < 5; i++)
>           pb->f();
>        return 0;
>   }
>>
>> The output on g++ is 'D1 D2 D1 D2 D1'. The type as seen by the loop
>> in main does not change, the layout of the types D1 and D2 are the
>> same, they have the same parents and the same function. Why would it
>> be illegal in this case ?  I do not ask this for purely theoretical
>> reason: I do use this (in a somewhat more involved ways) in a design
>> of networked pointers.
>
>Wow.  This code raises several questions.  As coded,
>the "object" goes through several ctors as it changes
>"personality" without ever a dtor called. Is that legal?

Yes, you are allowed to construct a new object over the top
of an existing object without first calling the destructor
for that existing object.

>Is that reasonable?

A more difficult question.  I certainly wouldn't recommend
that sort of coding style in any but extraordinary circumstances.

>Is there any requirement that the actual underlying
>images be the same size?

No, I'm pretty sure there isn't.  So therefore the above code has
undefined behaviour - it attempts to construct an object of type `D2'
using the storage for a `D1', and there is no guarantee that this
storage will be large enough or correctly aligned.

If you changed the line

       D1 d1 (7);

to

 D1 &d1 = *(D1 *) malloc(sizeof(D1) + sizeof(D2));
 new (&d1) D1 (7);

then the storage would be definitely large enough to hold either
a D1 or a D2 and would be correctly aligned to hold any object,
so this problem wouldn't arise.  But the code would still be undefined
behaviour by 3.8/7, since D1 and D2 are not the same type.

--
Fergus Henderson              WWW: http://www.cs.mu.oz.au/~fjh
fjh@cs.mu.oz.au               PGP: finger fjh@128.250.37.3
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: fjh@munta.cs.mu.OZ.AU (Fergus Henderson)
Date: 1995/11/24
Raw View
chase@centerline.com (David Chase) writes:

>[...] I think it is a mistake to argue the point in terms
>of 486 assembly language.

Although I don't agree with John Skaller's conclusion,
I do think he is using a legitimate line of argument.
x86 CPUs are pretty common; if the C++ standard makes certain
optimizations impossible on those CPUs and others like them,
and this has a significant impact on the performance of C++ code,
then this would be a bad thing, and it would be reasonable
to suggest that perhaps the C++ standard should be changed.

As it turns out, I don't think the C++ standard prevents the
optimization that Skaller was talking about (caching the value of the
`this' pointer in a segment/offset register pair over a member function
call).

>Furthermore, the situation described
>
>> This machine code may LOOK OK, but it ISNT. Why? Because an
>> interrupt may come flying in after the call to f and before the
>> return. The machine registers are pushed onto the stack
>> in the interrupt. After the interrupt has done its job
>> it pops the register back. WOOPS!!! A segment register
>> gets loaded with an invalid address. A general protection
>> fault ensues.
>
>sounds odd to me -- to me, this sounds like quality of OS
>implementation, not standards stuff (why would an interrupt handler
>fail to put the registers back?  Is this some DOS thing?).

I think you may possibly have missed Skaller's point here - the problem
is not that the interrupt handler doesn't try to restore the original
register contents.  The problem is that when it does attempt to do so,
it gets a GPF, because the segment registers contained values which
have since become invalid (because the segment that they point to has
since been deallocated), and so merely pushing them and popping them
can cause a GPF.

However, I agree that a better OS and/or C++ implementation could avoid
the problem.  For example, interrupt routines could set a flag before
restoring segment registers; if a GPF happened while this flag was set,
the GPF handler could just set the segment register to zero and then
continue.

The existence of this technique implies that the C++ standard does not
prevent caching the value of the `this' pointer in a segment/offset
register pair over a member function call.  It is conceivable that this
optimization might not be possible with certain (low-quality) operating
systems, but the problem would not unique be to C++ - such OSs would
also in general prevent caching *any* pointer in a segment/offset
register pair over *any* function call.  As a result, I don't think
Skaller's argument holds up.  I think the C++ standard is right to
allow code such as `delete this;'.

--
Fergus Henderson              WWW: http://www.cs.mu.oz.au/~fjh
fjh@cs.mu.oz.au               PGP: finger fjh@128.250.37.3
---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: clamage@Eng.Sun.COM (Steve Clamage)
Date: 1995/11/16
Raw View
In article 816557310@cadlab, martelli@cadlab.it (Alex Martelli) writes:
>
>In a debate over on comp.compilers, there was some discussion of
>whether it would be legal for a compiler to optimize the following
>snippet of code...:

>class P {
>public: virtual void f();
>    // etc
>};

>// with a P* p
>    for(i=0;i<1000;i++)
> p->f();

>by hoisting out of the loop the virtual lookup for f()'s address.

> ...

>Here's the counterexample I have in mind:
>
>#include <iostream>
>
>class Base {
>public: virtual void f() {cout<<"Base ";}
>};
>class Derived: public Base {
>    int countdown;
>public: Derived(int n=2):countdown(n) {}
>    virtual void f();
>};
>int main()
>{
>    Base *p=new Derived;
>    for(int i=0;i<4;i++)
>      p->f();
>    cout<<endl;
>    return 0;
>}
>
>and, separately compiled perhaps,
>
>#include <iostream>
>#include <new>
>// above-seen declarations of Base and Derived, then:
>    virtual void Derived::f() {
> cout<<"Derived ";
> if(--n==0) {
>     this->~Derived();
>     new(this) Base;
> }
>    }
>
>Isn't this a strictly conformant, well-defined program, ...


I am not at all convinced that constructing a new object on top of "this"
is well-defined. Recent changes in the DWP assure us that you can
construct a new object of the SAME type on top of an existing object if
you destroy the old one first, but even this case is not discussed in
terms of doing it from inside a member function.

Here is a different example which is well-defined. (I haven't compiled
it, so please excuse any typos.)

class Base { public: virtual void f(); };
class Derived : public Base { public: virtual void f(); };

extern Base* array[4] = { new Base, new Derived, new Base, new Derived };
Base* p = 0;

int main()
{
 p = Base[0];
 for(int i=0; i<4; i++)
  p->f();
}

void X::f() // X is Base or Derived
{
 static int i = 0;
 ... do something unique for each class, then
 i = (i+1)%4;
 p = array[i]; // change the value of p
}

The difference here is that p is global, not local, so its value cannot
be assumed to be unchanged. You didn't say whether that was an assumption
in the original case. Similarly, you could make p a local variable, but
pass it by reference to f. Again, it cannot be assumed to be unchanged,
which may not fit the original problem definition.

If we assume that the VALUE of p does not change, the question is whether
the object it points to can change type (except during construction and
destruction). I think the answer is no, in which case the optimization is valid.
---
Steve Clamage, stephen.clamage@eng.sun.com



---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: clamage@Eng.Sun.COM (Steve Clamage)
Date: 1995/11/17
Raw View
In article h00@engnews1.eng.sun.com, clamage@Eng.Sun.COM (Steve Clamage) writes:
>
>
>If we assume that the VALUE of p does not change, the question is whether
>the object it points to can change type (except during construction and
>destruction). I think the answer is no, in which case the optimization is valid.

I meant to say:

The question is whether an object can change type during execution of a
member function other than during construction or destruction.

You can allocate raw storage, construct an object in it, destroy the
object, and construct a new object of different type at the same location.
The key point is whether you can do that during execution of a member
function on behalf of the original object.

I think the answer is no.
---
Steve Clamage, stephen.clamage@eng.sun.com



---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: bgibbons@taligent.com (Bill Gibbons)
Date: 1995/11/17
Raw View
In article <48gmat$i43@engnews1.eng.sun.com>, clamage@Eng.Sun.COM wrote:

> You can allocate raw storage, construct an object in it, destroy the
> object, and construct a new object of different type at the same location.
> The key point is whether you can do that during execution of a member
> function on behalf of the original object.
>
> I think the answer is no.

I beg to differ.  I believe the issue of destroying an object from within
a member's execution has been resolved, and the answer was that it's OK:

    void A::f() { delete this; }  // OK

The catch is that the member function may not make any use of the object
after it's been destroyed.  Use of the raw memory is OK if it hasn't been
deallocated.

Here's a somewhat more robust version of the example:

  virtual void Derived::f() {
    if (--n == 0) {
      void *mem = dynamic_cast<void*>(this);  // start of complete object
      this->~Derived();
      new (mem) Base;
    }
  }

I agree that this is pretty ugly, but I think the current working paper
makes it well-formed.

Since non-array operator new is passed the *exact* size of the object being
allocated, the address point of the complete object is guaranteed to be the
same value as was returned by operator new when the object was allocated.
So by using dynamic_cast to get the start of the complete object, you also
get the start of the allocated memory, ensuring that the placement new
will be well-formed.

Of course when the object is deleted it will be a base class object, so
if the base and derived classes overrides operator delete, the base class
operator delete will be called.  This may be somewhat surprising, but it
should be well-formed under the (yet to be written, but informally agreed
upon) rules for the interaction of operator new, operator delete,
constructors, destructors, new-expressions and delete-expressions.

--
Bill Gibbons
bgibbons@taligent.com

---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]





Author: ark@research.att.com (Andrew Koenig)
Date: 1995/11/18
Raw View
In article <martelli.816557310@cadlab> martelli@cadlab.it (Alex Martelli) writes:

>     virtual void Derived::f() {
>  cout<<"Derived ";
>  if(--n==0) {
>      this->~Derived();
>      new(this) Base;
>  }
>     }

> Isn't this a strictly conformant, well-defined program

The work on the object model is not quite complete, but the
direction it is going is towards a `no' answer here.
One reason is this:

 class Foo {
 public:
  virtual void f();
 // ...
 };

 void g() {
  Foo foo;

  foo.f(); // virtual?
  foo.f(); // virtual?
 }

It would be nice if each call to foo.f could avoid the run-time
dispatch overhead, because object foo is known to be of type Foo.
But one could imagine that Foo::f, compiled separately, changes
the type of its object to Bar (derived from Foo and known to be
the same size) and Bar::f changes it back to Foo (so the destructor
will do the right thing).  The people I've talked to on the committee
agree that such programs should be taken out and shot; therefore
they will not bless such behavior.
--
    --Andrew Koenig
      ark@research.att.com


---
[ comp.std.c++ is moderated.  Submission address: std-c++@ncar.ucar.edu.
  Contact address: std-c++-request@ncar.ucar.edu.  The moderation policy
  is summarized in http://dogbert.lbl.gov/~matt/std-c++/policy.html. ]