http://anadoxin.org/blog

C++: Shooting yourself in the foot #2

Sun, 11 February 2018 :: #cpp :: #rant

Hi,

There are people in this world who are forced to use C++ against their will. Those people often defend themselves by writing C code, and by compiling it with a C++ compiler. Then hope that noone will notice.

There are a lot of problems with this, and this short note will be about one such problem.

Imagine you need to allocate some chunk of memory. Normally you would use std::vector class, because it will allocate the memory automatically, and will release the memory automatically when it's not used anymore. When there is a good reason to manually allocate memory, we would normally use the new keyword, because it works with types, and supports allocation of arrays. But the C programmer chooses malloc(3), because that's what he was using in C.

So, if the C programmer finds out that the memory block allocated previously is too small, he needs to reallocate the memory into a bigger block. When using std::vector, we would only need to call vec.resize(n) method and that's all (well, checking for out of memory exceptions would be helpful). But C programmer rejects C++'s bloat and because he's using malloc(3), he needs also to use realloc(3), thus he writes some code:

void* ptr = malloc(128);
if(!ptr) {
    printf("out of memory during alloc\n");
    return false;
}

// ...

ptr = realloc(ptr, 256);
if(!ptr) {
    printf("out of memory during realloc\n");
    return false;
}

So, the memory gets reallocated and if there's some error during reallocation, the program will handle the error and everything's fine.

Except, we just have introduced a memory leak. The realloc(3) function will return nullptr (or NULL) when reallocation will fail, but will not free the original block. So, by reusing the ptr variable to hold realloc(3)'s return value, we have destroyed the only pointer to the old memory block, while the memory is still allocated.

If realloc() returns a null pointer and errno has been set to [ENOMEM], the memory referenced by ptr shall not be changed.

The "correct" way of using realloc(3) is a little bit more complicated, mostly because of old code, which could use realloc(ptr, 0) to free the memory block. In order to be backwards-compatible, the standards group has introduced a kind of special case for realloc(3).

If realloc(3) returns nullptr, it can also set errno to ENOMEM to indicate the memory block in question has not been freed. If the errno is something else than ENOMEM when realloc(3) returns nullptr, this could mean the block was freed, but really it would be implementation-specific. Good thing is that modern systems seem to always return ENOMEM when realloc(3) returns nullptr, at least it seems like it.

So the realloc(3) pattern should look more or less like this:

void* ptr = malloc(128);
if(!ptr) ...

void* nptr = realloc(ptr, 256);
if(!nptr) {
    if(errno == ENOMEM) {
        free(ptr);
    }

    printf("out of memory during realloc\n");
    return false;
} else {
    ptr = nptr;
}

However, for the C programmer it would be more beneficial to use reallocf(3bsd) instead of realloc(3), but only if your platform supports this (Visual Studio doesn't, but Linux, BSD and macOS should support it). The reallocf(3bsd) function will actually free the memory block even if reallocation fails, so it will behave just as the C programmer expects the realloc(3) function to behave.

This way we don't introduce any memory leaks. Or we can just use std::vector, because the pattern for handling realloc(3) just got a little bit more complicated, and you know, we're using C++ and it's year 2018. Or simply because it's easier.

G.