Copyright © 2001 by Herb Sutter
In my previous column [1], I illustrated and justified the following coding guideline:
- Any class that provides its own class-specific operator new, or operator new[], should also provide corresponding class-specific versions of plain new, in-place new, and nothrow new. Doing otherwise can cause needless problems for people trying to use your class.
This time, well delve deeper into the question of what operator new failures mean, and how best to detect and handle them:
- Avoid using new(nothrow), and make sure that when youre checking for new failure youre really checking what you think youre checking.
The first part may be mildly surprising advice, but its the latter that is likely to raise even more eyebrows because on certain popular real-world platforms, memory allocation failures usually dont even manifest in the way the Standard says they must.
Like last time, for simplicity, Im not going to mention the array forms of new specifically. Whats said about the single-object forms applies correspondingly to the array forms.
Exceptions, Errors, and new(nothrow)
First, a recap of what we all know: whereas most forms of new report failure by throwing a bad_alloc exception, nothrow new reports failure the time-honored malloc way, namely by returning a null pointer. This guarantees that nothrow new will never throw, as indeed its name implies.
The question is whether this really buys us anything. Some people have had the mistaken idea that nothrow new enhances exception safety because it prevents some exceptions from occurring. So heres the $64,000 motivating question: does using nothrow new enhance program safety in general, or exception safety in particular? The (possibly surprising) answer is: no, not really. Error reporting and error handling are orthogonal issues. Its just syntax after all. [2]
The choice between throwing bad_alloc and returning null is just a choice between two equivalent ways of reporting an error. Therefore, detecting and handling the failure is just a choice between checking for an exception and checking for the null. To a calling program that checks for the error, the difference is just syntactic. That means that it can be written with exactly equivalent program safety, and exception safety, in either case the syntax by which the error happens to be detected isnt relevant to safety, because it is just syntax, and leads to only minor variations in the calling functions structure (e.g., something like if (null) { HandleError(); throw MyOwnException; } vs. something like catch(bad_alloc) { HandleError(); throw MyOwnException; }). Neither way of new error reporting provides additional information or additional inherent safety, and so neither way inherently makes programs somehow safer or able to be more correct, assuming of course that the handling is coded accurately.
But whats the difference to a calling program that doesnt check for errors? In that case, the only difference is that the eventual failure will be different in mode but not severity. Either an uncaught bad_alloc will unceremoniously unwind the program stack all the way back to main, or an unchecked null pointer will cause a memory violation and immediate halt when its later dereferenced. Both failure modes are fairly catastrophic, but theres some advantage to the uncaught exception: it will make an attempt to destroy at least some objects, and therefore release some resources, and if some of those objects are things like a TextEditor object that automatically saves recovery information when prematurely destroyed, then not all the program state need be lost if the program is carefully written. (Caveat: when memory really is exhausted, its harder than it appears to write code that will correctly unwind and back out, without trying to use a teensy bit more memory.) An abrupt halt due to use of a bad pointer, on the other hand, is far less likely to be healthy.
From this we derive Moral #1:
Moral #1: Avoid Nothrow New
Nothrow new does not inherently benefit program correctness or exception safety. For some failures, namely failures that are ignored, its worse than an exception because at least an exception would get a chance to do some recovery via unwinding. As pointed out in the previous column, if classes provide their own new but forget to provide nothrow new too, nothrow new will be hidden and wont even work. In most cases nothrow new offers no benefit, and for all these reasons it should be avoided.
I can think of two corner cases where nothrow new can be beneficial. The first case is one that these days is getting pretty hoary with age: when youre migrating a lot of legacy really-old-style C++ application code written before the mid-1990s that still assumes that (and checks whether) new returns null to report failure, then it can be easier to globally replace new with new (nothrow) in those files but its been a long time now since unadorned new behaved the old way! The amount of such hoary old code thats still sitting around and hasnt yet been migrated (or recompiled on a modern compiler) yet is dwindling fast. The second case for using nothrow new is if new is being used a lot in a time-critical function or inner loop, and the function is being compiled using a weaker compiler that generates inefficient exception handling code overhead, and this produces a measurable run-time difference in this time-critical function between using normal new and using nothrow new. Note that when I say measurably I mean that weve actually written a test harness that includes at least the entire piece of time-critical code (not just a toy example of new by itself), and timed two versions, one with new and one with new(nothrow). If after all that weve proved that it makes a difference to the performance of the time-critical code, we might consider new(nothrow) and should at the same time consider other ways to improve the allocation performance, including the option of writing a custom new using a fixed-size allocator or other fast memory arena [3].
This brings us to Moral #2:
Moral #2: Theres Often Little Point in Checking for new Failure Anyway
This statement might horrify some people. How can you suggest that we not check for new failure, or that checking failures is not important? some might say, righteously indignant. Checking failures is a cornerstone of robust programming! Thats very true in general, but alas it often isnt as meaningful for new, for reasons which are unique to memory allocation, as opposed to other kinds of operations whose failure should indeed be checked and handled.
Here are some reasons why checking for new failure isnt as important as one might think:
1) Checking new failure is useless on systems that dont commit memory until the memory is used.
On some operating systems, including specifically Linux [4], memory allocation always succeeds. Full stop. How can allocation always succeed, even when the requested memory really isnt available? The reason is that the allocation itself merely records a request for the memory; under the covers, the (physical or virtual) memory is not actually committed to the requesting process, with real backing store, until the memory is actually used. Even when the memory is used, often real (physical or virtual) memory is only actually committed as each page of the allocated buffer is touched, and so it can be that only the parts of the block that have actually been touched get committed.
Note that if new uses the operating systems facilities directly, then new will always succeed but any later innocent code like buf[100] = c; can throw or fail or halt. From a Standard C++ point of view, both effects are nonconforming, because the C++ Standard requires that if new cant commit enough memory it must fail (this doesnt), and that code like buf[100] = c shouldnt throw an exception or otherwise fail (this might).
Background: why do some operating systems do this kind of lazy allocation? Theres a noble and pragmatic idea behind this scheme, namely that a given process that requests memory might not actually immediately need all of said memory the process might never use all of it, or it might not use it right away and in the meantime maybe the memory can be usefully lent to a second process which may need it only briefly. Why immediately commit all the memory a process demands, when it may not really need it right away? So this scheme does have some potential advantages.
The main problem with this approach, besides that it makes C++ standards conformance difficult, is that it makes program correctness in general difficult, because any access to successfully-allocated dynamic memory might cause the program to halt. Thats just not good. If allocation fails up front, the program knows that theres not enough memory to complete an operation, and then the program has the choice of doing something about it, such as trying to allocate a smaller buffer size or giving up on only that particular operation, or at least attempting to clean up some things by unwinding the stack. But if theres no way to know whether the allocation really worked, then any attempt to read or write the memory may cause a halt and that halt cant be predicted, because it might happen on the first attempt to use part of the buffer, or on the millionth attempt after lots of successful operations have used other parts of the buffer.
On the surface, it would appear that our only way to defend against this is to immediately write to (or read from) the entire block of memory to force it to really exist. For example:
// Example 1: Manual initialization // // Deliberately go and touch each byte. // char* p = new char[1000000000]; memset( p, 0, 1000000000 );
If the type being allocated happens to be a non-POD [5] class type, the memory is in fact touched automatically for you:
// Example 2: Default initialization // // If T is a non-POD, this code initializes // all the T objects immediately and will // touch every (significant, non-padding) byte // T* p = new T[1000000000];
If T is a non-POD, each object is default-initialized, which means that all the significant bytes of each object are written to, and so the memory has to be accessed [6].
You might think thats helpful. Its not. Sure, if we successfully complete the memset in Example 1, or the new in Example 2, we do in fact know that the memory has really been allocated and committed. But if accessing the memory fails, the twist is that the failure wont be what we might naively expect it to be: we wont get a null return or a nice bad_alloc exception, but rather well get an access violation and a program halt, none of which the code can do anything about (unless it can use some platform-specific way to trap the violation). That may be marginally better and safer than just allocating without writing and pressing on regardless, hoping that the memory really will be there when we need it and that all will be well, but not by much.
This brings us back to the issue of standards conformance, because it may be possible for the compiler-supplied ::operator new and ::operator new[] themselves to do better with the above approach to working around operating system quirks than we as programmers could do easily. In particular, the compiler implementer may be able to use knowledge of the operating system to intercept the access violation and therefore prevent a program halt. That is, it may be possible for a C++ implementer to do all the above work: allocate, and then confirm the allocation by making sure each byte is written to, or at least to each page; catch any failure in a platform-specific way; and convert it to a standard bad_alloc exception (or a null return, in the case of a nothrow new). I doubt that any implementer would go to this trouble, though, for two reasons: first, it means a performance hit, and probably a big one to incur for all cases; and second, new failure is pretty rare in real life anyway... which happens to lead us nicely to the next point:
2) In the real world, new failure is a rare beast, made nearly extinct by the thrashing beast.
As a practical matter, many modern server-based programs rarely encounter memory exhaustion.
On a virtual memory system, most real-world server-based software performs work in various parts of memory while other processes are actively doing the same in their own parts of memory; this causes increasing paging as the amount of memory in use grows, and often the processes never reach new failure. Rather, long before memory can be fully exhausted, the system performance will hit the thrash wall and just grind ever more unusably slowly as pages of virtual memory are swapped in and out from disk, and the sysadmin will start killing processes.
I caveat this with words like most because it is possible to create a program that allocates more and more memory but doesnt actively use much of it. Thats possible, but unusual, at least in my own experience. This also of course does not apply to systems without virtual memory, such as many embedded and real-time systems; some of these are so failure-intolerant that they wont even use any kind of dynamic memory at all, never mind virtual memory.
3) Theres not always much you can do when you detect new failure.
As Andy Koenig pointed out in his 1996 article When Memory Runs Low, [7] the default behavior of halting the program on new failure (usually with at least an attempt to unwind the stack) is actually the best option in most situations, especially during testing.
Sometimes when new fails there are a few things you can do, of course: if you want to record some diagnostic info, the new handler is a nice hook for doing logging. It is sometimes possible to apply the strategy of keeping a reserve emergency memory buffer; although anyone who does this should know what they are doing, and actually carefully test the failure case on their target platforms, because this doesnt necessarily work the way people think. Finally, if memory really is exhausted, you cant necessarily rely on being able to throw a nontrivial (e.g., non-builtin) exception; even throw string("failed"); will usually attempt a dynamic allocation using new, depending on how highly optimized your implementation of string happens to be.
So yes, sometimes there are useful things you can do to cope with specific kinds of new failure, but often its not worth it beyond letting stack unwinding and the new handler (including perhaps some logging) do their thing.
What Should You Check?
There are special cases for which checking for memory exhaustion, and trying to recover from it, do make sense. Andy mentions some in his article referenced above. For example, you could choose to allocate (and if necessary write to) all the memory youre ever going to use up front, at the beginning of your program, and then manage it yourself; if your program crashes at all, it will crash right away before actually doing work. This approach requires extra work and is only an option if you know the memory requirements in advance.
The main category of recoverable new failure error Ive seen in production systems has to do with creating buffers whose size is externally supplied from some input. For example, consider a communications application where each transmitted packet is prepended with the packet length, and the first thing the receiver does with each packet is to read the length and then allocate a buffer big enough to store the rest of the packet. In just such situations, Ive seen attempts to allocate monstrously large buffers, especially when data stream corruption or programming errors cause the length bytes to get garbled. In this case, the application should be checking for this kind of corruption (and, better still, designing the protocol to prevent it from happening in the first place if possible) and aborting on invalid data and unreasonable buffer sizes, because the program can often continue doing something sensible, such as retrying the transmission with a smaller packet size or even just abandoning that particular operation and going on with other work. After all, the program is not really out of memory when an attempt to allocate 2 GB failed but theres still 1GB of virtual memory left [8]!
Another special case where new failure recovery can make sense is when your program optimistically tries to allocate a huge working buffer, and on failure just keeps retrying a smaller one until it gets something that fits. This assumes that the program as a whole is designed to adjust to the actual buffer size and does chunking as necessary.
Summary
Avoid using nothrow new, because it offers no significant advantages these days and usually has worse failure characteristics than plain throwing new. Remember that theres often little point in checking for new failure anyway, for several reasons. If you are rightly concerned about memory exhaustion then be sure that youre checking what you think youre checking, because: checking new failure is typically useless on systems that dont commit memory until the memory is used; in virtual memory systems new failure is encountered rarely or never because long before virtual memory can be exhausted the system typically thrashes and a sysadmin begins to kill processes; and, except for special cases, even when you detect new failure theres not always much you can do if there really is no memory left.
Notes and References
[1] Herb Sutter. To New, Perchance to Throw, Part 1 of 2, C/C++ Users Journal, March 2001.
[2] Can be sung to the tune of Its a Small World (After All).
[3] See also Herb Sutter, Containers in Memory: How Big is Big? C/C++ Users Journal, January 2001 and Herb Sutter, Exceptional C++ (Addison-Wesley, 2000).
[4] This is what Ive been told by Linux folks in a discussion about this on comp.std.c++. Lazy-commit is also a configurable feature on some other operating systems.
[5] POD stands for plain old data. Informally, a POD means any type thats just a bundle of plain data, though possibly with user-defined member functions just for convenience. More formally, a POD is a class or union that has no user-defined constructor or copy assignment operator or destructor, and no (non-static) data member that is a reference, pointer to member, or non-POD.
[6] This ignores the pathological case of a T whose constructor doesnt actually initialize the objects data.
[7] Andrew Koenig. When Memory Runs Low, C++ Report, June 1996.
[8] Interestingly, allocating buffers whose size is externally specified is a classic security vulnerability. Attacks by malicious users or programs specifically trying to cause buffer problems is a classic, and still favorite, security exploit to bring down a system. Note that trying to crash the program by causing allocation to fail is a denial-of-service attack, not an attempt to actually gain access to the system; the related, but distinct, overrun-a-fixed-length-buffer attack is also a perennial favorite in the hacker community, and its amazing just how many people still use strcpy and other unchecked calls and thereby leave themselves wide open to this sort of abuse.
Herb Sutter ([email protected]) is chief technology officer of PeerDirect Inc. and the architect of their heterogeneous database replication products. He is secretary of the ISO/ANSI C++ standards committees, a member of the ANSI SQL committee, and author of the book Exceptional C++ (Addison-Wesley).