Document number: N2158=07-0018

Howard E. Hinnant
2006-11-13

LWG Issue 206: Linking new/delete operators

Contents

Introduction

This paper addresses the default implementation of the following 8 standard library functions:

void* operator new     (std::size_t size)                        throw(std::bad_alloc);
void  operator delete  (void* ptr)                               throw();

void* operator new     (std::size_t size, const std::nothrow_t&) throw();
void  operator delete  (void* ptr,        const std::nothrow_t&) throw();

void* operator new   [](std::size_t size)                        throw(std::bad_alloc);
void  operator delete[](void* ptr)                               throw();

void* operator new   [](std::size_t size, const std::nothrow_t&) throw();
void  operator delete[](void* ptr,        const std::nothrow_t&) throw();

Note that all 8 of the above functions are replaceable. And if all 8 are replaced, the requirements on those replacements are clear. What is less clear is the requirements and behavior if only a subset of the above 8 functions are replaced.

Why do we care if only a subset of the 8 functions are replaced? Can't we just mandate that either none or all 8 of the above functions must be replaced?

Yes, we could do that. In fact that is effectively what we have done since 1998. It isn't working. A common bug is:

I've replaced sort with stable_sort as an internal implementation detail in my library. Why are a few of my clients now complaining that my library is causing memory corruption in their applications?

The answer to this seemingly bizarre bug report is that stable_sort typically indirectly calls new(nothrow) followed by delete, while sort typically doesn't. When the client replaces operator new/delete but not the nothrow variants, stable_sort crashes whereas sort doesn't.

This is just evil.

This isn't a idle/rare occurrence. I am writing this paper today in response to yet another customer of mine coming to me with a complaint of this nature. My official answer is that there is a bug in the customer's code: He must replace all eight of these signatures, even if he only wants to replace two. However, my honest answer is that there is a bug in the standard.

This all centers on what the default implementations of these functions are required to do. That default implementation is very visible to clients, and incorrectly specified in C++03, and the current (N2135) working draft.

What's Reasonable, Pass 1

In the discussion that follows, when I refer to an operator being replaced, I implicitly assume that the operator no longer directly references whatever memory pool the default operator references (which is typically malloc/free). For simplicity of discussion, the default operators reference the default memory pool, and client-replaced operators reference a distinct client-defined memory pool.

It appears to me that if you replace:

void* operator new(std::size_t size) throw(std::bad_alloc);

Then you must also replace:

void  operator delete(void* ptr) throw();

I think most people would immediately agree with this statement. If one doesn't link these two operators to the same underlying memory pool, then the delete will not be able to handle pointers that come from new. Furthermore I believe the same statement goes for the other 6 operators. Thus we can immediately separate these 8 functions into 4 cliques of 2 functions each: If you replace one function in a clique you must replace both:

CliqueOperators
1
void* operator new     (std::size_t size)                        throw(std::bad_alloc);
void  operator delete  (void* ptr       )                        throw();
2
void* operator new     (std::size_t size, const std::nothrow_t&) throw();
void  operator delete  (void* ptr,        const std::nothrow_t&) throw();
3
void* operator new   [](std::size_t size)                        throw(std::bad_alloc);
void  operator delete[](void* ptr       )                        throw();
4
void* operator new   [](std::size_t size, const std::nothrow_t&) throw();
void  operator delete[](void* ptr,        const std::nothrow_t&) throw();

But now the next question is:

What happens if I replace clique 1 and not clique 2? Or what happens if I replace clique 1 and not clique 3? etc.

What's Reasonable, Pass 2

To answer the above question we need to look more closely at the requirements. For example the requirements for both the nothrow and ordinary (but non-array) versions of operator delete are:

Requires: the value of ptr is null or the value returned by an earlier call to the default operator new(std:: size_t) or operator new(std::size_t,const std::nothrow_t&).

Ok, so we have a problem right here: These words say that if you replace cliques 1 and 2, then your replaced operator deletes must handle pointers allocated from the default operator new!

That can't be right. Surely we mean that if someone replaces cliques 1 and 2 that the delete operators in those cliques need only handle pointers from the replaced new operators in those cliques.

What did that Requires: clause intend to say?

To find out, let's look at an example use case of operator new(nothrow):

A* ap = new(std::nothrow) A;
...
delete ap;

Right! Clients of new(std::nothrow) are expected to be able to delete those pointers with a simple (non-nothrow) delete. And more generally, the delete operators in cliques 1 and 2 are expected to be able to delete the pointers from the new allocators in cliques 1 and 2. And similarly for cliques 3 and 4.

This means that cliques 1 and 2 really form a single group of functions that all refer to the same underlying pool of memory. Similarly for cliques 3 and 4. We can call these two groups A and B.

