How should I write ISO C++ Standard conformant custom new and delete operators?
Asked Answered
C

4

72

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

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.
Cao answered 25/8, 2011 at 16:43 Comment(20)
wow, people are getting their downvotes in early! - I'm guessing you haven't even finished asking your question yet? I think this is a good place to discuss such issues, +1 from me.Lumpy
@Lumpy The OP has finished, as he is now busy writing a long answer. The small text states "based on learning's from Scott Meyer's More Effective C++" (note the two spelling typos, including the name of Scott Meyers).Abelmosk
@Als Looks like there are some people that don't like you very much :-) I personally don't like rambling answers like this one, I feel it belongs in a dedicated FAQ section somewhere instead of being lost among the thousands of questions that get posted to SO every day. But +1 for the effort.Prowess
@Praetorian: It is a followup FAQ entry. We have an excellent Operator Overloading FAQ, but this topic is so vast that it warranted an special entry in itself.Cao
@Als I didn't mean the FAQ tag, but a dedicated section of the website, kinda like the Parashift C++ FAQ. But since we don't have that, this is the next best thing.Prowess
@Praetorian: there is somewhat of a dedicated section: c++-faq, which is linked from the about c++ tag page.Decennium
Is this question actually frequently asked?Alkmaar
I think "Frequently Asked Questions" might as well include "Answers That Are More Useful To Know Than You Ever Realised, When You Did Related Work Frequently"Octuple
@James McNellis: We have an excellent FAQ from Sbi, on Operator overloading & it omitted this aspect for the reason that it needs a separate mention. This tries to make that FAQ complete.Cao
But is this question frequently asked? If not, then while I have no objection to the question being asked and answered here, it should not have the [c++-faq] tag. The tag is already too noisy.Alkmaar
Actually I'd agree with that. c++-faq is not for every self-answered book-style Q&A that a regular user can think of.Octuple
Nice article, but please relax on the typography! One level of decoration at a time -- and can you do with fewer backticks? The whole thing is a bit hard on the eye. I'm happy to edit a bit if you like.Okeechobee
@Praetorian: Why don't you follow the link to the question on meta that started all this, read through the discussions, and propose such a section for SO in a question on meta? Or drop in the chat and discuss this for a few days. If you read through the discussions on meta you will see that, when we started, we didn't know what shape this FAQ thing should take, and hijacking the c++-faq tag was just the first-best crutch that came along, meant to be a temporary solution until we found a better one. I'd be very glad if some new ideas on how to do this would come up and could be discussed.Valoniah
@Kerrek SB: Please feel free to, Actually, it was in one of our discussions that I realized the existing operator overloading FAQ didn't cover this.Cao
Is there some reason that you couldn't just make this a single answer instead of 5? Also, why isn't it community Wiki?Superfluity
@Nicol Bolas: SO limits number of characters in an answer, So all of them couldn't just fit in one answer, Hence separate answers. This was meant to be an entry to the C++-Faq to complete the Operator Overloading Faq that is already in existence, This is not a community wiki because it was just a single person(me) who formulated the answer for the Faq, To be frank most of the users downvoted it without even botherig to read it. Given that this has received 10 Downvotes(no reasons?), I gather the community doesn't like it, & in a way I regret putting in the efforts to come up with this.Cao
@Als: The purpose of the community wiki is more than just who writes the answer. It's also about more people being able to correct errors, who gets up/down rep votes (ie: nobody), etc.Superfluity
@Nicol Bolas: Point is nobody is reading this. Leave aside the evaluation of what is wrong or correct in it.And the entries added as contibution have never been hostorically marked Community Wiki. You may drop in the C++ Loung room sometime to discuss the philosophy behind the C++ FAQ and the folks who came up with the idea to start it in first place.Cao
@Nicol: That used to be the reason for introducing CW, however, it's no longer useful for that, because everybody could step in and edit, even those with really low rep. IMO, the only real use of CW nowadays is for the system to flip the switch on questions that gain too much attention (defined roughly as "more than a dozen answers"), so that the worst cases of the voting system's inherent weakness (voting is handed out by popularity, rather than correctness) doesn't wreak as much havoc.Valoniah
A comment on operator new vs. operator new[] and the nothrow-operator new would also be helpful.Seoul
C
42

Part I

This C++ FAQ entry explained why one might want to overload new and delete 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 operator

The C++ standard (§18.4.1.1) defines operator new as:

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

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 allocation function attempts to allocate the requested amount of storage. If it is successful, it shall return the address of the start of a block of storage whose length in bytes shall be at least as large as the requested size...

The standard further imposes:

...The pointer returned shall be suitably aligned so that it can be converted to a pointer of any complete object type and then used to access the object or array in the storage allocated (until the storage is explicitly deallocated by a call to a corresponding deallocation function). Even if the size of the space requested is zero, the request can fail. If the request succeeds, the value returned shall be a non-null pointer value (4.10) p0 different from any previously returned value p1, unless that value p1 was sub-sequently passed to an operator delete.

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 type std::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:

