Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Why should I suppress the default copy constructor?

From Bjarne Stroustrup's The C++ Programming Language 4th Edition:

3.3.4. Suppressing Operations

Using the default copy or move for a class in a hierarchy is typically a disaster: given only a pointer to a base, we simply don’t know what members the derived class has (§3.2.2), so we can’t know how to copy them. So, the best thing to do is usually to delete the default copy and move operations, that is, to eliminate the default definitions of those two operations:

class Shape {
    public:
        Shape(const Shape&) =delete; // no copy operations
        Shape& operator=(const Shape&) =delete;
        Shape(Shape&&) =delete; // no move operations
        Shape& operator=(Shape&&) =delete;
        ~Shape();
        // ...
};

To try to understand what he meant to say, I created the following example:

#include <iostream>

using namespace std;

class Person {
    private:
            int age;
    public:
            Person(const int& Age) : age {Age} {};
            Person(const Person& from) : age {from.Age()} { cout << "copy constructor" << endl; };
            Person& operator=(const Person& from) { cout << "copy assignment" << endl; age = from.Age(); return *this; }
            virtual void Print() { cout << age << endl; };
            void Age(const int& Age) { age = Age; };
            int Age() const { return age; };
};

class Woman : public Person {
    private:
            int hotness;
public:
            Woman(const int& Age, const int& Hotness) : Person(Age), hotness {Hotness} {};
            Woman(const Woman& from) : Person(from), hotness {from.Hotness()} { cout << "copy constructor of woman" << endl; };
            Woman& operator=(const Woman& from) { Person::operator=(from); cout << "copy assignment of woman" << endl; hotness = from.Hotness(); return *this; };
            void Print() override { cout << Age() << " and " << hotness << endl; };
            int Hotness() const { return hotness; };
};

int main() {
    Woman w(24, 10);

    Person p = w;
    p.Print();

    return 0;
}

The output for this version of the program was:

copy constructor
24

Which was a bit of a surprise for me, being a noob, but then a realized that since p is not a pointer, the virtual table is not used, and since it's a Person, Person::Print() got called. So I knew that the copy constructor for Person got called, but I couldn't know if the copy constructor for Woman got called, but that wouldn't really matter, since p is a Person, and through it I'd never have access to Woman::Hotness, not even if I tried a cast.

So I thought he was probably just talking about pointers, so I tried this:

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    p = &w;
    p->Print();

    return 0;
}

the new output being:

20
24 and 10

Now p is a pointer, and because it's a pointer there's no copying or moving going on, just change of reference.

Then I thought I could try dereferencing p and assigning w to it:

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

the output is this:

20
copy assignment
24

I thought the second call to p->Print() would call Woman::Print() since p was pointing to a Woman, but it didn't. Any idea why? The copy assignment from Person got called, I think because p is a Person*.

Then I tried this:

