1

I was wondering how should I (or can I? does it make any sense?) overload the assignment operator when working with inheritance and upcasting? Let's say we have Base class and Derived class (inherited from Base). If i have something like:

/// supose we have one overloaded assignment operator in Base like Base& operator=(const Base&) and 
///one in Derived like Derived& operator=(const Derived&)...
Base* a, *b;
Derived c,d;

a = &c;
b = &d;

*a = *b  /// this will call the function in Base

If that calls the Base function, why should I overload "=" again in Derived? Is overloading assignment operator in Derived necessary only for working directly with objects, not upcasting (pointers) ?

  • 1
    Did you mean `Base* a, *b;`? – Zoso Apr 09 '21 at 15:45
  • @fabian That would not be a regular `=` operator isn't it since it would mandate something like `Derived::operator=(const B&)` kind of thing, wouldn't it? – Zoso Apr 09 '21 at 15:48
  • @Zoso yes, edited now. –  Apr 09 '21 at 15:55
  • 1
    https://stackoverflow.com/questions/669818/virtual-assignment-operator-c – armagedescu Apr 09 '21 at 16:01
  • I've added the edit so that my answer contains an example where Derived both does and does not manage its own dynamic resource. – sweenish Apr 09 '21 at 16:43
  • In C++, inheritance, assignment, and copy-construction usually require special care (possibly extraordinary effort) to allow them to behave as desired. I find it often easier with inheritance just to disallow assignment and copy-construction altogether. But if they are needed, it can be quite a bit of code to support those operations in some fashion. – Eljay Apr 09 '21 at 17:21

2 Answers2

0

Does the output of this example help clarify your question? You could always override the operator= in the derived class as follows:

#include <cstdio>

struct Base{
virtual ~Base() = default;

virtual void operator=(const Base&) {
    std::printf("Base::=\n");
}

};

struct Derived: public Base {
    void operator=(const Derived&) {
        std::printf("Derived::=\n");
    }
    void operator=(const Base&) override{
        std::printf("Derived::= Base\n");
    }
};


int main() {
    Base* a, *b;
    Derived c,d;

    a = &c;
    b = &d;

    *a = *b; //Dispatches the call to the derived class =

    Base base;
    Derived derived;
    derived = base; //Usual case now after operator=(const Base&) in Derived

    c = d; //Usual case

    Base base1, base2;
    base1 = base2; //Usual case
    a = &base1;
    b = &base2;

    *a = *b; //Usual case
}

Output:

Derived::= Base
Derived::= Base
Derived::=
Base::=
Base::=
Zoso
  • 3,273
  • 1
  • 16
  • 27
0

Here's some code that hopefully helps you out.

Derived Does Not Own A Dynamic Resource
The Base class holds a dynamic resource, so we are required to follow the Rule of 3 (should be 5, but kept it at 3 for brevity). I did so by utilizing the copy/swap idiom.

I then derive Derived from Base. It does not hold a dynamic resource, so I follow the Rule of 0 and don't provide a custom copy constructor, destructor, assignment operator, move constructor, or move assignment.

You can see from the output that the Base portion of the Derived objects are able to deep-copy themsleves just fine, and the Derived-only portion gets by just fine with shallow copy. The final output leaks memory, but I chose to do that to demonstrate an actual overwrite using a pointer to Base.

#include <iostream>

class Base {
 private:
  int* m = nullptr;

 public:
  Base() = default;
  Base(int v) : m(new int(v)) {}
  Base(const Base& other) : m(new int(*(other.m))) {}
  virtual ~Base() {
    delete m;
    m = nullptr;
  }

  Base& operator=(Base other) {
    swap(*this, other);

    return *this;
  }

  friend void swap(Base& lhs, Base& rhs) {
    using std::swap;

    swap(lhs.m, rhs.m);
  }

  virtual void print() const {
    std::cout << "Address: " << m << "\nValue: " << *m << '\n';
  }
};

class Derived : public Base {
 private:
  double x = 0.0;

