Exiting on malloc(3) Failure

Calls to malloc(3) Can Fail

To the annoyance of many students learning C programming, allocating memory by calling malloc(3) can fail. When it does, malloc(3) will return NULL. But, what should our program do when a call to malloc(3) fails? The question that today’s blog post explores is: when is it okay for our program to exit — that is, simply terminate — because of a memory-allocation failure?

To get started, let’s consider the following code, which might produce a segmentation fault (or “segfault” for short). This code might segfault because, if the call to malloc(3) fails, the attempt to write to the (non-)allocated memory will deference NULL. That is, it will attempt to deference the memory address 0x0:

int *i = malloc(sizeof(int));
*i = 123;  // i will be NULL if malloc fails
free(i);

Even if we use calloc(3) instead of malloc(3), as discussed in an earlier blog post, our code still has the same issue:

int *i = calloc(1, sizeof(int));
*i = 123;  // i will be NULL if calloc fails
free(i);

In order to deal with the possibility that the memory allocation fails, we need to check if the return value of calloc(3) is NULL, then take some appropriate action. For example, a function that uses calloc(3) could itself return an error value if the memory allocation fails:

#define ERROR_ALLOC_FAIL (-1)

int my_func(void)
{
    int *i = calloc(1, sizeof(int));
    if (i == NULL) {
        return ERROR_ALLOC_FAIL;
    }

    *i = 123;

    free(i);
    return 0;
}

In the above example, if the allocation inside my_func fails, my_func returns -1. That’s a perfectly reasonable behaviour for that function. But it doesn’t answer the larger question: what should our program, as a whole, do when an allocation fails? Can we just kill our program dead and call it a day?

A Graceful Approach

There are some applications where, if an attempt to allocate memory fails, we can gracefully try to allocate less memory. One example could be an image-editing application. Our attempt to allocate enough memory to hold a (potentially massive) high-resolution image might fail. In this case, we could warn the user, then attempt to allocate a smaller amount of memory to work with a reduced-resolution image.

There are also times where, if a memory allocation fails, we have no choice but to return an error value to the calling function. The classic example of this restriction is when we’re writing a function in a library that needs to allocate memory. A call to a library function should never cause the calling program to terminate. Instead, the library function has to indicate to the calling function that its memory allocation failed, to provide the calling function the opportunity to take appropriate action. After all, the program calling the library function might be something like an image-editing application, which can deal with an allocation failure gracefully.

Memory Necessary for Basic Functionality

However, there are times when an application we’re writing needs to allocate a specific amount of memory to function at all. Consider a command-line application that processes student records, which needs to allocate a linked list element for each student. Either the allocation succeeds and the program can run, or the allocation fails and the program can’t run. There’s presumably no graceful way to use less memory.

Let’s look at the following function that allocates a struct to hold a student record, and fills it with the provided data. For now, if the allocation fails, let’s have the function output an error message and return NULL:

struct student_record
{
    const char *name;
    int grade;
};

struct student_record *
create_record(const char *name, int grade)
{
    struct student_record *r =
        calloc(1, sizeof(struct student_record));
    if (r == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return NULL;
    }

    r->name = name;
    r->grade = grade;

    return r;
}

But, if our program needs to allocate a student record to work at all, what could a function that calls create_record possibly do if it gets a NULL return? It could propagate an error code all the way up through the call stack to main, but really nothing else. Consider a process_student_records function that calls create_record. If process_student_records gets a NULL return from create_record, it could propagate the error up the call stack by returning, for example, -1:

#define ERROR_ALLOC_FAIL (-1)

int process_student_records(void)
{
    struct student_record *r;
    const char *name;
    int grade;

    [...]

    r = create_record(name, grade);
    if (r == NULL) {
        return ERROR_ALLOC_FAIL;
    }

    [...]

    return 0;
}

While this approach does work, let’s talk about the deeper issue here. It’s not only the case that process_student_records can’t do anything meaningful with the NULL return from create_record, other than propagate the failure up the stack. It’s also the case that we need additional code in the process_student_records function — code that will probably never be tested — just to deal with the possibility that create_record could return NULL.

