CS253: Software Development with C++

Fall 2020

Self-assignment

Show Lecture.Self-assignment as a slide show.

CS253 Self-assignment

Intro

This illustrates the danger of self-assignment.

That is, when an object containing pointers or other handles to external resources is assigned to itself, you’ve got to be careful in operator=, or you might destroy your data before you copy it to yourself.

Copy Constructor General Outline

When a class contains a handle to external data, the copy constructor generally follows this pattern:

  1. Copy the resource from the other object to this object.

That is, don’t just copy the handle (pointer, file descriptor, etc.). Instead you have to make an actual copy of the resource (copy the memory from new, create a new temporary file and copy the contents, etc.).

Of course, this is a copy constructor, not an assignment operator, so there was no previous value. We are constructing.

Assignment Operator General Outline

When a class contains a handle to external data, the assignment operator (operator=) generally follows this pattern:

  1. Get rid of the old external data (delete memory, close network socket, delete temporary file, unlock semaphore, etc.).
  2. Copy the resource from the other object to this object.

Code

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2);
cout << a.get();
1.2

Try Copy Constructor

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2), b(a);
cout << a.get() << ' ' << b.get();
free(): double free detected in tcache 2
SIGABRT: Aborted

Fixed Copy Constructor

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2), b(a);
cout << a.get() << ' ' << b.get();
1.2 1.2

Try Assignment Operator

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2), b(3.4);
a = b;
cout << a.get() << ' ' << b.get();
free(): double free detected in tcache 2
SIGABRT: Aborted

Fix Assignment Operator

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    void operator=(const HeapFloat &rhs) {
        delete data;
        data = new float(*rhs.data);
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2), b(3.4);
a = b;
cout << a.get() << ' ' << b.get();
3.4 3.4

Self-assignment

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    void operator=(const HeapFloat &rhs) {
        delete data;
        data = new float(*rhs.data);
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
7.30357e-42

Explanation

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    void operator=(const HeapFloat &rhs) {
        delete data;
        data = new float(*rhs.data);
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
5.77195e-42

What happens inside a = a?

Fix

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    void operator=(const HeapFloat &rhs) {
        if (this != &rhs) { // avoid self-assignment
            delete data;
            data = new float(*rhs.data);
        }
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2

Victory!

Explanation

void operator=(const HeapFloat &rhs) {
    if (this != &rhs) {
        delete data;
        data = new float(*rhs.data);
    }
}

This Sounds Silly

Not very DRY

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    void operator=(const HeapFloat &rhs) {
        if (this != &rhs) { // avoid self-assignment
            delete data;
            data = new float(*rhs.data);
        }
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data;
};

HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2

new float(*rhs.data) appears twice—WET!

DRY

class HeapFloat {
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) { *this = rhs; }
    void operator=(const HeapFloat &rhs) {
        if (this != &rhs) { // avoid self-assignment
            delete data;
            data = new float(*rhs.data);
        }
    }
    ~HeapFloat() { delete[] data; }
    float get() const { return *data; }
    float *data = nullptr;
};

HeapFloat a(1.2);
a = a;
cout << a.get() << '\n';
1.2

Now DRY. The copy ctor invokes the assignment operator; complex objects often do this. Initialize data = nullptr so the initial delete data is ok.