Topic: Does the DWP break this?


Author: Graham Hughes <graham.hughes@resnet.ucsb.edu>
Date: 1996/11/25
Raw View
-----BEGIN PGP SIGNED MESSAGE-----

In the paper `Sixteen Ways to Stack a Cat', Stroustrup describes a
supposedly portable method of nuking the vtbl of an object and replacing
it at runtime.  To wit:

Suppose you have a root class foo, that looks like this:

class foo {
    ... general nonsense ...
    class unused {};
    foo(unused) {}
};

and you want to change the virtual functions to those of the class ffoo,
which also looks like

class ffoo : public foo {
    ... general nonsense (but no data members: important) ...
    ffoo(foo::unused) {}
};

You have an object of class foo, and you want to make it an ffoo.  The
paper suggests

void bar (foo & f)
{
    foo::unused u;
    new(&f) ffoo(u);
}

using the placement new operator.

Does the DWP break this in any way?  I ask because someone has claimed
that it does, and I don't know enough to confirm or deny.

(Note: I avoid any discussion of whether it *should* break this)
- --
    Graham Hughes (graham.hughes@resnet.ucsb.edu)
alt.PGPlike-key."graham@A-abe.resnet.ucsb.edu".finger.look.examine
alt.homelike-page."http://A-abe.resnet.ucsb.edu/~graham/".search.browse.view
alt.silliness."http://www.astro.su.se/~robert/aanvvv.html".look.go.laugh

-----BEGIN PGP SIGNATURE-----
Version: 2.6.2

iQCVAwUBMpjo5SqNPSINiVE5AQGeFQQAsbuVvfYKSrbf2vgdQEADE+diY8t6xemO
KZuGY7pZdP96viPT9TtR7lNmTv41GQKwZ4vdieA8GyKiFm/L1pekEMdMAl+RjiMA
5WIYtZDLd5WL4R6XMOnd2TFO7Qx+RBySgFB5ZTVkFDYdbccSnu8Vm1FbkA9n2Wtm
wdHIx32FGco=
=Fa9d
-----END PGP SIGNATURE-----
---
[ 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         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: clamage@taumet.eng.sun.com (Steve Clamage)
Date: 1996/11/25
Raw View
In article 2f9@yuggoth.ucsb.edu, Graham Hughes <graham.hughes@resnet.ucsb.edu> writes:
>
>In the paper `Sixteen Ways to Stack a Cat', Stroustrup describes a
>supposedly portable method of nuking the vtbl of an object and replacing
>it at runtime.  To wit:
>
>Suppose you have a root class foo, that looks like this:
>
>class foo {
>    ... general nonsense ...
>    class unused {};
>    foo(unused) {}
>};
>
>and you want to change the virtual functions to those of the class ffoo,
>which also looks like
>
>class ffoo : public foo {
>    ... general nonsense (but no data members: important) ...
>    ffoo(foo::unused) {}
>};
>
>You have an object of class foo, and you want to make it an ffoo.  The
>paper suggests
>
>void bar (foo & f)
>{
>    foo::unused u;
>    new(&f) ffoo(u);
>}
>
>using the placement new operator.
>
>Does the DWP break this in any way?  I ask because someone has claimed
>that it does, and I don't know enough to confirm or deny.
>
>(Note: I avoid any discussion of whether it *should* break this)

I don't think "break" is the appropriate word. That implies a rule
change that makes formerly valid code invalid, and I don't think
that is the case here.

The DWP does not assure us that the trick above will work, but I
don't think you could find any previous language rules that
said it was valid, either. The results before and after the DWP
I would say are undefined. That is not necessarily the same
as "unportable".

The derived class adds no data, but only new functions. An implicit
assumption is that the derived class is the same size as the base
class, and the layout is also the same. We are not assured that is
the case, but as a practical matter it probably is. Another
implicit assumption, which is at least as safe, is that the
derived destructor does nothing more than call the base
destructor. (That is, the changed object, now a "derived", can be
destroyed correctly by calling the "base" destructor.)

Another subtle point: The placement-new operation will cause the
(empty) special constructors to run, which will default-initialize
the base-class data. If each data member has a no-op default
constructor, its value should remain the same. If a data member
does not have a default constructor, the code won't compile. If
a data member has a nontrivial default constructor, it will be
run without the data member having been destroyed (possibly causing
resource leaks), and the value of the data member might change.

All in all, this is not a general technique, and is not directly
supported as far as I know by any version of C++ language rules.
Nonetheless, it might well be portable in restricted circumstances.

The DWP does assure us that we can destroy an object and create
a new object of the SAME type in its place via a placement-new
operation. Example:

 void f()
 {
  T t(1, 2);
  t.~T();     // destroy T object
  new (&t) T(3, 4); // make a new T object in place
  // ... use t;
 } // t automatically destroyed at exit
---
Steve Clamage, stephen.clamage@eng.sun.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         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1996/11/26
Raw View
clamage@taumet.eng.sun.com (Steve Clamage) writes:

