C++ Question

Why does virtual assignment behave differently than other virtual functions of the same signature?

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?

Answer

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.