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.
Correct
No, it didn't.
Consider that the line
Person p =
creates a new variablep
with enough bytes of memory to store the data for aPerson
. If you call the copy constructorPerson::Person(const Person&);
the code only knows about the data members for Person - not those for any derived type - so "slices" theWoman
object to copy just the data members constituting aPerson
. There was no room to puthotness
, and it wasn't copied.*p
refers to thePerson
object you've just allocated. Thenew
was only told aboutPerson
- it had no way of knowing you might want/expect/hope-for extra space into which the extra fields of aWoman
could later be copied, so it just allocated space for aPerson
. When you wrote*p = w;
it copied only the fields that are part of aPerson
, using thePerson::operator=(const Person&)
function. This does not set the pointer to the virtual dispatch table to addressWoman
's table... again there's no knowledge ofWoman
... which is why even avirtual
function likePrint
won't be resolved toWoman::Print
later.Here, while
p
does point to aWoman
with the extra data member forhotness
, the copy is still done usingPerson::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 usep->Print()
it dispatches toWoman::Print
.Yes, because the compiler knew to allocate and copy all the data members of a
Woman
, which includes the pointer to virtual dispatch table andhotness
.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.
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 simplePerson
object where at an application logic level aWoman
would have made sense. By deleting these operators the compiler will prevent this accidental slicing construction. The correct thing to do is to provide aclone()
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.