Smart pointers allow you to manage the memory in a program yourself, without relying on garbage collectors. If you're building performance critical software, you need to know how to use pointers. In this third article in our series covering how to use pointers, J. Nakamura takes a look at what smart pointers can do, and warns about the pitfalls to avoid.
Contributed by J. Nakamura Rating: / 4 December 13, 2004
In the previous articles we looked at methods we can use to employ pointers in C++. If pointers are that useful, then why do most modern languages make such a big deal of not using them and come with garbage collectors instead? The answer is quite simple: you need to travel quite a steep learning curve to become a responsible pointer user and it is much easier to rely on a garbage collector to look after your memory usage.
I don’t want to start a holy language war here, but I do want to point out that when you are building performance critical software, you want to be managing the memory yourself. Even when you are not working on high performance software, you should still be aware of memory usage and the kind of tasks from which a garbage collector relieves you! There is no excuse to go on a memory abuse rampage. Computer programming is a complex task and even though the learning curve can be flattened out by using garbage collectors, it doesn’t necessarily mean that the task itself has become less complex. It all depends on what you want the machine to do for you.
Let’s take a closer look first at why the usage of pointers can be dangerous, then I will introduce some mechanics C++ enables you to create, in order to make it safer to rely on pointers. Originally I expected that this topic would fit in one article, but there is so much to say about smart pointers that I have to spread it out over several articles instead. In this article we will mainly be looking at the STL std::auto_ptr.
In any procedural programming language, you heavily rely on functions. Functions acquire resources, perform operations and then free the resources. Functions create resources on the stack when the function is being entered and might create resources there during its execution. As soon as we leave the function, the resources are thrown from the stack, destroying them.
A typical function could look like this:
void foo() { MyClass *pMyObj = new MyClass; /* perform some operations here */ delete pMyObj; }
The most obvious problem with pointers is the possible misbalancing of new and delete (every allocated resource has to be freed to prevent memory leaks). In this example, you see the memory allocated being freed just before we exit the function. But is this the only way we can exit this function? An exception might be thrown in the body of this function, causing us to leave it without ever reaching the delete statement. If we want to avoid causing the resulting memory (or other resources like files) to leak, we need to catch all exceptions:
void foo() { try { MyClass *pMyObj = new MyClass; /* perform some operations here */ } catch (…) { delete pMyObj; throw; // rethrow the exception } delete pMyObj; }
You will most probably have found yourself writing code like this and know that this structure only makes the code more complex and more difficult to maintain--especially when we want to handle different types of exceptions differently. Sometimes you might find solutions that don’t need to rethrow the exception employ the following solution:
Still, the whole exception mechanism was invented to make sure the stack unwinds when an error is faced (offering constructed objects the possibility of properly destructing); all because the goto statement completely ignores this. Looking upon the code above, it becomes clear that this mixture of try/catch and goto is not the right way to handle possible resource leaks when exceptions are thrown.
Goto’s are bad and should never be used, unless you are 100% sure of what you are doing. This means that you are not jumping backwards and not out of a function; the goto label is nearby and there are no possible resource leaks when you jump.
It is obvious why the implementation of a garbage collector takes a lot of the worries of memory management away from you. In C++ there is another alternative--a ‘smart pointer.’ A smart pointer behaves much like a built-in pointer, except that it automatically frees resources when they are destroyed. The concept of a smart pointer is essentially simple, though the many different ways we can implement this concept provides different behaviours and leaves us with much to investigate.
When an object is declared locally in a function and thus on the stack, its destructor is automatically called when the object is thrown from the stack. If we were to create an object that can hold a built-in pointer for us, it can then call delete for us in its destructor. Our resource is automatically released and there is no need for the complex try/catch (or even worse goto) structure to make sure we are not leaking. You can see that this is as useful for freeing allocated memory as it is for guaranteeing the closure of opened files.
The smart pointers we will be looking at can be used just like the built-in pointers we have been looking at in previous articles. This means that you can use the reference and pointer-to-member operators, but you are prohibited from using the dereference and arithmetic operators. Why this is will be discussed when we look at common pitfalls when using pointers.
The Standard Library offers a smart pointer known as the auto_ptr. The auto_ptr serves as the owner of the resource it refers to and frees it automatically when it gets destroyed. Exactly the kind of thing we are looking for, right? Well there is one important caveat with the std::auto_ptr – a requirement of the std::auto_ptr is that its object can have only one owner. We will look at the consequences of this requirement, but first let’s rewrite previous example using an auto_ptr.
void foo() { std::auto_ptr<MyClass> pMyObj(new MyClass); /* perform some operations here */ }
There no longer is a need to worry about memory leaks in the face of exceptions, and we can even forget about including the delete statement! Great, isn’t it? Note that you can only initialise smart pointers through the constructor; the assignment operator has been reserved to only work with objects of the same (smart pointer) class, making the following statement invalid:
Because the auto_ptr forces the object it contains to have exactly one owner, this means that no two auto_ptrs should be pointing to the same object at the same time. It is up to you to make sure that this doesn’t happen.
So what happens when ownership is transferred? Look at the following example:
It means that when ownership is transferred from pMyObj to pMyObj2, whatever pMyObj2 was pointing to is freed (not a big deal in this case, but what happens when you assign it another auto_ptr again?), the pointer to the resource is copied from pMyObj to pMyObj2, and pMyObj no longer points to that resource. In fact, it is pointing to NULL!
When you are using auto_ptr for making sure that resources are freed upon exiting a function, its usage is straightforward. Still, I recommend you make the object const, just to show that it is not your intention to allow its ownership to be transferred:
void foo() { std::auto_ptr<MyClass> const pMyObj(new MyClass); /* perform some operations here */ }
You can still make changes to the object to which the pointer contained by pMyObj refers:
pMyObj->myVar = 10;
But the const modifier prohibits you from making changes to pMyObj itself:
pMyObj = other_auto_ptr;
This way you can make it clear to other coders (who might have to maintain your code later) that the resource pMyObj contains has to be freed upon exiting this function and that its ownership is not to be transferred. Why is this important, you ask? Well, simply because the auto_ptr violates the general behavior of initializations and assignments in programming languages.
Because of the requirement that there can be only one owner of a resource when using the auto_ptr, the copy constructor of an auto_ptr modifies the object that is used to initialize the new object, and the assignment operator modifies the right-hand side of the assignment. Yes, you read that right! It is up to you to make sure that the auto_ptr that lost ownership and is now holding NULL, is no longer dereferenced. This behavior is quite counter-intuitive, since you would expect a copy constructor and assignment operator to look like this:
Spot the difference! That’s right, it violates general behavior, because the function parameters have no const modifier and are being changed in both functions! The fact that they have to be changed (the pointer they held is set to NULL to indicate they lost ownership), prevents the function arguments from being const. This can cause a lot of confusion!
Because ownership of resources is transferred from one auto_ptr to another, it is possible to originate allocated resources from a function or to make a function work like a dead-end for allocated resources. When an auto_ptr is returned, the function is known to behave as a source of the data.
Every time foo() is called, a new MyClass object is constructed and its ownership is transferred to the caller of this function using the auto_ptr. For example:
In the example above, MyClass is allocated 10 times and destructed 10 times. The allocation takes place in foo() and the destruction happens in the scope of the for-loop within bar(). The ownership of MyClass is transferred to pObj in every loop and we know that, since the assignment operator releases any previous resource owned by the current auto_ptr, MyClass gets properly destructed 9 times. Yes, 9 times, because at the end of bar() pObj will still be the owner of a MyClass. But as we leave the scope of the function, that last MyClass resource will be freed as well.
When an auto_ptr is passed as an argument to a function, the function will obtain ownership of the auto_ptr and behave as a sink of the data.
void foo(std::auto_ptr<MyClass> pMyObj);
Every time foo is called, it will free the resource held by the auto_ptr:
So in the example above, MyClass gets constructed 10 times by bar() and is freed 10 times in foo(), simply because the auto_ptr that is passed into foo() is not being passed back to bar(). As soon as it loses scope in foo(), the MyClass resource allocated in bar() gets released inside foo().
Often a function takes a const reference to the object passed into it when they copy that object internally (this happens in the STL with containers for example):
template<class T> void container::insert( T const &value );
An auto_ptr modifies the object with which it is being initialized. So when insert tries to make a copy of value, an auto_ptr will try to modify value from which it is prohibited by the const modifier. This is actually good behaviour; imagine what would happen if you could validly store auto_ptrs inside a container. The agony of having ownership transferred to containers as you are filling them, then taking back ownership as you are using them, would make it just a too big a headache to keep track of what could happen in that situation!
You understand now that the auto_ptr is not suitable for every kind of smart pointer you can think of. In fact, if you don’t understand how the ownership semantics work, I think you should steer away from it. What if you do want to store smart pointers in STL containers? What if you want a smart pointer that you can reset with another resource and that destroys itself when it goes out of scope (no a const std::auto_ptr<T> won’t suffice here)?
The boost (www.boost.org) library extends the STL for that reason with smart pointers of its own (it is peer-reviewed and some of the boost libraries might be included in the STL by the C++ Standards Committee) and we will take a look at them in the next article.