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

5

22

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?

Treadwell answered 9/6, 2009 at 10:20 Comment(0)
O
14

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.

Outspoken answered 9/6, 2009 at 10:58 Comment(3)
There's no guessing involved here. The rules are very strict.Timberhead
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.Resume
a = static_cast<Base &>(b); would be a way to avoid C-style casts (which carry a risk of accidentally doing a reinterpret cast)Chromaticity
T
5

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.

Timberhead answered 9/6, 2009 at 11:18 Comment(0)
S
4

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.

Schell answered 9/6, 2009 at 10:58 Comment(0)
C
3

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.

Choreodrama answered 9/6, 2009 at 11:13 Comment(0)
D
2

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://mcmap.net/q/258768/-virtual-assignment-operator-c

Danaides answered 13/11, 2014 at 10:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.