Why does virtual assignment behave differently than other virtual functions of the same signature?
-
13-09-2019 - |
Question
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?
Solution
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.
OTHER TIPS
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.
There is no user-provided 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.
If you fail to provide an appropriate operator=
(i.e. correct return and argument types), the default operator=
is provided by the compiler which overloads 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.
The reason being there is compiler provided default assignment operator=
.
Which is called in the scenario a = b
and as we know default internally calls base assignment operator.
More explanation about virtual assignment can be found at : https://stackoverflow.com/a/26906275/3235055