My Rant on C++'s operator new

by David Mazières

Abstract

Memory allocation in ANSI C

void *malloc (size_t); void free (void *);

void f (void) { struct sss *s = malloc (sizeof (*s)); char *string2 = malloc (strlen (string1) + 1); /* ... */ free (s); free (string2); }

The simplicity of malloc's syntax offers many benefits. Though malloc is defined as part of the C language, it can be implemented as an ordinary library function requiring no special support from the compiler. Thus, anyone can define additional malloc-like functions. Many programs define a function called xmalloc that aborts execution if the system runs out of memory. People also write special purpose memory allocators, such as arenas, which are ordinary C functions returning type void *.

Another great advantage of malloc's syntax is that malloc can be defined as a preprocessor macro. This technique is invaluable for tracking down memory allocation bugs such as memory leaks. Debugging malloc libraries like Gray Watson's dmalloc library can define malloc and free as follows:

void *malloc (size_t); void *_malloc_leap (const char *file, int line, size_t size); #define malloc(size) _malloc_leap(__FILE__, __LINE__, size) void free (void *); void _free_leap (const char *file, int line, void *); #define free(ptr) _free_leap (__FILE__, __LINE__, ptr);

With malloc defined in this way, the dmalloc library can record the source file and line number at which most pieces of memory are allocated. This doesn't work in every possible situation. The program may, for instance, assign malloc to a function pointer. In that case, the function malloc may get called rather than _malloc_leap. Nonetheless, the code still compiles and runs fine. (malloc as a function pointer has no arguments and consequently does not get expanded by the preprocessor). Any code can also call _malloc_leap directly. For instance, if a memory leak is tracked to the strdup function, strdup could be reimplemented as follows:

static inline char * _strdup_leap (const char *file, int line, char *str) { return strcpy (_malloc_leap (file, line, strlen (str) + 1), str); } #define strdup(str) _strdup_leap (__FILE__, __LINE__, str)

Of course, there is one drawback to malloc's simplicity--it is not type safe. A small typo, such as

struct sss *s = malloc (sizeof (s));

struct sss *s = malloc (sizeof (*s));

Memory allocation in C++

Why did they eliminate implicit casts from void *? Such implicit casts were probably viewed as a hole in the type system. In general, Stroustrup deprecates any use void * for something other than "low-level software," because he feels that with C++'s protection mechanisms all pointers should be typed. More importantly, in C, if p1 and p2 are pointers, the two statements

p1 = p2; p1 = (void *) p2;

Thus, no ordinary function can perform the role of malloc in C++--there is no suitable return type. More importantly, even, C++ classes have constructors and destructors that must be called when the objects are allocated and freed. The C++ compiler must be aware of memory allocation so as to ensure that proper constructors are called for a new object, and to create a pointer of the appropriate type. Thus, C++ has a special syntax for allocating and freeing memory. It uses the new and delete operators, with the following syntax:

[::] "new" ["(" expression-list ")"] {new-type-id | "(" type-id ")"} ["(" expression-list ")"]

[::] "delete" ["[]"] cast-expression