>Graham Hughes <graham.hughes@resnet.ucsb.edu> writes:
>>
>>In the paper `Sixteen Ways to Stack a Cat', Stroustrup describes a
>>supposedly portable method of nuking the vtbl of an object and replacing
>>it at runtime.
[...]
>>Does the DWP break this in any way?  I ask because someone has claimed
>>that it does, and I don't know enough to confirm or deny.
[...]
>The derived class adds no data, but only new functions. An implicit
>assumption is that the derived class is the same size as the base
>class, and the layout is also the same. We are not assured that is
>the case, but as a practical matter it probably is. Another
>implicit assumption, which is at least as safe, is that the
>derived destructor does nothing more than call the base
>destructor. [...]

Yes, as a practical matter these are pretty portable.

>Another subtle point: The placement-new operation will cause the
>(empty) special constructors to run, which will default-initialize
>the base-class data.

With care, a programmer can work around that one.

But there's a worse problem that you didn't mention:  a compiler can
optimize virtual function calls based on the assumption that the vtable
doesn't change.  Most existing compilers do this in at least some
circumstances: three of four C++ compilers I tried will avoid the
virtual dispatch if the variable is an object, not a reference or
pointer.  This is enough to "break" the technique, unless the
programmer is careful to write the code in such a way that they can
prevent any such optimizations.  As compilers get more mature, we can
expect that they will do a better job of optimization, and so will
perform optimizations like this in more general circumstances.
If you use this technique, there is no really safe way of ensuring that
your code will work on the next release of the compiler.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ 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         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]





Author: fjh@mundook.cs.mu.OZ.AU (Fergus Henderson)
Date: 1996/11/28
Raw View
[apologies if anyone gets this article twice]

Graham Hughes <graham.hughes@resnet.ucsb.edu> writes:

>In the paper `Sixteen Ways to Stack a Cat', Stroustrup describes a
>supposedly portable method of nuking the vtbl of an object and replacing
>it at runtime.
[...]
>Does the DWP break this in any way?  I ask because someone has claimed
>that it does, and I don't know enough to confirm or deny.

I haven't read the paper in question, but the DWP does break
this general technique.  Since it was I made this claim
(in comp.lang.java.*), I guess I owe you an explanation ;-)

>class foo {
>    ... general nonsense ...
>    class unused {};
>    foo(unused) {}
>};
[...]
>class ffoo : public foo {
>    ... general nonsense (but no data members: important) ...
>    ffoo(foo::unused) {}
>};
>
>You have an object of class foo, and you want to make it an ffoo.  The
>paper suggests
>
>void bar (foo & f)
>{
>    foo::unused u;
>    new(&f) ffoo(u);
>}

Well, there's no guarantee that sizeof(ffoo) == sizeof(foo), so
you could be in trouble already.  However, for the sake of argument
let's assume that sizeof(ffoo) == sizeof(foo); this will probably
be true for all sensible implementations, if ffoo has no data members.
So, if we assume that, then your code is OK so far.  You haven't hit
undefined behaviour -- yet.  The trouble comes when you attempt to
use the value that you passed to `bar'.

 int main() {
  foo f;
  bar(f);
  f.baz();   // undefined behaviour
 }

The reason why the DWP breaks this code is to give compilers freedom
to optimize the code better.  For example, in the code above, the
compiler could call `foo::baz' directly, or even inline the code,
rather than doing an ordinary virtual call via the vtable pointer.
For another example, if the code is something like this,

 void quux(foo& f) {
  f.baz();   // ok
  bar(f);
  f.baz();   // undefined behaviour
 }

where the compiler does not no the type of `f' at compile time,
the compiler is still allowed to load the address of f.baz into
a callee-save register, so that it will be preserved across the
call to bar(), and then use the value in the register rather
than reloading it from the vtable for the second call.

The relevent part of the DWP which prohibits such code is 3.8 [basic.life].

]   3.8  Object Lifetime                                      [basic.life]
]
] 4 A  program  may  end the lifetime of any object by reusing the storage
]   which the object occupies [...]
]
] 5 Before the lifetime of an object has started  but  after  the  storage
]   which the object will occupy has been allocated or, after the life-
]   time  of  an  object has ended and before the storage which the object
]   occupied is reused or released, any pointer that refers to the storage
]   location  where the object will be or was located may be used but only
]   in  limited  ways.   Such  a  pointer  refers  to  allocated   storage
]   (_basic.stc.dynamic.deallocation_),  and  using  the pointer as if the
]   pointer were of type void*, is well-defined.  Such a  pointer  may  be
]   dereferenced  (to  initialize a reference, for example) but converting
]   the resulting lvalue to an rvalue (_conv.lval_) results  in  undefined
]   behavior.   If  the  object will be or was of a class type with a non-
]   trivial destructor, and the pointer  is  used  as  the  operand  of  a
]   delete-expression,  the program has undefined behavior.  If the object
]   will be or was of a non-POD class  type,  the  program  has  undefined
]   behavior if:
]
]   --the  pointer  is  used  to access a non-static data member or call a
]     non-static member function of the object, or
[...]
]     [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 '*this'
]                       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 ended
]               }
]      --end example]
]
] 7 If,  after  the lifetime of an object has ended and before the storage
]   which the object occupied is reused or released, a new object is  cre-
]   ated  at  the  storage  location which the original object occupied, a
]   pointer that pointed to the  original  object  or,  a  reference  that
]   referred  to  the  original object or, the name of 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 storage for the new object exactly overlays the storage location
]     which the original object occupied, and
]
]   --the  new object is of the same type as the original object

In this case, since `ffoo' is not the same type as `foo', the behaviour
is undefined.

--
Fergus Henderson <fjh@cs.mu.oz.au>   |  "I have always known that the pursuit
WWW: <http://www.cs.mu.oz.au/~fjh>   |  of excellence is a lethal habit"
PGP: finger fjh@128.250.37.3         |     -- the last words of T. S. Garp.
---
[ 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         ]
[ FAQ:      http://reality.sgi.com/employees/austern_mti/std-c++/faq.html    ]
[ Policy:   http://reality.sgi.com/employees/austern_mti/std-c++/policy.html ]
[ Comments? mailto:std-c++-request@ncar.ucar.edu                             ]