GroupCliqueOperators
A
1
void* operator new     (std::size_t size)                        throw(std::bad_alloc);
void  operator delete  (void* ptr       )                        throw();
2
void* operator new     (std::size_t size, const std::nothrow_t&) throw();
void  operator delete  (void* ptr,        const std::nothrow_t&) throw();
B
3
void* operator new   [](std::size_t size)                        throw(std::bad_alloc);
void  operator delete[](void* ptr       )                        throw();
4
void* operator new   [](std::size_t size, const std::nothrow_t&) throw();
void  operator delete[](void* ptr,        const std::nothrow_t&) throw();

However the above table does not imply that one must implement every signature in a group. It only implies that each signature in a group must refer to the same underlying memory pool.

Is there anything the standard can do to make it easy for functions in the same group to refer to the same memory pool, and difficult for them not to?

Yes! Consider this proposed default implementation of the signatures in clique 2:

void* operator new(std::size_t size, const std::nothrow_t&) throw()  // clique 2
{
    try
    {
        return operator new(size);                        // forward to clique 1
    }
    catch (...)
    {
    }
    return 0;
}

void  operator delete(void* ptr, const std::nothrow_t&) throw()  // clique 2
{
    operator delete(ptr);                             // forward to clique 1
}

With the above default implementation, all the client has to do to replace group A is to just replace the two signatures in clique 1. Since clique 2 forwards to clique 1 (replaced or not), then clique 2 is always linked to clique 1 as it should be. The client can still replace clique 2 if desired. However there is little point in doing so. It must remain linked to clique 1, and not doing so will result in a run time error when delete is called with a pointer from new(nothrow).

There is no advantage in easily allowing clique 2 to become unlinked from clique 1. And there is every advantage in actively discouraging cliques 1 and 2 from becoming unlinked. They must be linked or a run time crash is inevitable.

Everything I've said about the relationship between cliques 1 and 2 also applies to cliques 3 and 4. Cliques 3 and 4 must always be linked. That implies that the proper default implementation of clique 4 simply forwards to clique 3:

void* operator new[](std::size_t size, const std::nothrow_t&) throw()  // clique 4
{
    try
    {
        return operator new[](size);                        // forward to clique 3
    }
    catch (...)
    {
    }
    return 0;
}

void  operator delete(void* ptr, const std::nothrow_t&) throw()  // clique 4
{
    operator delete[](ptr);                           // forward to clique 3
}

The above is the bare minimum the standard must do in order to keep the standard library from being extremely fragile.

Linkage Between Groups A and B

We have now established firm reasoning for why clique 2 should forward to clique 1, and clique 4 should forward to clique 3. But should we (by default) link groups A and B by having clique 3 forward to clique 1? I.e. implement the array operators in terms of the non-array operators.

I believe that this is the question actually originally answered in LWG 206. Note that the actual question of LWG 206 was the issue of linking the nothrow and throwing versions of the operators as discussed in the previous sections. They must not become unlinked. However the same is not true of array vs non-array.

The reason groups A and B do not have to be linked is because the delete operators in group A never have to deal with pointers from the new operators in group B. Similarly the delete operators in group B never have to deal with pointers from the new operators in group A. Thus if the groups refer to different underlying memory pools (become unlinked), no harm (as in crashing) is done.

History of Linkage Between Groups A and B

The original 1998 and 2003 C++ standards partially link groups A and B. The default operator new[] is specified to call operator new. However the operator delete[] does not have the corresponding specification to call operator delete. LWG 298 corrects that inconsistency by specifying operator delete[] calls operator delete. Thus the current (N2135) working draft links groups A and B.

Current Practice of Linkage Between Groups A and B

The following test was run on various compilers and platforms. It detects whether groups A and B are linked or not by replacing the non-array operators, and then calling the array operators and observing if the replaced operators are called or not.

#include <cstdio>
#include <cstdlib>
#include <new>

void* operator new(std::size_t size) throw(std::bad_alloc)
{
    std::printf("custom allocation\n");
    if (size == 0)
        size = 1;
    void*p = std::malloc(size);
    if (p == 0)
        throw std::bad_alloc();
    return p;
}

void  operator delete(void* ptr) throw()
{
    std::printf("custom deallocation\n");
    std::free(ptr);
}

int main()
{
    std::printf("begin main\n");
    int* i = new int;
    delete i;
    std::printf("---\n");
    int* a = new int[3];
    delete [] a;
    std::printf("end main\n");
}

The following tools indicated complete linkage between groups A and B:

The following tools indicated no linkage between groups A and B:

The following tool indicated inconsistent linkage between groups A and B:

Recommendation for Linkage Between Groups A and B