What would be far nicer, from a code-design perspective, is if create_record were guaranteed never to return NULL. In that case, process_student_records wouldn’t have to check if create_record failed:

// Never returns NULL
struct student_record *
create_record(const char *name, int grade);

int process_student_records(void)
{
    struct student_record *r;
    const char *name;
    int grade;

    [...]

    r = create_record(name, grade);
    // r guaranteed to be non-NULL

    [...]

    return 0;
}

Taking this idea one step further, process_student_records might not even have to return an int value representing success or failure. Provided there isn’t any other way — aside from create_record failing — that process_student_records could fail, it might be possible to make process_student_records a void function:

// Never returns NULL
struct student_record *
create_record(const char *name, int grade);

void process_student_records(void)
{
    struct student_record *r;
    const char *name;
    int grade;

    [...]

    r = create_record(name, grade);
    // r guaranteed to be non-NULL

    [...]

    // No return value
}

In this case, any function calling process_student_records also wouldn’t have to check for a return value indicating an error.

Just exit(3)

Let’s stop and ask: assuming that our application needs the allocation inside create_record to succeed in order to function at all, what would the purpose of propagating an error from create_record all the way up to main even be? Generally speaking, we propagate this sort of error up to main so that we can close all active resources (for example, free all allocated memory and close any open file descriptors) along the way, then terminate the program with a non-zero exit code.

But, there’s a single function that can do all of that for us: exit(3). This function, declared in stdlib.h, will close all open file descriptors, free all allocated memory, and terminate the program with a an error code of our choice. So, if allocation of memory that’s necessary for a program to function fails, we can just call exit(3) to instantly stop the program.

Here’s what our create_record function could look like if it calls exit(3) instead of returning NULL when calloc(3) fails. The argument to exit(3), EXIT_FAILURE, is defined in the C standard to be an exit status indicating unsuccessful program termination:

struct student_record *
create_record(const char *name, int grade)
{
    struct student_record *r =
        calloc(1, sizeof(struct student_record));
    if (r == NULL) {
        fprintf(stderr, "Allocation failed\n");
        exit(EXIT_FAILURE);
    }

    r->name = name;
    r->grade = grade;

    return r;
}

Having removed the NULL error return from create_record, opting instead to call exit(3), process_student_records is free to assume the return from create_record is non-NULL:

/*
 * Returns a newly created student record,
 * or exits if memory allocation fails.
 */
struct student_record *
create_record(const char *name, int grade);

void process_student_records(void)
{
    struct student_record *r;
    const char *name;
    int grade;

    [...]

    r = create_record(name, grade);
    // r guaranteed to be non-NULL

    [...]
}

With this change to create_record, our code in process_student_records is simplified: it’s easier to read, and it’s easier to properly test.

I should emphasize, though, that this approach of just calling exit(3) when memory allocation fails only makes sense if our program has no meaningful way to recover from an allocation failure. But, given how many programs simply rely on memory allocation to succeed, this situation is quite common. So consider, in your C code, whether just calling exit(3) when memory allocation fails is the correct approach.

Exercise

As an exercise for students, write a C macro that verifies the return value from a call to malloc(3) or calloc(3) is non-NULL. If the value is NULL, the macro should output an error message and terminate the program with exit(3). The ultimate goal of this exercise is to rewrite the following code:

int *i = calloc(1, sizeof(int));
if (i == NULL) {
    fprintf(stderr, "Allocation failed\n");
    exit(EXIT_FAILURE);
}

The improved version of the code should define a macro called VERIFY_ALLOC that will take a pointer argument, performs a NULL-check on it, then (if necessary) output an error message and exit(3) the program. That is, the following code should behave identically to the code above:

#define VERIFY_ALLOC [...write your macro here...]

int *i = calloc(1, sizeof(int));
VERIFY_ALLOC(i);

Remember to wrap the body of your VERIFY_ALLOC macro inside a do/while, as discussed in an earlier blog post.

For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.