Solution

I l @ ve RuBoard

graphics/bulb.gif

Problem: Arrays and auto_ptr Don't Mix

1. Consider the following function, which illustrates a common mistake when using auto_ptr :

  template<typename T>   void f( size_t n ) {   auto_ptr<T> p1( new T );   auto_ptr<T> p2( new T[n] );   // ... more processing ...   }  

What is wrong with this code? Explain .

Every delete must match the form of its new . If you use single-object new , you must use single-object delete ; if you use the array form of new[] , you must use the array form of delete[] . Doing otherwise yields undefined behavior, as illustrated in the following slightly modified code.

 T* p1 = new T; // delete[] p1; // error delete p1;      // ok - this is what auto_ptr does T* p2 = new T[10]; delete[] p2;    // ok // delete p2;   // error - this is what auto_ptr does 

The problem with p2 is that auto_ptr is intended to contain only single objects, so it always calls delete , not delete[] , on the pointer it owns. This means that using plain delete , p1 will be cleaned up correctly, but p2 will not.

What will actually happen when you use the wrong form of delete depends on your compiler. The best you can expect is a resource leak. A more typical result is memory corruption soon followed by a core dump. To see this effect, try the following complete program on your favorite compiler:

 #include <iostream> #include <memory> #include <string> using namespace std; int c = 0; class X { public:   X() : s( "1234567890" ) { ++c; }  ~X()                     { --c; }     string s; }; template<typename T> void f( size_t n ) {   {     auto_ptr<T> p1( new T );     auto_ptr<T> p2( new T[n] );   }   cout << c << " ";   // report # of X objects }                     // that currently exist int main() {   while( true )   {     f<X>(100);   } } 

This program will usually either crash or else output a running update of the number of leaked X objects. (For extra fun, try running a system-monitoring tool in another window that shows your system's total memory usage. It will help you to appreciate how bad the leak can be if the program doesn't crash right off.)

Aside on a Non-problem: Zero-Length Arrays Are Okay

What if f() 's parameter is zero (for example, in the call f<int>(0) )? Then the second new turns into new T[0] , and programmers often wonder : "Hmm, is this okay? Can we have a zero-length array?"

The answer is yes. Zero-length arrays are perfectly okay, kosher, and fat-free. The result of new T[0] is just a pointer to an array with zero elements, and that pointer behaves just like any other result of new T[n] including the fact that you may not attempt to access more than n elements of the array. In this case, you may not attempt to access any elements at all because there aren't any.

From 5.3.4 [expr.new], paragraph 7:

When the value of the expression in a direct-new-declarator is zero, the allocation function is called to allocate an array with no elements . The pointer returned by the new-expression is non-null . [Note: If the library allocation function is called, the pointer returned is distinct from the pointer to any other object . ]

"If you can't do anything with zero-length arrays (other than remember their address)," you may wonder, "why should they be allowed?" One important reason is that it makes it easier to write code that does dynamic array allocation. For example, the function f() above would be needlessly more complex if it were forced to check the value of its n parameter before performing the new T[n] call.

Of course, getting back to the main problem, just because zero-length arrays are legal doesn't mean we can get an auto_ptr to own one, any more than we can get an auto_ptr to own an array of any other length. We can't. The array-deletion problem remains.

2. How would you fix the problem? Consider as many options as possible, including the Adapter pattern, alternatives to the problematic construct, and alternatives to auto_ptr .

There are several options (some better, some worse ). Here are four:

Option 1: Roll Your Own auto_array

This can be both easier and harder than it sounds.

Option 1 (a): By Deriving from auto_ptr (Score: 0 / 10)

Bad idea. For example, you'll have a lot of fun reproducing all the ownership and helper-class semantics. This might be tried only by true gurus, but true gurus would never try it because there are easier ways.

Advantages: Few.

Disadvantages: Too many to count.

Option 1 (b): By Cloning auto_ptr Code (Score: 8 / 10)

The idea here is to take the code from your library's implementation of auto_ptr , clone it (renaming it auto_array or something like that), and change the delete statements to delete[] statements.

Advantages:

  1. Easy to implement (once). We don't need to hand-code our own auto_array , and we keep all the semantics of auto_ptr automatically, which helps avoid surprises for future maintenance programmers who are already familiar with auto_ptr .

  2. No significant space or time overhead .

Disadvantage:

  1. Hard to maintain. You'll probably want to be careful to keep your auto_array in sync with your library's auto_ptr when you upgrade to a new compiler/library version or switch compiler/library vendors .

Option 2: Use the Adapter Pattern (Score: 7 / 10)

This option came out of a discussion I had with C++ World attendee Henrik Nordberg, after one of my talks. Henrik's first reaction to the problem code was to wonder whether it would be easiest to write an adapter to make the standard auto_ptr work correctly, instead of rewriting auto_ptr or using something else. This idea has some real advantages and deserves analysis despite its few drawbacks.

Here's the idea. Instead of writing

 auto_ptr<T> p2( new T[n] ); 

we write

 auto_ptr< ArrDelAdapter<T> >   p2( new ArrDelAdapter<T>(new T[n]) ); 

where the ArrDelAdapter (array deletion adapter) has a constructor that takes a T* and a destructor that calls delete[] on that pointer:

 template<typename T> class ArrDelAdapter { public:   ArrDelAdapter( T* p ) : p_(p) { }  ~ArrDelAdapter() { delete[] p_; }   // operators like "->" "T*" and other helpers private:   T* p_; }; 

Since there is only one ArrDelAdapter<T> object, the single-object delete statement in ~auto_ptr() is fine. Because ~ArrDelAdapter<T> correctly calls delete[] on the array, the original problem has been solved .

Sure, this may not be the most elegant and beautiful approach in the world, but at least we didn't have to hand-code our own auto_array template!

Advantage:

  1. Easy to implement (initially). We don't need to write an auto_array . In fact, we get to keep all the semantics of auto_ptr automatically, which helps avoid surprises for future maintenance programmers who are already familiar with auto_ptr .

Disadvantages:

  1. Hard to read. This solution is rather verbose.

  2. (Possibly) hard to use. Any code later in f that uses the value of the p2 auto_ptr will need syntactic changes, which will often be made more cumbersome by extra indirections.

  3. Incurs space and time overhead. This code requires extra space to store the required adapter object for each array. It also requires extra time, because it performs twice as many memory allocations (this can be ameliorated by using an overloaded operator new), and then an extra indirection each time calling code accesses the contained array.

Having said all that, even though other alternatives turn out to be better in this particular case, I was very pleased to see people immediately think of using the Adapter pattern. Adapter is widely applicable and one of the core patterns every programmer should know.

Guideline

graphics/guideline.gif

Know about and use design patterns.


Just a final note on Option 2: It's worth pointing out that writing

 auto_ptr< ArrDelAdapter<T> >   p2( new ArrDelAdapter<T>(new T[n]) ); 

isn't much different from writing

 auto_ptr< vector<T> > p2( new vector<T>(n) ); 

Think about that for a moment. For example, ask yourself, "What, if anything, am I gaining by allocating the vector dynamically that I wouldn't have if I just wrote vector p2(n); ?" Then see Option 4.

Option 3: Replace auto_ptr with Hand-Coded Exception-Handling Logic (Score: 1 / 10 )

Function f() uses auto_ptr for automatic cleanup and probably for exception safety. Instead, we could drop auto_ptr for the p2 array and hand-code our own exception-handling logic.

That is, instead of writing:

 auto_ptr<T> p2( new T[n] ); // // ... more processing ... // 

we write something like:

 T* p2( new T[n] ); try {   //   // ... more processing   // } delete[] p2; 

Advantages:

  1. Easy to use. This solution has little impact on the code in "more processing" that uses p2 ; probably all that's necessary is to remove . get() wherever it occurs.

  2. No significant space or time overhead.

Disadvantages:

  1. Hard to implement. This solution probably involves many more code changes than are suggested by the above. The reason is that while the auto_ptr for p1 will automatically clean up the new T no matter how the function exits, to clean up p2 we now have to write cleanup code along every code path that might exit the function. For example, consider the case in which "more processing" includes more branches, some of which end with " return; ".

  2. Brittle. See (a): Did we put the right cleanup code along all code paths?

  3. Hard to read. See (a): The extra cleanup logic is likely to obscure the function's normal logic.

Option 4: Use a vector Instead of an Array (Score: 9.5 / 10 )

Most of the problems we've encountered have been due to the use of C-style arrays. If appropriate, and it's almost always appropriate, it would be better to use a vector instead of a C-style array. After all, a major reason why vector exists in the standard library is to provide a safer and easier-to-use replacement for C-style arrays!

So instead of writing:

 auto_ptr<T> p2( new T[n] ); 

we write:

 vector<T> p2( n ); 

Advantages:

  1. Easy to implement (initially). We still don't need to write an auto_array .

  2. Easy to read. People who are familiar with the standard containers (and that should be everyone by now!) will immediately understand what's going on.

  3. Less brittle. Since we're pushing down the details of memory management, our code is (usually) further simplified. We don't need to manage the buffer of T objects ”that's the job of the vector<T> object.

  4. No significant space or time overhead.

Disadvantages:

  1. Syntactic changes. Any code later in f that uses the value of the p2 auto_ptr will need syntactic changes, although the changes will be fairly simple and not as drastic as those required by Option 2.

  2. (Sometimes) usability changes. You can't instantiate any standard container (including a vector ) of T objects if those T objects are not copy-constructible and assignable. Most types are both copy-constructible and assignable. But if they are not, this solution won't work.

Note that passing or returning a vector by value is more work than passing or returning an auto_ptr . I consider this objection somewhat of a red herring, however, because it's an unfair comparison. If you want to get the same effect, you simply pass a pointer or reference to the vector .

Guideline

graphics/guideline.gif

Prefer using vector instead of built-in (C-style) arrays.


I l @ ve RuBoard


More Exceptional C++
More Exceptional C++: 40 New Engineering Puzzles, Programming Problems, and Solutions
ISBN: 020170434X
EAN: 2147483647
Year: 2001
Pages: 118
Authors: Herb Sutter

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net