 public:
  Derived() = default;
  Derived(double v) : Base(), x(v) {}
  Derived(int i, double v) : Base(i), x(v) {}

  void print() const override {
    std::cout << "Address: " << &x << "\nValue: " << x << '\n';
    Base::print();
  }
};

int main() {
  std::cout << "A\n";
  Base* a = new Derived(5, 3.14);
  a->print();

  std::cout << "\nB\n";
  Derived b = *(dynamic_cast<Derived*>(a));  // Copy ctor
  b.print();

  std::cout << "\nC\n";
  Derived c;
  c = b;
  c.print();

  std::cout << "\nReplace A (This leaks)\n";
  a = new Derived(7, 9.81);
  a->print();
}

Output:

A
Address: 0x21712d0
Value: 3.14
Address: 0x21712e0
Value: 5

B
Address: 0x7ffdd62964c8
Value: 3.14
Address: 0x2171300
Value: 5

C
Address: 0x7ffdd62964b0
Value: 3.14
Address: 0x2171320
Value: 5

Replace A (This leaks)
Address: 0x2171350
Value: 9.81
Address: 0x2171360
Value: 7

Derived Owns A Dynamic Resource
Now, Derived has a dynamic of its own to manage. So I follow the Rule of 3 and provide a copy constructor, destructor, and assignment operator overload. You'll notice that the assignment operator looks identical to the Base version; this is intentional.

It's because I'm using the copy/swap idiom. So in the swap() function for Derived, I add a step where it swaps the Base portion, then swaps the Derived portion. I do this by invoking the Base swap() function through the dynamic cast.

And we can again observe that all objects have their own memory for each dynamically allocated piece.

#include <iostream>

class Base {
 private:
  int* m = nullptr;

 public:
  Base() = default;
  Base(int v) : m(new int(v)) {}
  Base(const Base& other) : m(new int(*(other.m))) {}
  virtual ~Base() {
    delete m;
    m = nullptr;
  }

  Base& operator=(Base other) {
    swap(*this, other);

    return *this;
  }

  friend void swap(Base& lhs, Base& rhs) {
    using std::swap;

    swap(lhs.m, rhs.m);
  }

  virtual void print() const {
    std::cout << "Address: " << m << "\nValue: " << *m << '\n';
  }
};

class Derived : public Base {
 private:
  double* x = nullptr;

 public:
  Derived() = default;
  Derived(double v) : Base(), x(new double(v)) {}
  Derived(int i, double v) : Base(i), x(new double(v)) {}
  Derived(const Derived& other) : Base(other), x(new double(*(other.x))) {}
  ~Derived() {
    delete x;
    x = nullptr;
  }

  Derived& operator=(Derived other) {
    swap(*this, other);

    return *this;
  }

  friend void swap(Derived& lhs, Derived& rhs) {
    using std::swap;

    swap(dynamic_cast<Base&>(lhs), dynamic_cast<Base&>(rhs));
    swap(lhs.x, rhs.x);
  }

  void print() const override {
    std::cout << "Address: " << &x << "\nValue: " << *x << '\n';
    Base::print();
  }
};

int main() {
  std::cout << "A\n";
  Base* a = new Derived(5, 3.14);
  a->print();

  std::cout << "\nB\n";
  Derived b = *(dynamic_cast<Derived*>(a));  // Copy ctor
  b.print();

  std::cout << "\nC\n";
  Derived c;
  c = b;
  c.print();

  std::cout << "\nReplace A (This leaks)\n";
  a = new Derived(7, 9.81);
  a->print();
}

Output:

A
Address: 0x14812d0
Value: 3.14
Address: 0x14812e0
Value: 5

B
Address: 0x7fffe89e8d68
Value: 3.14
Address: 0x1481320
Value: 5

C
Address: 0x7fffe89e8d50
Value: 3.14
Address: 0x1481360
Value: 5

Replace A (This leaks)
Address: 0x14813b0
Value: 9.81
Address: 0x14813c0
Value: 7
sweenish
  • 4,793
  • 3
  • 12
  • 23