Given that the current (N2135) working draft and several (prominent) compilers currently link groups A and B, this paper recommends to not change the current (N2135) working draft (with respect to A/B linkage) and thus continue linking groups A and B. The default implementation of the clique 3 operators should forward to clique 1:

void* operator new[](std::size_t size) throw(std::bad_alloc)  // clique 3
{
    return operator new(size);                     // forward to clique 1
}

void  operator delete[](void* ptr) throw()  // clique 3
{
    operator delete(ptr);        // forward to clique 1
}

With the above recommendations, clients will be able to replace both groups A and B by simply replacing clique 1. Indeed, this is the most common intention of replacing the new and delete operators. If clients wish to treat array allocations differently from non-array allocations, the client can still safely and easily unlink groups A and B by replacing both cliques 1 and 3, and have them refer to different memory pools. There is little motivation for replacing cliques 2 or 4, but of course that is still allowed by this proposal as long as the replacements continue to link to cliques 1 and 3 respectively.

Proposed Wording

Proposed wording to accomplish the recommendations of this paper is provided below. The differences are with respect to the current (N2135) working draft.

Change 18.5.1.1 [new.delete.single]:

void* operator new(std::size_t size, const std::nothrow_t&) throw();

-5- Effects: Same as above, except that it is called by a placement version of a new-expression when a C++ program prefers a null pointer result as an error indication, instead of a bad_alloc exception.

-6- Replaceable: a C++ program may define a function with this function signature that displaces the default version defined by the C++ Standard library.

-7- Required behavior: Return a non-null pointer to suitably aligned storage (3.7.4), or else return a null pointer. This nothrow version of operator new returns a pointer obtained as if acquired from the (possibly replaced) ordinary version. This requirement is binding on a replacement version of this function.

-8- Default behavior:

-9- [Example:

T* p1 = new T;                 // throws bad_alloc if it fails
T* p2 = new(nothrow) T;        // returns 0 if it fails

--end example]

void operator delete(void* ptr) throw();
void operator delete(void* ptr, const std::nothrow_t&) throw();

-10- Effects: The deallocation function (3.7.4.2) called by a delete-expression to render the value of ptr invalid.

-11- Replaceable: a C++ program may define a function with this function signature that displaces the default version defined by the C++ Standard library.

-12- Requires: the value of ptr is null or the value returned by an earlier call to the default (possibly replaced) operator new(std::size_t) or operator new(std::size_t, const std::nothrow_t&).

-13- Default behavior:

-14- Remarks: It is unspecified under what conditions part or all of such reclaimed storage is allocated by a subsequent call to operator new or any of calloc, malloc, or realloc, declared in <cstdlib>.

void operator delete(void* ptr, const std::nothrow_t&) throw();

-15- Effects: Same as above, except that it is called by the implementation when an exception propagates from a nothrow placement version of the new-expression (i.e. when the constructor throws an exception).

-16- Replaceable: a C++ program may define a function with this function signature that displaces the default version defined by the C++ Standard library.

-17- Requires: the value of ptr is null or the value returned by an earlier call to the (possibly replaced) operator new(std::size_t) or operator new(std::size_t, const std::nothrow_t&).

-18- Default behavior: Calls operator delete(ptr).

Change 18.5.1.2 [new.delete.array]

void* operator new[](std::size_t size, const std::nothrow_t&) throw();

-5- Effects: Same as above, except that it is called by a placement version of a new-expression when a C++ program prefers a null pointer result as an error indication, instead of a bad_alloc exception.

-6- Replaceable: a C++ program can define a function with this function signature that displaces the default version defined by the C++ Standard library.

-7- Required behavior: Same as for operator new(std::size_t, const std::nothrow_t&). This nothrow version of operator new[] returns a pointer obtained as if acquired from the ordinary version. Return a non-null pointer to suitably aligned storage (3.7.4), or else return a null pointer. This nothrow version of operator new returns a pointer obtained as if acquired from the (possibly replaced) operator new[](std::size_t size). This requirement is binding on a replacement version of this function.

-8- Default behavior: Returns operator new(size, nothrow).

void operator delete[](void* ptr) throw(); 
void operator delete[](void* ptr, const std::nothrow_t&) throw();

-9- Effects: The deallocation function (3.7.4.2) called by the array form of a delete-expression to render the value of ptr invalid.

-10- Replaceable: a C++ program can define a function with this function signature that displaces the default version defined by the C++ Standard library.

-11- Requires: the value of ptr is null or the value returned by an earlier call to operator new[](std::size_t) or operator new[](std::size_t, const std::nothrow_t&).

-12- Default behavior: Calls operator delete(ptr) or operator delete[](ptr, std::nothrow) respectively.

Acknowledgments

I would like to thank David Harmon, Rahtgaz, and Alexey Sarytchev for help in surveying existing practice.