int main() {
    Woman w(24, 10);

    Person* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

the new output is this:

20 and 7
copy assignment
24 and 7

So I guess because p is Person* the copy assignment for Person got called, but not the one for Woman. Weirdly enough, the age got updated but the value of hotness remained the same, and I have no idea why.

One more try:

int main() {
    Woman w(24, 10);

    Woman* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

Output:

20 and 7
copy assignment
copy assignment of woman
24 and 10

Now the numbers seem to be right.

My next move was to remove the implementation of the copy assignment for Person, and see if the default would be called:

//Person& operator=(const Person& from) { cout << "copy assignment" << endl; age = from.Age(); return *this; }

output:

20 and 7
copy assignment of woman
24 and 10

Note that the age was copied, so no worries.

The next obvious move is to remove the implementation of the copy assigment for Woman, and see what happens:

//Woman& operator=(const Woman& from) { Person::operator=(from); cout << "copy assignment of woman" << endl; hotness = from.Hotness(); return *this; };

output:

20 and 7
24 and 10

Everything seems to be fine.

So at this point I can't quite understand what the author meant to say, so if anyone could help me out, I'd appreciate it.

Thanks.

bccs.

like image 867
Bruno Santos Avatar asked Mar 18 '14 00:03

Bruno Santos


2 Answers

One example at a time.

int main() {
    Woman w(24, 10);

    Person p = w;
    p.Print();

    return 0;
}

The object p is not a Woman, it is just a Person object. It is constructed using the copy constructor and makes a copy of the Person base class subobject of w, and so has the same age.

Whether or not a virtual override takes effect is not based on whether you have a pointer, reference, or neither. It is based on the most-derived type of the object as it was created, which can be different from the type of a reference or pointer to that object.

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    p = &w;
    p->Print();

    return 0;
}

The statement p = &w; discards (leaks) the old value of p, and then makes p a pointer to the original object w, as though you had simply done Person* p = &w;. So in this case *p is a Woman, the same object w.

int main() {
    Woman w(24, 10);

    Person* p = new Person(20);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

The statement *p = w; calls the assignment operator of *p. But since *p is a Person, the assignment used is Person::operator=(const Person&);, not Woman::operator=(const Woman&);. So the age member of *p is reassigned, but the most-derived type of *p cannot change and is still Person.

int main() {
    Woman w(24, 10);

    Person* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

This time *p is created as a Woman to begin with. So the most-derived type of object *p is Woman, although the type of expression *p is Person. Then when you call the virtual function Print, both before and after the assignment, the function override from the most-derived type is used, so Woman::Print() is called, not Person::Print().

In the statement *p = w;, the left side has type Person and the right side has type Woman. Since (for these classes) operator= is not a virtual function, the function called depends on the expression types only, so the function used is Person::operator=(const Person&);. As you saw, this has the effect of changing the age member but not the hotness member of object *p!

int main() {
    Woman w(24, 10);

    Woman* p = new Woman(20, 7);
    p->Print();
    *p = w;
    p->Print();

    return 0;
}

This time the type of expression *p is Woman, so *p = w; calls Woman::operator=(const Woman&); and does what you probably expect.

When you start removing the definition of operator= functions, note that's different from deleting the functions as Stroustrup suggests. If an assignment operator is not declared for a class, the compiler automatically generates its own. So removing those declarations has no effect on your program other than the fact that you get less output.

The perhaps unexpected behavior of Person p = w; and *p = w; (where p is a Person*) is known as "object slicing". Stroustrup's recommendation of deleting the copy and assignment functions is meant to avoid accidentally writing code that tries to do it. If those declarations are defined as deleted, neither of those two statements would compile.

like image 42
aschepler Avatar answered Sep 18 '22 10:09

aschepler


Woman w(24, 10);

Person p = w;
p.Print();

24

Which was a bit of a surprise for me, being a noob, but then a realized that since p is not a pointer, the virtual table is not used, and since it's a Person, Person::Print() got called.

Correct

So I knew that the copy constructor for Person got called, but I couldn't know if the copy constructor for Woman got called,...

No, it didn't.

...but that wouldn't really matter, since p is a Person, and through it I'd never have access to Woman::Hotness, not even if I tried a cast.

Consider that the line Person p = creates a new variable p with enough bytes of memory to store the data for a Person. If you call the copy constructor Person::Person(const Person&); the code only knows about the data members for Person - not those for any derived type - so "slices" the Woman object to copy just the data members constituting a Person. There was no room to put hotness, and it wasn't copied.


Person* p = new Person(20);
p->Print();
*p = w;
p->Print();

20
copy assignment
24

I thought the second call to p->Print() would call Woman::Print() since p was pointing to a Woman, but it didn't. Any idea why? The copy assignment from Person got called, I think because p is a Person*.

*p refers to the Person object you've just allocated. The new was only told about Person - it had no way of knowing you might want/expect/hope-for extra space into which the extra fields of a Woman could later be copied, so it just allocated space for a Person. When you wrote *p = w; it copied only the fields that are part of a Person, using the Person::operator=(const Person&) function. This does not set the pointer to the virtual dispatch table to address Woman's table... again there's no knowledge of Woman... which is why even a virtual function like Print won't be resolved to Woman::Print later.


Person* p = new Woman(20, 7);
p->Print();
*p = w;
p->Print();

20 and 7
copy assignment
24 and 7

So I guess because p is Person* the copy assignment for Person got called, but not the one for Woman. Weirdly enough, the age got updated but the value of hotness remained the same, and I have no idea why.

Here, while p does point to a Woman with the extra data member for hotness, the copy is still done using Person::operator=, so it doesn't know to copy the extra field over. Interestingly, it does copy the internal pointer to the virtual dispatch table, so when you use p->Print() it dispatches to Woman::Print.


Woman* p = new Woman(20, 7);
p->Print();
*p = w;
p->Print();

20 and 7
copy assignment
copy assignment of woman
24 and 10

Now the numbers seem to be right.

Yes, because the compiler knew to allocate and copy all the data members of a Woman, which includes the pointer to virtual dispatch table and hotness.


What the rest of your experiments (removing the explicitly defined assignment operators) show is that the problem with which members get copied and whether/how the virtual dispatch table pointer is updated are fundamental to the static types involved, so those problems are there with or without your implementations.


So at this point I can't quite understand what the author meant to say, so if anyone could help me out, I'd appreciate it.

What he's saying is that if someone thinks they're getting a pointer or reference to a Person and copies it as such (like in your earlier attempts), they are often accidentally removing the derived class (Woman) related members and ending up with a simple Person object where at an application logic level a Woman would have made sense. By deleting these operators the compiler will prevent this accidental slicing construction. The correct thing to do is to provide a clone() function that creates a new object of whatever the dynamic object type is, allowing a kind of "virtual copy". If you search for "clone" you'll turn up lots of explanation and examples.

like image 163
Tony Delroy Avatar answered Sep 20 '22 10:09

Tony Delroy