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.Have an opinion? Readers have already posted 36 comments about this article. Why not add yours?
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. He appreciates it when people send him
interesting emails at bjorn.karlsson@readsoft.com.
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/.
Artima provides consulting and training services to help you make the most of Scala, reactive
and functional programming, enterprise systems, big data, and testing.