Let me start today’s post with a question. Can you spot the issue in the following code snippet? (Yes, I know, it’s a bit artificial, but it is inspired by a lot of real code I have seen.)
void foo(struct S *ptr)
{
assert(ptr);
ptr->a = new int[100];
ptr->b = new int[100];
}
void bar()
{
struct S s;
try {
foo(&s);
} catch (std::bad_alloc &ba) {
// foo failed to allocate memory
return;
}
// continue processing
}
Do you see the issue?
It’s not a C++ problem, but C++ exceptions make it possible. The answer is: there is a potential memory leak.
Everyone has always told me how amazing exceptions are, but I always found them counter intuitive. I’ve used several languages that have you manage the memory, and so I am rather familiar with the following construct (I’m using C for this, but the same idea exists in other languages):
ptr = malloc(sizeof(xyz));
if (!ptr) {
/* allocation failed */
}
Everyone has always told me that with exceptions, you can do a bunch of things in the try block and then figure out what exactly went wrong in one of the catch blocks. In other words, something like:
try {
ptr = new xyz;
// more processing
} catch (std::bad_alloc &ba) {
// oops, failed to allocate memory
} catch (whatever &w) {
// handle 'whatever' exception
}
That all sounds great, but what if you need to allocate two objects? When you catch a bad_alloc exception, how do you know which of the allocations failed? Sure, you could initialize it to NULL, and then check for it in the catch block. It becomes a bigger issue when the function using new does no exception handling and instead relies on the caller to take care of it. (Java handles this better by forcing you to either catch the exception, or marking the function with an annotation indicating that it may throw a specific exception. Interestingly enough, C++ lets you annotate functions to tell the compiler whether or not it will throw an exception, but that’s a topic for another time.) Since the caller does not have access to the callee’s stack frame, it has no way of determining whether or not there was a successful allocation in addition to the failed one. Again, this is not an issue with the language, but rather with the view that exceptions are magic that makes memory management simple.
Recently, I mentioned this issue to someone who uses C++ on daily basis, and he suggested that this could be fixed by having the function doing the allocation (in my example foo) use auto_ptr instead of raw pointers. That means, that the if execution leaves the scope whatever was allocated gets destroyed. This compilicates the implementation of foo — now, we need two auto_ptrs to store the actual pointers to the integer arrays, and if we managed to allocate both, we can set the raw pointers in the structure and everything is fine and dandy. If there was an allocation failure, we exit the scope because of the exception — the auto_ptrs will destroy things for us.
Finally, I would like to address those of you that are about to tell me that if memory allocation failed, then we are doomed anyway and possibly leaking some other memory is not a big deal. In some software, that is certainly true. However, there is a huge amount of code that can (and should) continue to function even in the presence of such failure. For example, suppose you are writing a web server. A client connects, you try to allocate some sort of structure to keep track of per-client state. The allocation fails. Does it mean that the whole web server process is doomed? No. Just this one client as far as we know. We should clean up, and then close the network connection. That one client may end up unhappy, but there’s nothing we could do for it. Other clients that already happen to have all the memory they need, can continue on their merry way. Additionally, memory may become available in the future — a memory hog may exit freeing up a lot of memory.