GotW #10

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 Exceptional C++ (Addison-Wesley, 2000) for the most current solutions to GotW issues #1-30. 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.

Memory Management - Part II
Difficulty: 6 / 10

Are you thinking about doing your own class-specific memory management, or even replacing C++'s global new and delete? First try this problem on for size.

Problem

Here are excerpts from a program with classes that perform their own memory management. Point out as many memory- related errors as possible, and answer the additional questions.

    //  Why do B's operators delete have a second parameter,
    //  whereas D's do not?
    //
    class B {
    public:
        virtual ~B();
        void operator delete  ( void*, size_t ) throw();
        void operator delete[]( void*, size_t ) throw();
        void f( void*, size_t ) throw();
    };

    class D : public B {
    public:
        void operator delete  ( void* ) throw();
        void operator delete[]( void* ) throw();
    };

    void f()
    {
        //  Which operator delete is called for each of the
        //  following?  Why, and with what parameters?
        //
        D* pd1 = new D;
        delete pd1;

        B* pb1 = new D;
        delete pb1;

        D* pd2 = new D[10];
        delete[] pd2;

        B* pb2 = new D[10];
        delete[] pb2;

        //  Are the following two assignments legal?
        //
        B b;
        typedef void (B::*PMF)(void*, size_t);
        PMF p1 = &B::f;
        PMF p2 = &B::operator delete;
    }


    class X {
    public:
        void* operator new( size_t s, int )
                         throw( bad_alloc ) {
            return ::operator new( s );
        }
    };


    class SharedMemory {
    public:
        static void* Allocate( size_t s ) {
            return OsSpecificSharedMemAllocation( s );
        }
        static void  Deallocate( void* p, int i ) {
            OsSpecificSharedMemDeallocation( p, i );
        }
    };

    class Y {
    public:
        void* operator new( size_t s,
                            SharedMemory& m ) throw( bad_alloc ) {
            return m.Allocate( s );
        }

        void  operator delete( void* p,
                               SharedMemory& m,
                               int i ) throw() {
            m.Deallocate( p, i );
        }
    };


    void operator delete( void* p ) throw() {
        SharedMemory::Deallocate( p );
    }

    void operator delete( void* p,
                          std::nothrow_t& ) throw() {
        SharedMemory::Deallocate( p );
    }

Solution

    //  Why do B's operators delete have a second parameter,
    //  whereas D's do not?
    //
    class B {
    public:
        virtual ~B();
        void operator delete  ( void*, size_t ) throw();
        void operator delete[]( void*, size_t ) throw();
        void f( void*, size_t ) throw();
    };

    class D : public B {
    public:
        void operator delete  ( void* ) throw();
        void operator delete[]( void* ) throw();
    };

Answer: Preference. Both are usual deallocation functions, not placement deletes. (See 3.7.3.2/2)

However, both classes provide operators delete and delete[] without providing corresponding operators new and new[]. This is extremely dangerous. For example, consider what happens if a further-derived class provides its own operator new or new[].

    void f()
    {
        //  Which operator delete is called for each of the
        //  following?  Why, and with what parameters?
        //
        D* pd1 = new D;
        delete pd1;

Calls D::operator delete(void*).

        B* pb1 = new D;
        delete pb1;

Calls D::operator delete(void*). Since B's dtor is virtual, of course D's dtor is properly called, but the fact that B's dtor is virtual also implicitly means that D::operator delete() must be called, even though B::operator delete() is not (in fact, cannot) be virtual.

        D* pd2 = new D[10];
        delete[] pd2;

Calls D::operator delete[](void*).

        B* pb2 = new D[10];
        delete[] pb2;

Undefined behaviour. The language requires that the static type of the pointer that is passed to operator delete must be the same as its dynamic type. For more information on this topic, see also Scott Meyers' section on "Never Treat Arrays Polymorphically" in Effective C++ or More Effective C++.

        //  Are the following two assignments legal?
        //
        B b;
        typedef void (B::*PMF)(void*, size_t);
        PMF p1 = &B::f;
        PMF p2 = &B::operator delete;
    }

The first assignment is fine, but the second assignment is illegal because "void operator delete( void*, size_t ) throw()" is NOT a member function of B even though as written above it may look like one. The trick here is to remember that operators new and delete are always static, even if not explicitly declared static. It's a good habit to always declare them static, just to make sure that the fact is obvious to all programmers reading through your code.

    class X {
    public:
        void* operator new( size_t s, int )
                         throw( bad_alloc ) {
            return ::operator new( s );
        }
    };

This invites a memory leak since no corresponding placement delete exists. Similarly below:

    class SharedMemory {
    public:
        static void* Allocate( size_t s ) {
            return OsSpecificSharedMemAllocation( s );
        }
        static void  Deallocate( void* p, int i ) {
            OsSpecificSharedMemDeallocation( p, i );
        }
    };

    class Y {
    public:
        void* operator new( size_t s,
                            SharedMemory& m ) throw( bad_alloc ) {
            return m.Allocate( s );
        }

This invites a memory leak, since no operator delete matches this signature. If an exception is thrown during construction of an object to be located in memory allocated by this function, the memory will not be properly freed. For example:

        SharedMemory shared;
        ...
        new (shared) T; // if T::T() throws, memory is leaked

Further, the memory cannot safely be deleted since the class does not provide a usual operator delete. This means that a base or derived class's operator delete, or the global one, will have to try to deal with this deallocation (almost certainly unsuccessfully, unless you also replace all such surrounding operators delete, which would be onerous and evil).

        void  operator delete( void* p,
                               SharedMemory& m,
                               int i ) throw() {
            m.Deallocate( p, i );
        }
    };

This operator delete is useless since it can never be called.

    void operator delete( void* p ) throw() {
        SharedMemory::Deallocate( p );
    }

A serious error, since the replacement global operator delete is going to delete memory allocated by the default ::operator new, not by SharedMemory::Allocate(). The best you can hope for is a quick core dump. Evil.

    void operator delete( void* p,
                          std::nothrow_t& ) throw() {
        SharedMemory::Deallocate( p );
    }

Same here, but slightly more subtle. This replacement operator delete will only be called if a "new (nothrow) T" fails because T's ctor exits with an exception, and will try to deallocate memory not allocated by SharedMemory::Allocate(). Evil and insidious.

If you got all of these answers, then you're definitely on your way to becoming an expert in memory-management mechanics.

Copyright © 2009 Herb Sutter