If set_new_handler has been used to define a new_handler function, this new_handler function is called by the standard default definition of operator new if it cannot allocate the requested storage by its own.

To understand how our custom new needs to support this requirement, we should understand:

What is the new_handler and set_new_handler?

new_handler is a typedef for a pointer to a function that takes and returns nothing, and set_new_handler is a function that takes and returns a new_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:

#include <iostream>
#include <cstdlib>

// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
    std::cerr << "Unable to satisfy request for memory\n";

    std::abort();
}

int main()
{
    //set the new_handler
    std::set_new_handler(outOfMemHandler);

    //Request huge memory size, that will cause ::operator new to fail
    int *pBigDataArray = new int[100000000L];

    return 0;
}

In the above example, operator new (most likely) will be unable to allocate space for 100,000,000 integers, and the function outOfMemHandler() 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 the new-handler function repeatedly until it can find enough memory or there is no more new handlers. In the above example, unless we call std::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


Cao answered 25/8, 2011 at 16:43 Comment(4)
Personally I would save the result of std::set_new_handler. Then my version of new handler would call the old version if my version failed to provide any emergency space. This way if another library has installed a new handler that will be called as expected by that library.Navelwort
Are you sure new is in namespace std?Okeechobee
100,000,000 * 4 bytes = 400,000,000 bytes / 1024 = 390625 KiB / 1024 = ~381.47 MiB. Most likely will not fail on anything you could view this webpage on :)Violaceous
Note: 10 years later and the links are now dead. I have a proposed edit that updated some of them, but your iedone.com solutions no longer are found and I have no idea what they were originally to fix it.Honeysweet
C
24

Part II

... continued

Given the behavior of operator new from the example, a well designed new_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 by operator new, but will propagate to the site originating the request for memory.

Not return: By calling abort or exit.

To implement an class-specific new_handler we have to provide a class with its own versions of set_new_handler and operator new. The class's set_new_handler allows clients to specify the new-handler for the class (exactly like the standard set_new_handlerallows clients to specify the global new-handler). The class's operator 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 is null does operator new throw an exception.

As promised, the citation from the Standard:
Section 3.7.4.1.3:

An allocation function that fails to allocate storage can invoke the currently installed new_handler(18.4.2.2), if any. [Note: A program-supplied allocation function can obtain the address of the currently installed new_handler using the set_new_handler function (18.4.2.3).] If an allocation function declared with an empty exception-specification (15.4), throw(), fails to allocate storage, it shall return a null pointer. Any other allocation function that fails to allocate storage shall only indicate failure by throw-ing an exception of class std::bad_alloc (18.4.2.1) or a class derived from std::bad_alloc.

Armed with the #4 requirements, let us attempt the pseudo code for our new operator:

void * operator new(std::size_t size) throw(std::bad_alloc)
{  
   // custom operator new might take additional params(3.7.3.1.1)

    using namespace std;                 
    if (size == 0)                     // handle 0-byte requests
    {                     
        size = 1;                      // by treating them as
    }                                  // 1-byte requests

    while (true) 
    {
        //attempt to allocate size bytes;

        //if (the allocation was successful)

        //return (a pointer to the memory);

        //allocation was unsuccessful; find out what the current new-handling function is (see below)
        new_handler globalHandler = set_new_handler(0);

        set_new_handler(globalHandler);


        if (globalHandler)             //If new_hander is registered call it
             (*globalHandler)();
        else 
             throw std::bad_alloc();   //No handler is registered throw an exception

    }

}

Continuation 2

