CS253: Software Development with C++

Fall 2021

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 {
    float *data;
  public:
    HeapFloat(float f) : data(new float(f)) { }
    ~HeapFloat() { delete data; }
    float get() const { return *data; }
};

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

Try Copy Constructor

class HeapFloat {
    float *data;
  public:
    HeapFloat(float f) : data(new float(f)) { }
    ~HeapFloat() { delete data; }
    float get() const { return *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 {
    float *data;
  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; }
};

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

Try Assignment Operator

class HeapFloat {
    float *data;
  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; }
};

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 {
    float *data;
  public:
    HeapFloat(float f) : data(new float(f)) { }
    HeapFloat(const HeapFloat &rhs) : data(new float(*rhs.data)) { }
    HeapFloat &operator=(const HeapFloat &rhs) {
        delete data;
        data = new float(*rhs.data);
        return *this;
    }
    ~HeapFloat() { delete data; }
    float get() const { return *data; }
};

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

Self-assignment

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

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

Explanation

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

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

What happens inside a = a?

Fix

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

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

Explanation

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

This Sounds Silly

Not very DRY

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

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

DRY

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

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

The copy ctor calls operator=; as complex objects often do. Initialize data = nullptr so the initial delete data is ok.