views:

510

answers:

4

While playing with implementing a virtual assignment operator I have ended with a funny behavior. It is not a compiler glitch, since g++ 4.1, 4.3 and VS 2005 share the same behavior.

Basically, the virtual operator= behaves differently than any other virtual function with respect to the code that is actually being executed.

struct Base {
   virtual Base& f( Base const & ) {
      std::cout << "Base::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Base::operator=(Base const &)" << std::endl;
      return *this;
   }
};
struct Derived : public Base {
   virtual Base& f( Base const & ) {
      std::cout << "Derived::f(Base const &)" << std::endl;
      return *this;
   }
   virtual Base& operator=( Base const & ) {
      std::cout << "Derived::operator=( Base const & )" << std::endl;
      return *this;
   }
};
int main() {
   Derived a, b;

   a.f( b ); // [0] outputs: Derived::f(Base const &) (expected result)
   a = b;    // [1] outputs: Base::operator=(Base const &)

   Base & ba = a;
   Base & bb = b;
   ba = bb;  // [2] outputs: Derived::operator=(Base const &)

   Derived & da = a;
   Derived & db = b;
   da = db;  // [3] outputs: Base::operator=(Base const &)

   ba = da;  // [4] outputs: Derived::operator=(Base const &)
   da = ba;  // [5] outputs: Derived::operator=(Base const &)
}

The effect is that the virtual operator= has a different behavior than any other virtual function with the same signature ([0] compared to [1]), by calling the Base version of the operator when called through real Derived objects ([1]) or Derived references ([3]) while it does perform as a regular virtual function when called through Base references ([2]), or when either the lvalue or rvalue are Base references and the other a Derived reference ([4],[5]).

Is there any sensible explanation to this odd behavior?

+4  A: 

Here's how it goes:

If I change [1] to

a = *((Base*)&b);

then things work the way you expect. There's an automatically generated assignment operator in Derived that looks like this:

Derived& operator=(Derived const & that) {
    Base::operator=(that);
    // rewrite all Derived members by using their assignment operator, for example
    foo = that.foo;
    bar = that.bar;
    return *this;
}

In your example compilers have enough info to guess that a and b are of type Derived and so they choose to use the automatically generated operator above that calls yours. That's how you got [1]. My pointer casting forces compilers to do it your way, because I tell compiler to "forget" that b is of type Derived and so it uses Base.

Other results can be explained the same way.

Jasiu
There's no guessing involved here. The rules are very strict.
MSalters
Thanks, The real answer (as posted by already three people) is that the compiler generated operator= for the Derived class implicitly calls the Base::operator=. I am marking this as 'accepted answer' as it was the first one.
David Rodríguez - dribeas
+3  A: 

IMO, there is no assignment operator defined for Derived class. Hence, compiler synthesizes one and internally base class assignment operator is called from that synthesized assignment operator for Derived class.

virtual Base& operator=( Base const & ) //is not assignment operator for Derived

Hence, a = b; // [1] outputs: Base::operator=(Base const &)

In Derived class, the Base class assignment operator has been overridden and hence, the overridden method gets an entry in virtual table of the Derived class. When the method is invoked via reference or pointers then Derived class overridden method gets called due to VTable entry resolution at run time.

ba = bb;  // [2] outputs: Derived::operator=(Base const &)

==>internally ==> (Object->VTable[Assignement operator]) Get the entry for assignment operator in VTable of the class to which the object belongs and invoke the method.

aJ
+2  A: 

If you fail to provide an appropriate operator= (i.e. correct return and argument types), the default operator= is provided by the compiler which overrides any user-defined one. In your case it will call the Base::operator= (Base const& ) before copying the Derived members.

Check this link for details on operator= being made virtual.

Abhay
+1  A: 

There are three operator= in this case:

Base::operator=(Base const&) // virtual
Derived::operator=(Base const&) // virtual
Derived::operator=(Derived const&) // Compiler generated, calls Base::operator=(Base const&) directly

This explains why it looks like Base::operator=(Base const&) is called "virtually" in case [1]. It's called from the compiler-generated version. The same applies to case [3]. In case 2, the right-hand side argument 'bb' has type Base&, so Derived::operator=(Derived&) cannot be called.

MSalters