Pixelate:Issue 11/Rule of Three
From Allegro Wiki
| The Rule of Three | |
| Original author: | Chris Barry |
|---|---|
| Website: | |
| zip: | some.zip |
This begins a new series on C++ programming in general, acting as a bit of an FAQ on intermediate topics. After four grueling articles on the C++ Standard Library, I thought it was time for something easier to write about. Perhaps articles about things like stringstreams and file streams will come later.
This article simply details what is known in some circles as the Rule of Three. It's not really a solution to a problem as much as it is something you should just be aware of, since not knowing what I'm about to teach you can lead to some really subtle bugs that will have you ripping out your hair trying to figure out what's wrong. The Rule of Three is simply this:
Any class that has an explicitly defined destructor, copy constructor or assignment operator generally needs all three.
"Explicitly defined" means that you added the code for it to your class yourself. If you don't, the compiler will write it's own. So you can do something like this:
class Coords {
public:
int x, y;
};
Coords c1, c2;
....
c1 = c2;
That will work even though you never overloaded the assignment operator, because the computer is smart enough to write its own implicit version that just copies the members of c2 into c1. However, this fails if your class has pointers and dynamically allocated data, and that's when you get your subtle bugs. Let's make a simple class with a destructor, but no copy constructor or assignment operator:
class Array {
public:
// member variables
int* data;
int size;
// constructor
Array(int A): size(A), data(new int[A]) {}
// destructor
~Array() {delete[] data;}
};
Okay, simple class. No errors, right? Well, other than the errors of omission, which we'll see presently. Let's try something with it. Notice that this is only an example; I don't want to hear smart alecks e-mailing me telling me I should make this an Array:: member function or overload [] or something ...
void PrintArray(Array temp)
{
for(int i = 0 ; i < temp.size; ++i)
cout << temp.data[i] << " ";
}
....
Array myarray(10);
// do stuff with myarray, like assign values to elements
PrintArray(myarray);
Do you see the error?
Here's the idea; notice that we're passing to PrintArray() by value. That means temp is a copy of myarray (likely using the copy constructor, though this applies to the assignment operator too). The size variable is copied (no problem) and so is the data variable (BIG problem). See, now temp.data and myarray.data are pointing at the same dynamically allocated array, because only the pointer was copied, NOT the data. When temp goes out of scope at the end of the function, the destructor is called, and the data pointed at is deleted. Which myarray was still using. Whoopsie. Now that data no longer exists, and accessing it now produces undefined behavior. This can crash or not, and makes pinning down a bug very frustrating.
I know this example looks contrived (temp should be a reference, PrintArray should be a member function, etc.), but it's not so uncommon a thing. For example, if you call the STL member function push_back() to add objects to an STL container, a copy of the object is added. Same problem.
The solution?
Write an explicit copy constructor and assignment operator. But when you write them, don't just copy the variables; make a copy of the dynamically allocated data. Like this:
// copy constructor
Array(const Array& a): size(a.size), data(new int[a.size])
{
for(int i = 0 ; i < size ; ++i)
data[i] = a.data[i];
}
There. Now instead of just copying the pointer, we copy the data. Now we can have two objects with their own data, and when one is destroyed, the other is unaffected. Similarly with the assignment operator:
// assignment operator
Array& operator= (const Array& a)
{
size = a.size;
data = new int[size];
for(int i = 0 ; i < size ; ++i)
data[i] = a.data[i];
return *this;
}
Mostly the same, but without the initialization list (only constructors can have those). Plus it's wise to return the object, so we can do "chaining":
a1 = a2 = a3;
Anyway, that's about it. Here's our new nice and safer class:
class Array {
public:
// member variables
int* data;
int size;
// constructor
Array(int A): size(A), data(new int[A]) { }
// copy constructor
Array(const Array& a): size(a.size), data(new int[a.size]) {
for(int i = 0 ; i < size ; ++i)
data[i] = a.data[i];
}
// destructor
~Array() { delete[] data; }
// assignment operator
Array& operator= (const Array& a) {
size = a.size;
data = new int[size];
for(int i = 0 ; i < size ; ++i)
data[i] = a.data[i];
return *this;
}
};
Hopefully, you can now see the meaning behind the Rule of Three, which basically acts as a guideline for making sure that any class that manages its own dynamic data protects it. This was just an example; in the real world you'd make size and data private and overload the [] operator, and maybe add a function for retrieving the size, etc. Of course, that would apply to any class, but we see that dynamically allocated data needs extra treatment above and beyond that. You can also get around some of these difficulties in other ways too; for example, I already mentioned that the STL can copy stuff behind your back. Well, if you keep a container of pointers instead, the object itself doesn't get copied, and the problem evaporates. But it's so little effort to add explicit code for all three functions, and it can save you big headaches later on.
I hope this information was helpful to you.