Cao answered 25/8, 2011 at 16:44 Comment(18)
I fail to see what "Deinstall the new-handler" has to do with an answer to "How should I write a C++ new operator".Abelmosk
@Sjoerd: You will need to read the answer to know how it is organized.Cao
Please quote where in the standard it is stated that a user supplied new operator has to call the new-handler. If it is not a requirement on an user defined operator new, what is it doing in this (far too long) answer?!Abelmosk
@Sjoerd: All the citations from the Standard are present in there you will need to read to understand it.Cao
Your references are to the C++98 standard, not the current C++11 standard.Abelmosk
@Sjoerd: As of this writing, the current standard is still C++03. But if you want one from the C++11 approved draft, the paragraph number is the same.Decennium
Quote: "An allocation function that fails to allocate storage can invoke the currently installed new-handler function (18.6.2.3), if any." note the "CAN". Not a requirement.Abelmosk
@Sjoerd: C++11, is not an standard yet, not officially atleast. So the official standard at the moment is still C++03. I won't mind adding the relevant C++11 quotes as I track them.Cao
@Sjoerd: "Our operator new should try to allocate memory more than once (...)". Also note the "SHOULD". Not a requirement.Decennium
The new standard has been approved, it will be published in a couple of weeks. Do you really want to rewrite all the references in this post in two weeks time? Src: herbsutter.com/2011/08/12/…Abelmosk
@Sjoerd: The FDIS was approved. It is not a standard until it is published. When Herb says "it is now C++11", he was lying. All we have is the C++0x FDIS, which is identical in content to what will be the C++11 standard in a few weeks.Octuple
And my point stands: Calling the new-handler is not a requirement on a user-supplied operator new function, so it should not be part of an answer to the question. Just list the 4 requirement of the first part of the answer, and a conforming implementation as short as possible. Could be half a page, at most.Abelmosk
@Sjoerd: Please quote where in the answer it is stated that a user supplied new operator has to call the new-handler. If it is not written in the answer, what is it doing in your (far too long) comment complaints?!Valoniah
@Valoniah The question is about an user defined standard compliant operator new. If the new_handler does not have to be called, why is it in the answer at all? It only distracts.Abelmosk
@Valoniah I don't see why you SHOULD call the new_handler. of course, one COULD call it, but one could also call a lot of other functions. And that argument has been the same since my first comment, so I don't see where I shifted goalpost. Strawman argument, you didn't have anything better? The answer is now too long to be useful, and I was looking for ways to shorten it to make it more useful. But I have stated my opinion enough times here, and you still fail to see my point so it's pointless to continue. And BTW, calling me a troll is offensive and rude (and flagged as such).Abelmosk
@Sjoerd: Your issues ranged from "why talk about deinstalling the new-handler" via "a user supplied new operator does not have to call the new-handler" to "which is the current C++ standard". All your arguments in these have been shown to be false by Als and several bystanders. Now you come back with that you don't see why a new-handler should be called - something you had complained about before.Valoniah
However, I agree to your question to the extend that Als could make it clearer why he thinks a user-defined new operator should fall back onto a new handler. Had you asked this immediately and politely, we would have discussed this, rather than bickering with you, and would be a lot further by now.Valoniah
As for the length of the answer: Why don't you suggest how to break it into smaller, independent parts? That might indeed be more useful.Valoniah
C
20

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 inside new operator will never terminate.

A caveat: Note that the standard (§3.7.4.1.3, quoted above) does not explicitly say that the overloaded new 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.

class Base
{
    public:
        static void * operator new(std::size_t size) throw(std::bad_alloc);
};

class Derived: public Base
{
   //Derived doesn't declare operator new
};

int main()
{
    // This calls Base::operator new!
    Derived *p = new Derived;

    return 0;
}

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 our operator new to handle this situation is to divert such calls requesting the "wrong" amount of memory to the standard operator new, like this:

void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
    if (size != sizeof(Base))          // If size is "wrong,", that is, != sizeof Base class
    {
         return ::operator new(size);  // Let std::new handle this request
    }
    else
    {
         //Our implementation
    }
}

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.

Cao answered 25/8, 2011 at 16:45 Comment(0)
C
18

Implementing a custom delete operator

The C++ Standard(§18.4.1.1) library defines operator delete as:

void operator delete(void*) throw();

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 be void*. A custom delete 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:

The value of the first argument supplied to one of the deallocation functions provided in the standard library may be a null pointer value; if so, the call to the deallocation function has no effect. Otherwise, the value supplied to operator delete(void*) in the standard library shall be one of the values returned by a previous invocation of either operator new(size_t) or operator new(size_t, const std::nothrow_t&) in the standard library, and the value supplied to operator delete[](void*) in the standard library shall be one of the values returned by a previous invocation of either operator new[](size_t) or operator new[](size_t, const std::nothrow_t&) in the standard library.

Requirement #3: If the pointer being passed is not null, then the delete operator should deallocate the dynamic memory allocated and assigned to the pointer.

Citation from C++ Standard: Section §3.7.3.2.4:

If the argument given to a deallocation function in the standard library is a pointer that is not the null pointer value (4.10), the deallocation function shall deallocate the storage referenced by the pointer, render-ing invalid all pointers referring to any part of the deallocated storage.

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:

class Base
{
    public:
        //Same as before
        static void * operator new(std::size_t size) throw(std::bad_alloc);
        //delete declaration
        static void operator delete(void *rawMemory, std::size_t size) throw();

        void Base::operator delete(void *rawMemory, std::size_t size) throw()
        {
            if (rawMemory == 0)
            {
                return;                            // No-Op is null pointer
            }

            if (size != sizeof(Base))
            {
                // if size is "wrong,"
                ::operator delete(rawMemory);      //Delegate to std::delete
                return;
            }
            //If we reach here means we have correct sized pointer for deallocation
            //deallocate the memory pointed to by rawMemory;

            return;
        }
};
Cao answered 25/8, 2011 at 18:15 Comment(1)
I read this whole post for the "deallocate the memory pointed to by rawMemory" part... should I use free and just assume that the default operator new used malloc (or whatever) ?Fretwell

© 2022 - 2024 — McMap. All rights reserved.