The Law of The Big Two
by Bjorn Karlsson and Matthew Wilson
October 1, 2004

Summary
Welcome to the first installment of Smart Pointers, a monthly- ish column written exclusively for The C++ Source. Here, two seasoned [1] programmers—Bjorn Karlsson and Matthew Wilson—carefully dissect C++ idioms, tricks, and power techniques. To make up for the fact that these are very serious topics, we shall occasionally expose you to really crummy programming jokes, too. Now, who said there was no such thing as a free lunch? In this instalment the authors update The Law of The Big Three, and explain which of the three magic member functions is often not needed.
The Law of The Big Two
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.

Background

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.

Disabling Copy Construction and Copy Assignment

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.)

The Big Three Aren't Enough

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.

RAII Saves the Day

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.

Smart Pointers or Smart Resources?

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.

Conclusion

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.

Acknowledgements

  • We'd like to thank Marshall Cline for inventing the easily remembered name "The Big Three" in C++ FAQs. It has helped many programmers remember to add copy constructors and assignment operators as needed.
  • Thanks to Bjarne Stroustrup for the concise explanation of RAII in C++ Style and Technique FAQ and in his immortal tome, The C++ Programming Language (3rd Edition).
  • Thanks to Chuck Allison, for editing this article (and many of our other articles) and greatly improving upon it (them) without getting the frequent acknowledgement and appreciation he really deserves (but here you go, Chuck. You rock!).
  • Thanks to Andrei Alexandrescu, David Abrahams, and Thorsten Ottosen, for reviewing the article.
  • Thanks to Andrei Alexandrescu, Bjarne Stroustrup, Chuck Allison, David Abrahams, Nathan Myers, and Walter Bright, for thoroughly discussing the ins and outs of function- try-blocks[13].
  • Thanks to Daniel Teske, Mehul Mahimtura, and Vesa Karvonen, for valuable feedback and comments.

Notes and References

  1. One with Wasabi, the other a little Soy sauce and some pine kernels.
  2. C++ FAQs (Addison-Wesley 1998), by Marshall Cline, Greg Lomow, and Mike Girou.
  3. See Herb Sutter's GotW 59.
  4. More Exceptional C++ (Addison-Wesley 2001), by Herb Sutter.
  5. boost::noncopyable is documented here.
  6. Imperfect C++ (Addison-Wesley 2004), by Matthew Wilson.
  7. See Bjarne Stroustrup's C+ + Style and Technique FAQ.
  8. In some cases, for example when defining a base class, you'll want to declare the destructor virtual, to ensure that deletion of an instance of a derived class through a pointer to a base class works correctly. Of course, an empty implementation will suffice.
  9. A const std::auto_ptr would have done the sa me for the examples in this article.
  10. More about Boost at www.boost.org
  11. Boost.Smart_ptr is documented at http://www.boost.org/libs/smart_ptr/smart_ptr.htm
  12. Boost.Lambda is documented at http://www.boost.org/libs/lambda/doc/index.html
  13. Some readers may wonder why there was no mention of function-try-blocks in the article. Well, that was the conclusion of the discussions: for the topics depicted herein, do not resort to using function-try-blocks. Even though many modern compilers will allow you to manipulate member variables in the catch clauses of function-try-blocks, and POD types such as our pointers keep their values—whether initialized or uninitialized—from the epoch of the exception thrown in the ctor-initializer, any reliance on such behavior is a violation of the standard and is non-portable. The fact that it's also mind-bendingly confusing puts the cherry on the cake. Just say no!

Talk back!

Have an opinion? Readers have already posted 36 comments about this article. Why not add yours?

About the authors

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/.