|
|
|
The C++ Source |
C++ Community News |
Discuss |
Print |
Email |
Screen Friendly Version |
Previous |
Next
|
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.
It should be noted that there are times when copy construction and copy assignment do not make sense for a class. In those cases, it's trivial to disable them by using the common idiom of declaring the copy constructor and copy assignment operator private, as in:
class SelfishBeastie
{
. . .
private:
SelfishBeastie(const SelfishBeastie&);
SelfishBeastie& operator=(const SelfishBeastie&);
};
An alternative is to use existing code; there's a class called
boost::noncopyable [5] in the Boost libraries;
inheriting from that class is nice as it documents that the
class does not support copying and assignment (at least for
all who are familiar with noncopyable!).
class SelfishBeastie
: boost::noncopyable
{
. . .
Another way of prohibiting copy construction and copy
assignment is to make one or more members a reference or
const (or const reference, for the
especially cautious)—this effectively shuts down the
compiler's ability to generate these special member functions.
As Matthew discusses in detail in Imperfect C++ [6], this is not the
preferred way of denoting that a class is not copyable, since
it fails to adequately communicate the class's design to its
users. It is, however, an excellent way to enforce design
decisions; hence, it is a mechanism for communicating the
original design decisions to future maintainers of the class,
rather than a way of documenting semantics to the class's
users. (Of course, with this technique all constructors of the
class will need to initialize the reference members (as
opposed to overwriting them within the constructor body),
which is itself a good thing.)
Although you've now come quite far in making the class
Example behave properly, it's easy to go astray
when exceptions come into play. Let's add another pointer to
SomeResource in our Example class,
like so:
class Example {
SomeResource* p_;
SomeResource* p2_;
public:
Example() :
p_(new SomeResource()),
p2_(new SomeResource()) {
std::cout << "Creating Example, allocating SomeResource!\n";
}
Example(const Example& other) :
p_(new SomeResource(*other.p_)),
p2_(new SomeResource(*other.p2_)) {}
Example& operator=(const Example& other) {
// Self assignment?
if (this==&other)
return *this;
*p_=*other.p_;
*p2_=*other.p2_;
return *this;
}
~Example() {
std::cout << "Deleting Example, freeing SomeResource!\n";
delete p_;
delete p2_;
}
};
Now consider what happens when an instance of Example is
being constructed, and the second instance of
SomeResource (pointed to by p2_)
throws upon construction. One SomeResource,
pointed to by p_, will already have been
allocated, but still, the destructor will not be called! The
reason is that from the compiler's point of view, the instance
of Example has never existed, because the
constructor never completed. This unfortunately means that
Example is not exception-safe, due to a potential
resource leak.
To make it safe, one practicable alternative is to move the initialization outside of the ctor-initializer, like so:
Example() : p_(0),p2_(0)
{
try {
p_=new SomeResource();
p2_=new SomeResource("H",true);
std::cout << "Creating Example, allocating SomeResource!\n";
}
catch(...) {
delete p2_;
delete p_;
throw;
}
}
Although you've managed to solve the issue of exception safety for the moment, this is not a very appealing solution, since we C++ programmers are weaned on initialization over assignment. As you'll soon see, an old and trustworthy technique comes to the rescue.
The ubiquitous reference to RAII (Resource Acquisition
Is Initialization [7])
should be justified in this case, because we come here seeking
the essence of what RAII is, namely that the constructor of a
local object handles the acquisition of a resource and its
destructor releases it. Using this idiom means that it isn't
possible to forget about releasing a resource; nor is it
required to think about the subtle issues that you've just
handled manually for the Example class. A simple wrapper
class, intended mainly for the purpose of adding RAII to
simple classes like SomeResource, might look like
this:
template <typename T> class RAII {
T* p_;
public:
explicit RAII(T* p) : p_(p) {}
~RAII() {
delete p_;
}
void reset(T* p) {
delete p_;
p_=p;
}
T* get() const {
return p_;
}
T& operator*() const {
return *p_;
}
void swap(RAII& other) {
std::swap(p_,other.p_);
}
private:
RAII(const RAII& other);
RAII& operator=(const RAII& other);
};
The only responsibilities this RAII class has are to store a
pointer, return it when someone needs it, and properly delete
it when destructed. Using such a class greatly simplifies the
Example class; both now and when applying any
future changes to it:
class Example {
RAII<SomeResource> p_;
RAII<SomeResource> p2_;
public:
Example() :
p_(new SomeResource()),
p2_(new SomeResource()) {}
Example(const Example& other)
: p_(new SomeResource(*other.p_)),
p2_(new SomeResource(*other.p2_)) {}
Example& operator=(const Example& other) {
// Self assignment?
if (this==&other)
return *this;
*p_=*other.p_;
*p2_=*other.p2_;
return *this;
}
~Example() {
std::cout << "Deleting Example, freeing SomeResource!\n";
}
};
You're basically back where you started with the original version, which hasn't a care with respect to exception safety. However, this time being oblivious to resource management and potential exceptions is intended, because it's already taken care of. This brings us to a very important point—the destructor now does nothing except write out a simple trace message:
~Example() {
std::cout << "Deleting Example, freeing SomeResource!\n";
}
This means that one could (or even should!) remove the
destructor, and rather rely on the compiler-generated version [8].
One of the Big Three is suddenly out of a job in the
Example class! However, you must duly note that
this simple example only handles raw pointers; there are many
other resources than that in real-world programs. Although
many of them provide clean-up services upon deletion (again,
RAII in action), some don't or can't. This, too, can be solved
without defining a destructor, which is the topic of the next
section.
Note: Diligent readers may observe that the
RAII class isn't exactly all it could be, but in
fact, it doesn't have to, because a similar implementation
already exists in the C++ Standard Library, namely
std::auto_ptr. It basically works the way we've
shown for the RAII class, only better. Why
provide your own, then? Because auto_ptr defines
a public copy constructor and copy assignment operator, both
of which assume ownership of the resource, whereas you need it
to be copied (the RAII class doesn't do that
either, but at least it reminds you to do it [9]). You
need to copy the resource, not have its ownership silently
transferred, so for the sake of this example we're happy to
reinvent the wheel.
What we've shown here in terms of resource management
exists in virtually every smart pointer class (many thousand
programmers think that the ones in Boost [10, 11] are especially fine
examples of smart pointers). But as mentioned, resource
management is not just about calling delete, it
may require some other logic, or other means of freeing the
resources (for example, calling close()). That's
probably the reason why more and more smart pointer types are
becoming smart resources; beyond supporting automatic deletion
of dynamically allocated resources, they allow customization
so that it is possible to have user-defined deleters called,
or even defining the deallocation function inline (made
possible through bind expressions and lambda expressions, as
those found in Boost.Lambda [12]). Much of the code that one
has previously put in the destructor of the aggregating class
is now coupled more tightly with the resource (or resource
holder), which makes perfect sense. It will be interesting to
see what the future brings in this area. With multi-threading
and exception-safety management that go far beyond what many
of us previously were exposed to (at least that's true for the
authors), intelligent resource management tools are becoming
increasingly important.
The Law of the Big Three has played, and continues to play, an important role in C++. However, we think that there is good reason to leave the destructor out of both the discussion and the implementation when possible, leading to a derivative "Law of the Big Two". The reason is that often, unadorned pointers should be avoided as class members—to be replaced by smart pointers. Either way, the role of copy constructors and copy assignment operators is often forgotten or ignored; it's our hope that this article may help address that in some small way.
boost::noncopyable is documented here.
const std::auto_ptr would have done the sa
me for the examples in this article.
Matthew Wilson is a software development consultant for Synesis Software, and creator of the STLSoft libraries. He is author of the forthcoming book Imperfect C++ (Addison-Wesley, 2004), and is currently working on his next two books, one of which is not about C++. Matthew can be contacted via http://imperfectcplusplus.com/.
Bjorn Karlsson is proud to be a C++ designer, programmer, teacher, preacher, and student. He has finally learned enough about C++ to realize how little he knows. When not reading or writing articles, books, or code, he has the privilege to be a part of the Boost community, and a member of The C++ Source Advisory Board. His book, Beyond The C++ Standard Library: An Introduction to Boost, will be published by Addison-Wesley in 2005. He appreciates it when people send him interesting emails at bjorn.karlsson@readsoft.com.
|
Sponsored Links
|