class foo { /* ... */ }; void f () { foo *foop = new foo; char *string = new char[20]; int *intp = new int (7); // ... delete intp; delete[] string; delete foop; }

void *operator new (size_t); void operator delete (void *); void *operator new[] (size_t); // for arays void operator delete[] (void *);

struct nothrow_t {}; extern const nothrow_t nothrow; void *operator new throw() (size_t, const nothrow_t&);

foo *fp = new (nothrow) foo; if (!fp) { /* Deal with being out of memory */ }

inline void * operator new(size_t, void *p) { return p; }

In addition to overloaded global new functions, each class can have its own set of operator new and delete functions. Note, however, that operator delete cannot be overloaded. There is at most one operator delete and one operator delete[] per class. While the global ::operator delete and delete[] take one argument of type void *, per-class operators can optionally take a second argument of type size_t. This second argument will contain the size of the deleted chunk of memory.

delete vs. delete[] and free

Why are operator delete and delete[] different? First, given a pointer of type foo *, the compiler has no way of knowing if it points to one foo or an array of them--in short, the compiler doesn't know which memory allocator was used to allocate the memory pointed to by foo. Second, when the elements of an array have destructors, the compiler must remember the size of the array so as to destroy every element. Even when array elements don't have destructors, the per-class operator delete functions may rely on being told the exact size of what is being freed. For non-array types, the compiler can reconstruct the size from the type of the pointer being freed (in the case of deleting a supertype, the compiler gets the actual size from the virtual function table if there is a virtual destructor). Thus, when allocating an array, the compiler must sometimes stash its length somewhere; operator delete[] retrieves the length to perform the necessary destruction and deallocation.

The global operator delete and delete[] functions cannot take a second size_t argument. For types without destructors, then, the global operators new and new[] essentially perform the same function. They are usually both just wrappers around malloc. However, the compiler may treat memory allocated by the various allocators differently even when it doesn't need to. Thus, any memory allocated with new[] must be freed with delete[], memory allocated with new freed with delete, and memory allocated with malloc freed with free.

The most obvious way of dynamically allocating n bytes of memory is to call new char[n]. Unfortunately, memory allocated this way must subsequently be freed with delete[], which may lead to inconsistent or counter-intuitive function interfaces. Consider the following C code:

#include <stddef.h> typedef struct intlist { int size; int i[1]; } intlist; intlist * makeintlist (int size) { intlist *ilp = malloc (offsetof (intlist, i[size])); /* not C++ */ ilp->size = size; return ilp; }

intlist *ilp = new intlist; intlist *ilp = (intlist *) malloc (offsetof (intlist, i[size])); intlist *ilp = (intlist *) new char[offsetof (intlist, i[size])];

In either case 2 or 3, the person invoking the function will have to remember how to free the object--with either free or delete[]. It might provide a more intuitive interface if the user could instead free such an object with the simple scalar ::operator delete. In this case, the memory should be allocated through an explicit call to ::operator new:

intlist *ilp = (intlist *) operator new (offsetof (intlist, i[size]));

Specialized allocators

class arena { /* ... */ arena (const arena &); // No copying arena &operator= (const arena &); // No copying public: arena (); ~arena (); void *allocate (size_t bytes, size_t alignment) { /* ... */ } }; inline void * operator new (arena &a, size_t bytes) { return a.allocate (bytes, sizeof (double)); } inline void * operator new[] (arena &a, size_t bytes) // This function is bad news { return a.allocate (bytes, sizeof (double)); } class foo { /* ... */ }; void f () { arena a; char *string = new (a) char[80]; // opreator new[] (arena &, size_t) int *intp = new (a) int; // opreator new (arena &, size_t) foo *fp = new (a) foo; // better hope foo::~foo() isn't useful /* No need to free anything before returning */ }

Though this may seem nice on the surface, there are some fairly ugly aspects to this use of new. Consider what happens during "new (a) foo". Two functions are invoked: the arena allocator--operator new (arena &, size_t)--and foo's default constructor, foo::foo(). Operator new does not know what type is being allocated, while foo::foo() doesn't know what memory allocator is being used. If, for instance, foo::foo() dynamically allocates memory, it cannot do so from the arena in which it resides itself. Thus, the speed benefit of an arena allocation may be overshadowed by several heap allocations within foo::foo(). Moreover, foo's destructor must be called before freeing the arena so as to avoid a memory leak.

C++ does allow explicit calls to destructors, so function f can be fixed with:

void f () { arena a; // ... foo *fp = new (a) foo; // must be destroyed // ... fp->~foo (); }

void f () { arena a; // ... foo *fp = new (a) foo (a); // Tell foo about our arena // foo no longer needs its destructor invoked }

A potential problem may arise if the constructor for foo throws an exception. Because constructors have no return values in C++, exceptions are the simplest way for them to indicate failure. When constructors throw exceptions, the compiler will try to free the memory allocated by new. While this may ordinarily be a good thing, it would have disastrous consequences in the case of the arena allocator, where calling ::operator delete on a pointer in the middle of the arena would have unpredictable results. One can avoid this problem through so-called "placement-delete" functions that, after the first argument, match the argument types of particular placement new operators. For the arena type, we should therefore declare an empty placement delete:

inline void operator delete (void *, arena &) {}

arena a; foo *fp = new (a) foo; operator delete (fp, a); // Problematic

inline void operator delete (void *, void *) {}

Another annoyance of operator new is that operator new[] (arena &, size_t) doesn't know what kind of objects it is allocating. If it did, it might be able to relax the alignment requirements on arrays of certain objects. Worse yet, as we will see in the next section, it is almost certainly incorrect to use the arena operator new[] for any class with a destructor, and probably incorrect to use the arena operator new[] at all.

Dangers of overloading ::operator new[]

inline void * operator new[](size_t, void *place) { return place; }

obj * f () { void *p = special_malloc (sizeof (obj[10])); return new (p) obj[10]; // Serious trouble... }

struct sizenew_t {}; const sizenew_t sizenew; inline void * operator new[] (size_t size, const sizenew_t &, size_t *sizep) throw () // Don't let NULL return throw an exception { *sizep = size; return NULL; // Don't let compiler call constructors } obj * f () { size_t size; (void) new (sizenew, &size) obj[10]; void *p = special_malloc (size); return new (p) obj[10]; }

If placement operator new[] can't work, what about the arena operator new above? It certainly was convenient and intuitive to say "new (a) char[80]" for 80 bytes of space. Does this create a problem? In fact, the arena new[] above will work with g++, but will cause a memory leak on other compilers. According to The C++-FAQ-Lite:

[16.13] After p = new Fred[n], how does the compiler know there are n objects to be destructed during delete[] p? Short answer: Magic. Long answer: The run-time system stores the number of objects, n, somewhere where it can be retrieved if you only know the pointer, p. There are two popluar techniques that do this. Both these techniques are in use by commercial grade compilers, both have tradeoffs, and neither is perfect. These techniques are: Over-allocate the array and put n just to the left of the first Fred object[33.5].

Use an associative array with p as the key and n as the value[33.6].

For that reason, operator new[] (size_t, arena &) should probably not exist. Instead of writing

char *string = new (a) char [80];

char *string = (char *) a.allocate (80, 1);

Punching a hole in the type system

class voidp { void *p; public: voidp (void *pp) : p (pp) {} template<class T> operator T *() { return (T *) p; } };

inline voidp mymalloc (size_t size) { return malloc (size); } #define malloc mymalloc

Of course, as in C, such memory allocation is not type safe. Moreover, the memory allocation process is entirely separate from the invocation of constructors. Thus, you certainly can't call malloc to allocate any types with virtual functions. It might, however, be reasonable for a simple arena allocator used only on basic types to return voidp.

Note that as of version 2.8.1, g++ will not compile voidp correctly. However, the code does work with egcs.

Debugging memory allocation in C++

The short answer is no. Not for a general C++ program. However, the benefit of such a tool is so great that it is worth shaping a project around a plan for effectively debugging memory problems. If one restricts oneself to particular coding conventions, it is possible in C++ to reap some of the benefits of a C debugging malloc.

Here is a simple attempt at using a debugging malloc in C++:

void *_malloc_leap (const char *file, int line, size_t size); void _free_leap (const char *file, int line, void *); struct dmalloc_t {}; extern struct dmalloc_t dmalloc; inline void * operator new (size_t size, dmalloc_t, const char *file, int line) { return _malloc_leap (file, line, size); } inline void * operator new[] (size_t size, dmalloc_t, const char *file, int line) { return _malloc_leap (file, line, size); } inline void operator delete (void *p) { _free_leap ("unknown file", 0, p); } #define NEW new (dmalloc, __FILE__, __LINE__) #define new NEW

void f () { int *intp = new int; delete intp; }

void f () { int *intp = new (dmalloc, "foo.C", 31) int; delete intp; }

class bar { /* ... */ public: void *opreator new (size_t); void operator delete (void *, size_t); /* ... */ }; void f () { bar *bp = new bar; delete bp; }

void f () { bar *bp = new (dmalloc, "bar.C", 23) bar; delete bp; }

#ifdef DMALLOC #define delete ::delete #endif /* DMALLOC */ /* ... */ class bar { /* ... */ public: #ifndef DMALLOC void *opreator new (size_t); void operator delete (void *, size_t); #endif /* !DMALLOC */ /* ... */ };

Class-specific allocators are a pain when debugging, but they are manageable. Unfortunately, arena allocation brings on some far more serious problems. Recall we were allocating objects like this:

void f () { arena a; foo *fp = new (a) foo; fp->~foo (); }

void f () { arena a; foo *fp = new (dmalloc, "arena_test.C", 14) (a) foo; fp->~foo (); }

Changing most uses of new to NEW is problematic, particularly when a project regularly imports lots of code that uses doesn't follow the same convention. Changing the syntax of specialized allocations is incredibly tricky, however. It's not enough simply to write

foo *fp = arena_new (a) foo;

Don't use operator new

(I say almost because there are two exceptions: First, because C++ has no varargs templates, new-like functions in C++ must set some fixed, though arbitrarily large, maximum number of constructor arguments. Second, the compiler's memory allocation functions have the benefit of knowing whether a type has a destructor or not. This allows some space and time optimization in certain circumstances.)

Our goal is then to define memory allocators other than new and delete. Let us call these functions New, Delete, and Delarray. They will behave as follows:

void f () { int *intp = New (int) (7); foo *fp = New (foo); char *string = New (char) [20]; Delete (foo); Delete (intp); DelArray (string); }

There will have to be a few syntactic differences between New and new. New's type argument must be surrounded by parentheses, whereas new only requires parentheses around complex types (those containing parentheses themselves). Non-constant array dimentions will have to go outside the parentheses containing the type. New also won't have any placement syntax, which is fine since it also has no special support from the compiler--other allocators can simply go by other names. Delete and Delarray are similar to delete and delete[], but their arguments must also be in parentheses.

Before actually implementing New, we need a few pieces of infrastructure. One of the big advantages the compiler's new has over a user-defined one is that the compiler knows what types have destructors. Thus, for instance, when allocating an array of chars the compiler doesn't need to remember its length. While the language provides no way of knowing for sure if a type has a destructor, we can approximate the problem using templates:

#include <new> #include <cstdlib> #define size_t unsigned int // Work around specialization bug in egcs /* * isTypeSimple(T) - returns true only if T has no constructor * or destructor. By default only built in * types are simple, but more can be declared * simple with SIMPLETYPE(T) */ template<class T> struct SimpleType { enum {value = 0}; }; template<class T> struct SimpleType<T *> { enum {value = 1}; }; template<class T> struct SimpleType<const T> { enum {value = SimpleType<T>::value}; }; template<class T, size_t n> struct SimpleType<T[n]> { enum {value = SimpleType<T>::value}; }; #define SIMPLETYPE(T) \ template<> struct SimpleType<T> { \ union { \ T t; \ } catcherr; \ enum {value = 1}; \ } SIMPLETYPE (bool); SIMPLETYPE (char); SIMPLETYPE (unsigned char); SIMPLETYPE (short); SIMPLETYPE (unsigned short); SIMPLETYPE (int); SIMPLETYPE (unsigned int); SIMPLETYPE (long); SIMPLETYPE (unsigned long); SIMPLETYPE (float); SIMPLETYPE (double); SIMPLETYPE (long double); SIMPLETYPE (long long); SIMPLETYPE (unsigned long long); #define isTypeSimple(T) ((bool) SimpleType<T>::value) template<class T, bool S = isTypeSimple (T)> struct EnsureSimple; template<class T> struct EnsureSimple<T, true> {};