The Artima Developer Community
The C++ Source | C++ Community News | Discuss | Print | Email | First Page | Previous | Next
Sponsored Link

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

Page 1 of 3  >>

Advertisement

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

Page 1 of 3  >>

The C++ Source | C++ Community News | Discuss | Print | Email | First Page | Previous | Next

Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us