How should I write ISO C++ standard conformant custom new
and delete
operators?
This is in continuation of Overloading new and delete in the immensely illuminating C++ FAQ, Operator overloading, and its follow-up, Why should one replace default new and delete operators?
Section 1: Writing a standard-conformant new
operator
- Part 1: Understanding the requirements for writing a custom
new
operator - Part 2: Understanding the
new_handler
requirements - Part 3: Understanding specific scenario requirements
Section 2: Writing a standard-conformant delete
operator
(Note: This is meant to be an entry to Stack Overflow's C++ FAQ. If you want to critique the idea of providing an FAQ in this form, then the posting on meta that started all this would be the place to do that. Answers to that question are monitored in the C++ chatroom, where the FAQ idea started out in the first place, so your answer is very likely to get read by those who came up with the idea.)
Note: The answer is based on learnings from Scott Meyers' More Effective C++ and the ISO C++ Standard.
Part I
This C++ FAQ entry explained why one might want to overload
new
anddelete
operators for one's own class. This present FAQ tries to explain how one does so in a standard-conforming way.Implementing a custom
new
operatorThe C++ standard (§18.4.1.1) defines
operator new
as:The C++ standard specifies the semantics that custom versions of these operators have to obey in §3.7.3 and §18.4.1
Let us summarize the requirements.
Requirement #1: It should dynamically allocate at least
size
bytes of memory and return a pointer to the allocated memory. Quote from the C++ standard, section 3.7.4.1.3:The standard further imposes:
This gives us further important requirements:
Requirement #2: The memory allocation function we use (usually
malloc()
or some other custom allocator) should return a suitably aligned pointer to the allocated memory, which can be converted to a pointer of an complete object type and used to access the object.Requirement #3: Our custom operator
new
must return a legitimate pointer even when zero bytes are requested.One of the evident requirements that can even be inferred from
new
prototype is:Requirement #4: If
new
cannot allocate dynamic memory of the requested size, then it should throw an exception of typestd::bad_alloc
.But! There is more to that than what meets the eye: If you take a closer look at the
new
operator documentation (citation from standard follows further down), it states:To understand how our custom
new
needs to support this requirement, we should understand:What is the
new_handler
andset_new_handler
?new_handler
is a typedef for a pointer to a function that takes and returns nothing, andset_new_handler
is a function that takes and returns anew_handler
.set_new_handler
's parameter is a pointer to the function operator new should call if it can't allocate the requested memory. Its return value is a pointer to the previously registered handler function, or null if there was no previous handler.An opportune moment for an code sample to make things clear:
In the above example,
operator new
(most likely) will be unable to allocate space for 100,000,000 integers, and the functionoutOfMemHandler()
will be called, and the program will abort after issuing an error message.It is important to note here that when
operator new
is unable to fulfill a memory request, it calls thenew-handler
function repeatedly until it can find enough memory or there is no more new handlers. In the above example, unless we callstd::abort()
,outOfMemHandler()
would be called repeatedly. Therefore, the handler should either ensure that the next allocation succeeds, or register another handler, or register no handler, or not return (i.e. terminate the program). If there is no new handler and the allocation fails, the operator will throw an exception.Continuation 1
Part II
... continued
Given the behavior of
operator new
from the example, a well designednew_handler
must do one of the following:Make more memory available: This may allows the next memory allocation attempt inside operator new's loop to succeed. One way to implement this is to allocate a large block of memory at program start-up, then release it for use in the program the first time the new-handler is invoked.
Install a different new-handler: If the current new-handler can't make any more memory available, and of there is another new-handler that can, then the current new-handler can install the other new-handler in its place (by calling
set_new_handler
). The next time operator new calls the new-handler function, it will get the one most recently installed.(A variation on this theme is for a new-handler to modify its own behavior, so the next time it's invoked, it does something different. One way to achieve this is to have the new-handler modify static, namespace-specific, or global data that affects the new-handler's behavior.)
Uninstall the new-handler: This is done by passing a null pointer to
set_new_handler
. With no new-handler installed,operator new
will throw an exception ((convertible to)std::bad_alloc
) when memory allocation is unsuccessful.Throw an exception convertible to
std::bad_alloc
. Such exceptions are not be caught byoperator new
, but will propagate to the site originating the request for memory.Not return: By calling
abort
orexit
.To implement an class-specific
new_handler
we have to provide a class with its own versions ofset_new_handler
andoperator new
. The class'sset_new_handler
allows clients to specify the new-handler for the class (exactly like the standardset_new_handler
allows clients to specify the global new-handler). The class'soperator new
ensures that the class-specific new-handler is used in place of the global new-handler when memory for class objects is allocated.Now that we understand
new_handler
&set_new_handler
better we are able to modify the Requirement #4 suitably as:Requirement #4 (Enhanced):
Our
operator new
should try to allocate memory more than once, calling the new-handling function after each failure. The assumption here is that the new-handling function might be able to do something to free up some memory. Only when the pointer to the new-handling function isnull
doesoperator new
throw an exception.As promised, the citation from the Standard:
Section 3.7.4.1.3:
Armed with the #4 requirements, let us attempt the pseudo code for our
new operator
:Continuation 2
Part III
... continued
Note that we cannot get the new handler function pointer directly, we have to call
set_new_handler
to find out what it is. This is crude but effective, at least for single-threaded code. In a multithreaded environment, probably some kind of lock to safely manipulate the (global) data structures behind the new-handling function will be needed. (More citation/details are welcome on this.)Also, we have an infinite loop and the only way out of the loop is for memory to be successfully allocated, or for the new-handling function to do one of the things we inferred before. Unless the
new_handler
does one of those things, this loop insidenew
operator will never terminate.A caveat: Note that the standard (
§3.7.4.1.3
, quoted above) does not explicitly say that the overloadednew
operator must implement an infinite loop, but it merely says that such is the default behaviour.So this detail is open to interpretation, but most of the compilers (GCC and Microsoft Visual C++) do implement this loop functionality (you can compile the code samples provided earlier).Also, since an C++ authory such as Scott Meyers suggests this approach, it is reasonable enough.Special scenarios
Let us consider the following scenario.
As this FAQ, explains, a common reason for writing a custom memory manager is to optimize allocation for objects of a specific class, not for a class or any of its derived classes, which basically means that our operator new for the Base class is typically tuned for objects of size
sizeof(Base)
-nothing larger and nothing smaller.In the above sample, because of inheritance the derived class
Derived
inherits the new operator of the Base class. This makes calling operator new in a base class to allocate memory for an object of a derived class possible. The best way for ouroperator new
to handle this situation is to divert such calls requesting the "wrong" amount of memory to the standard operator new, like this:Note that, the check for size also incoprporates our requirement #3. This is because all freestanding objects have a non-zero size in C++, so
sizeof(Base)
can never be zero, so if size is zero, the request will be forwarded to::operator new
, and it is gauranteed that it will handle it in standard compliant way.Citation: From the creator of C++ himself, Dr Bjarne Stroustrup.
Implementing a custom delete operator
The C++ Standard(
§18.4.1.1
) library definesoperator delete
as:Let us repeat the exercise of gathering the requirements for writing our custom
operator delete
:Requirement #1: It shall return
void
and its first parameter shall bevoid*
. A customdelete operator
can have more than one parameter as well but well we just need one parameter to pass the pointer pointing to the allocated memory.Citation from the C++ Standard:
Section §3.7.3.2.2:
"Each deallocation function shall return void and its first parameter shall be void*. A deallocation function can have more than one parameter....."
Requirement #2: It should guarantee that it is safe to delete a null pointer passed as an argument.
Citation from C++ Standard: Section §3.7.3.2.3:
Requirement #3: If the pointer being passed is not
null
, then thedelete operator
should deallocate the dynamic memory allocated and assigned to the pointer.Citation from C++ Standard: Section §3.7.3.2.4:
Requirement #4: Also, since our class-specific operator new forwards requests of the "wrong" size to
::operator new
, We MUST forward "wrongly sized" deletion requests to::operator delete
.So based on the requirements we summarized above here is an standard conformant pseudo code for a custom
delete operator
: