|
|
|
Sponsored Link •
|
|
Advertisement
|
A pontificating pair of programmers
Sought to answer community clamours
For gnomes plain and simple,
From "safe bool" to Pimpl:
Quick learning for time-poor code crammers
Yes, you read the subject line correctly. A well-known and very important rule known as the Law of the Big Three [2] states that whenever you need either a (non-trivial) copy constructor, copy assignment operator, or destructor, you'll most likely need to implement the others, too. This set of special functions—copy constructor, copy assignment operator, and destructor—is fondly called the Big Three by C++ programmers all over the world; it was given its catchy name by Marshall Cline et al in C++ FAQs [2]. We contend that nowadays, one of these three should be a non-issue in many classes. This article explains why this is so.
To understand what the Law of the Big Three is all
about, consider what happens when you add a dynamically
allocated resource to a class (SomeResource* p_ in the code
below):
class Example {
SomeResource* p_;
public:
Example() : p_(new SomeResource()) {}
};
Now, because you are acquiring the resource in the constructor, you need to release it in the destructor, like so:
~Example() {
delete p_;
}
That's it; you're done, right? Wrong! As soon as someone decides to
copy-construct this class, all hell breaks loose. The reason is that the
compiler-generated copy constructor will simply make a copy of the
pointer p_; it has no way of knowing it should also allocate
memory for a new SomeResource. Thus, when the first instance
of Example is deleted, its destructor frees p_.
Subsequent use of the resource in the other instance of
Example (including deleting it in the destructor, of course)
wreaks havoc, because that instance of SomeResource doesn't
exist any more. Check it out with some simple tracing:
class Example {
SomeResource* p_;
public:
Example() : p_(new SomeResource()) {
std::cout << "Creating Example, allocating SomeResource!\n";
}
~Example() {
std::cout << "Deleting Example, freeing SomeResource!\n";
delete p_;
}
};
int main() {
Example e1;
Example e2(e1);
}
Executing this program is guaranteed to end in tears. With tissues at the ready, let's run it:
C:\projects>bigthree.exe
Creating Example, allocating SomeResource!
Deleting Example, freeing SomeResource!
Deleting Example, freeing SomeResource!
6 [main] bigthree 2664 handle_exceptions:
Exception: STATUS_ACCESS_VIOLATION
1176 [main] bigthree 2664 open_stackdumpfile:
Dumping stack trace to bigthree.exe.stackdump
Clearly, you need to define a copy constructor that
correctly copies SomeResource:
Example(const Example& other) : p_(new SomeResource(*other.p_)) {}
Assuming that SomeResource has an accessible
copy constructor, this improves the situation slightly; but it
is still subject to a crash as soon as someone decides to have
a go at assigning to an instance of Example:
int main() {
Example e1;
Example e2;
e2=e1;
}
Here's more grief coming your way; have a look at this output:
C:\projects>bigthree.exe
Creating Example, allocating SomeResource!
Creating Example, allocating SomeResource!
Deleting Example, freeing SomeResource!
Deleting Example, freeing SomeResource!
5 [main] bigthree 3780 handle_exceptions:
Exception: STATUS_ACCESS_VIOLATION
1224 [main] bigthree 3780 open_stackdumpfile:
Dumping stack trace to bigthree.exe.stackdump
As you can see, two instances of SomeResource
are being allocated, and two are being deleted. So what's the
problem? Well, the problem is that both instances of
Example are pointing to the same instance of
SomeResource! This is due to the automatically
generated copy assignment operator, which only knows how to
assign the pointer to SomeResource. Thus, you
will also need to define a reasonable copy assignment operator
to go with the copy constructor:
Example& operator=(const Example& other) {
// Self assignment?
if (this==&other)
return *this;
*p_=*other.p_; // Uses SomeResource::operator=
return *this;
}
You'll note that this operator first checks for self-
assignment, and simply returns *this if that is
the case. With regards to exception safety, the copy
assignment operator provides the basic guarantee. For
in-depth discussions on exception safety and copy assignment
operators, see [3, 4].
Now the program behaves correctly! The lesson to learn here is exactly what is called the Law of the Big Three—as soon as a non-trivial destructor is needed, make sure that the copy constructor and copy assignment operator do the right thing. Most of the time, this is ensured by manually defining them.
|
Sponsored Links
|