Show Lecture.RuleOfThree as a slide show.
CS253 Rule Of Three
The Rule of Three
The
Rule of Three
states that if a class defines any of these methods, called the
“Big Three”, then it should probably define all of them:
- copy constructor
- copy assignment operator
- destructor
There are certainly exceptions to this rule.
Fortunately, the rule says “probably”.
The C++ standard nudges you along
The C++11 standard states:
The implicit definition of a copy constructor as defaulted is deprecated
if the class has a user-declared copy assignment operator or a
user-declared destructor. The implicit definition of a copy assignment
operator as defaulted is deprecated if the class has a user-declared
copy constructor or a user-declared destructor (15.4, 15.8). In a future
revision of this International Standard, these implicit definitions
could become deleted (11.4).
In other words,
• Write a copy ctor if you wrote operator=
or a dtor.
• Write an operator=
if you wrote a copy ctor or a dtor.
because we might stop providing them someday.
When none of the Big Three are needed
- Often, none of the Big Three are required. Consider:
class Hero {
string name;
public:
Hero(string n) : name(n) { }
auto who() const { return name; }
};
Hero h1("Superman"), h2(h1);
cout << h1.who() << ' ' << h2.who();
Superman Superman
- For
class Hero
, the default big three are all fine:
- copy constructor: copy-constructs the string
- copy assignment operator: copies the string
- destructor: does nothing
- Great!
No problem
class Hero
worked fine, because the string objects
didn’t require any maintenance.
- Sure, the string objects, internally, do all sorts of
dynamic memory manipulation, with calls to new[] and delete[],
but that’s string’s business.
Hero
’s dtor does not
have to delete anything.
- What if I (foolishly) decide that string objects have too much
overhead, and so I rewrite
Hero
to use const char *
instead?
Problem
class Hero {
char *name;
public:
Hero(const char *n) : name(new char[strlen(n)+1]) {
copy(n, n+strlen(n)+1, name);
}
~Hero() { delete[] name; }
auto who() const { return name; }
};
Hero h1("Superman");
cout << h1.who();
Superman
It works well so far. Let’s try using the compiler-generated copy ctor.
Problem
class Hero {
char *name;
public:
Hero(const char *n) : name(new char[strlen(n)+1]) {
copy(n, n+strlen(n)+1, name);
}
~Hero() { delete[] name; }
auto who() const { return name; }
};
Hero h1("Superman"), h2(h1);
cout << h1.who() << ' ' << h2.who() << endl;
Superman Superman
free(): double free detected in tcache 2
SIGABRT: Aborted
Problem with copy ctor
- What did the compiler-generated copy ctor do? This:
Hero(const Hero &rhs) : name(rhs.name) { }
- It simply copied the member variable.
- Trouble is, copying the pointers doesn’t copy the data.
It just copies the pointers. After calling the copy ctor,
we have two pointers that both point to the same memory.
- When
h2
falls out of scope, its dtor is called,
which does delete[] name
.
- Then,
h1
falls out of scope. Its dtor is called,
which does delete[] name
. But … that memory has already been
freed!
- >boom<
Problem with assignment operator
- Of course, the default assignment operator would be similar:
Hero& operator=(const Hero &rhs) { name = rhs.name; }
- Like the default copy ctor, it just copies the pointer,
so we have two pointers pointing to the same location.
- One gets deleted, the other is dangling.
- >boom<
Solution #1
class Hero {
char *name;
public:
Hero(const char *n) : name(new char[strlen(n)+1]) {
copy(n, n+strlen(n)+1, name);
}
Hero(const Hero &rhs) : name(new char[strlen(rhs.name)+1]) {
copy(rhs.name, rhs.name+strlen(rhs.name)+1, name);
}
Hero& operator=(const Hero &rhs) {
delete[] name;
name = new char[strlen(rhs.name)+1];
copy(rhs.name, rhs.name+strlen(rhs.name)+1, name);
return *this;
}
~Hero() { delete[] name; }
auto who() const { return name; }
};
Hero h1("Superman"), h2(h1);
cout << h1.who() << ' ' << h2.who();
Superman Superman
Solution #2
class Hero {
char *name = nullptr;
void set(const char *p) {
delete[] name;
name = new char[strlen(p)+1];
copy(p, p+strlen(p)+1, name);
}
public:
Hero(const char *n) { set(n); }
Hero(const Hero &rhs) { set(rhs.name); }
Hero& operator=(const Hero &rhs) {
set(rhs.name);
return *this;
}
~Hero() { delete[] name; }
auto who() const { return name; }
};
Hero h1("Superman"), h2(h1);
cout << h1.who() << ' ' << h2.who();
Superman Superman
It gets worse
- There are two more methods of interest:
- the move constructor:
Hero(Hero &&rhs)
- the move assignment operator:
operator=(Hero &&rhs)
- These methods don’t copy from the right-hand-side object,
they move from it. They steal the value.
- Why on earth would you want to do that?
Move methods
string where = "base", what = "ball";
string sport = where+what;
cout << sport;
baseball
- This built a temporary string containing
"base" + "ball"
,
and copied that temporary string to sport
.
- Now we have two copies of the string
"baseball"
: one in the unnamed temporary variable, and
one in sport
.
- That’s a shame, given that the string inside of
the temporary variable is no longer needed.
- Imagine if we could just move
"baseball"
from the temporary variable to sport
!
Move methods
- That’s what the move ctor and move assignment operator do.
- They only get called for variables that the compiler has designated
as temporaries—no longer needed, doomed to die soon.
- They steal the value from the temporary variable.
- They must leave the temporary variable in a consistent state,
such that when its dtor is called, we won’t have a problem.
- For a string, steal the char *, set the source
char * to nullptr.
- Neither move method is necessary.
They’re of no use for plain scalar data.
- Big Three + two move methods = Big Five.