GotW #42

Home Blog Talks Books & Articles Training & Consulting

On the
blog
RSS feed November 4: Other Concurrency Sessions at PDC
November 3
: PDC'09: Tutorial & Panel
October 26: Hoare on Testing
October 23
: Deprecating export Considered for ISO C++0x

This is the original GotW problem and solution substantially as posted to Usenet. See the book More Exceptional C++ (Addison-Wesley, 2002) for the most current solution to this GotW issue. The solutions in the book have been revised and expanded since their initial appearance in GotW. The book versions also incorporate corrections, new material, and conformance to the final ANSI/ISO C++ standard.

Using auto_ptr
Difficulty: 5 / 10

This issue illustrates a common pitfall with using auto_ptr. What is the problem, and how can you solve it?

Problem

JG Question

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

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

Guru Question

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.

Solution

Problem: Arrays and auto_ptr Don't Mix

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

    template <class 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 behaviour, 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 that auto_ptr does

The problem with p2 is that auto_ptr is intended to contain only single objects, and so it always calls "delete" -- not "delete[]" -- on the pointer that it owns. This means that 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, but a more typical result is memory corruption soon followed by a core dump. To see this effect, try the following complete program on your favourite compiler:

    #include <iostream>
    #include <memory>
    #include <string>
    using namespace std;

    int c = 0;

    struct X {
        X() : s( "1234567890" ) { ++c; }
       ~X()                     { --c; }
        string s;
    };

    template <class 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 will 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 just crash right off.)

Non-Problem: Zero-Length Arrays Are Okay

What if f's parameter is zero (e.g., in the call f<int>(0))? Then the second new turns into "new T[0]" and often programmers will 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.]

"Well, 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 was forced to check the value of its n parameter before performing the "new T[n]" call.

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 of the ownership and helper-class semantics. This might only be tried 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:

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

b) NO SIGNIFICANT SPACE OR TIME OVERHEAD.

Disadvantages:

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

The idea is as follows: 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 <class 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; and since ~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!

Advantages:

a) 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:

a) HARD TO READ. This solution is rather verbose.

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

c) 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 client code accesses the contained array.

Having said all that: Even though the 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 that every programmer should know.

FINAL NOTE ON 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 EH 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 (EH) logic.

The idea is as follows: 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:

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

b) NO SIGNIFICANT SPACE OR TIME OVERHEAD.

Disadvantages:

a) 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 where "more processing" includes more branches, some of which end with "return;".

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

c) 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!

The idea is as follows: Instead of writing

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

we write

    vector<T> p2( n );

Advantages:

a) EASY TO IMPLEMENT (INITIALLY). We still don't need to write an auto_array.

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

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

d) NO SIGNIFICANT SPACE OR TIME OVERHEAD.

Disadvantages:

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

b) (SOMETIMES) USABILITY CHANGES. You can't instantiate any standard container (including a vector) of T's if T objects are not copy-constructible and assignable. Most types are both copy-constructible and assignable, but if they are not then this solution won't work.

Note that passing or returning a vector by value is much 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 wanted to get the same effect, you would simply pass a pointer or reference to the vector.

From the GotW coding standards:

- prefer using vector<> instead of built-in (C-style) arrays

 

Copyright © 2009 Herb Sutter