TITLE: Problems with "placement" form of operator =

PROBLEM: rmartin@rcmcon.com (Robert Martin), 21 Jan 94

There have been several postings about the potential for using
placement syntax in assignment operators as follows:

X& X::operator=(const X& x)
{
  if (this != &x)
  {
    this->X::~X();	// destruct
    new (this) X(x);	// copy construct in place.
  }
  return *this;
}

While this model seems compelling, there appear to be several
significant dangers.  Consider the following program:

----------------------------------------------------------------------------
#include <iostream.h>
#include <stddef.h>

void* operator new (size_t, void* p) {return p;}

class B
{
  public:
    B(int i) : id(i) {}
    B(const B& b) : id(b.id) {}
    B& operator=(const B&);
    virtual ~B() {cout << this << ":" << *this << "is destroyed" << endl;}
    void Identify() {cout << this << ": points at " << *this << endl;}
    virtual ostream& Print(ostream&) const;
    int GetID() const {return id;}
  friend ostream& operator<<(ostream& o, const B& b) {return b.Print(o);}
  private:
    int id;
};

B& B::operator=(const B& b)
{
    if (this != &b)
    {
	this->B::~B();
	new (this) B(b);
    }
    return *this;
}

ostream& B::Print(ostream& o) const
{
    o << "B(" << id << ")";
    return o;
}

class D : public B
{
  public:
    D(int i, int j) : B(i), id2(j) {}
    D(const D& d) : B(d), id2(d.id2) {}
    D& operator=(const D&);
    virtual ~D() {cout << this << ":" << *this << "is destroyed" << endl;}
    virtual ostream& Print(ostream&) const;

  private:
    int id2;
};

D& D::operator=(const D& d)
{
    if (this != &d)
    {
	this->D::~D();
	new (this) D(d);
    }
    return *this;
}

ostream& D::Print(ostream& o) const
{
    o << "D(" << GetID() << ',' << id2 << ")";
    return o;
}

main()
{
    B b(1);
    D d(2,2);
    b.Identify();
    d.Identify();

    B& b2 = d;
    b2 = b;		// change type of d to B!!!!!
    b.Identify();
    d.Identify();
}

----------------------------------------------------------------------

gnu 2.5.7 produces the following output (which I have annotated.)

0xbffffdc0: points at B(1)
0xbffffdb4: points at D(2,2)    <---------- 'd'
0xbffffdb4:B(2)is destroyed     <---------- Destructor not virtually deployed.
0xbffffdc0: points at B(1)
0xbffffdb4: points at B(1)      <---------- 'd'
0xbffffdb4:D(1,2)is destroyed   <---------- Destructor not virtually deployed.
0xbffffdb4:B(1)is destroyed
0xbffffdc0:B(1)is destroyed

Although there are several interesting anomalies to consider, the one
at issue is the fact that the D object contained by the variable 'd'
has had its type changed to B; i.e. virtual deployment through 'd' now
only accesses functions in B.

Clearly this is very dangerous since the static type of 'd' is still
D, and other parts of the program will treat it as such.  While it is
interesting that you can use this form of assignment to change the
type of an object, it is also pretty scary.

Other features of interest in the above output involve the handling of
destructors.  Even though the destructors for B and D are virtual, the
destructor call from the assignment operator was not virtually
deployed.  This is not quite what I expected, and I could be convinced
that it was a bug in gcc, however it does appear consistent with the
ARM's restriction that destructors can only be explicitly called by
qualified name.  Perhaps the intent is that virtual deployment of
destructors only takes place at 'deletion' and not simple
'destruction'.

Another case of "aberant destruction" occurs when 'd' goes out of
scope.  At this time, the object in d is "virtually" only a B, and yet
D::~D() is being called.  So again, it appears that virtual deployment
of destructors is reserved for 'delete'.

-------------------------------------------

The fact that one can use this kind of assignment operator to change
the vtbl of an object is interesting.  Consider what might happen if
this idiom were employed in MI.  

                                 A
                                /
                           B   C
                            \ /
                             D 

D d;
A& a = d; // legal upcast
A x;
a = x; // Does this change D::C to A?  

This seems likely, not just to replace C's vtbl with one for A, but to
corrupt it as well.  At least in some implementations, offsets for MI
are stored in the vtbl.  These offsets will be destroyed when the vtbl
is replaced; yeilding undefined behaviors.
