Skip to content
academia.sh

Lesson 10 / 15

Resource Cleanup Idioms

Measuring a resource that closes without ever reaching a reader, on the failure path of a function that opens several resources in sequence.

Published Updated

In this lesson

  • Builds a function whose resources must be closed in the reverse of the order they were opened
  • Measures that a resource left half-open on a failure path closes without ever reaching a reader
  • Names the difference between code that releases a resource and code that sees it
Contents

This lesson closes the series’ final measurement class: unobserved. In earlier lessons, a change either stayed with its writer (copy), became visible to everyone through a shared object (shared), or passed through a mediator and was read exactly once (handoff). Here is the fourth case: a resource is opened, and closed by a failure before it is filled or read — it never reaches any reader at all. What is measured is not where a value reached, but which resource disappeared without ever reaching anywhere.

Resources Opened in Order, Closed in Reverse Order

Store carries two separate allocations: an int array and a label string. Both are opened in sequence inside store_open; if one fails, everything opened up to that point is closed in reverse order — the one opened last is closed first.

// store.h — a struct carrying more than one resource
#ifndef STORE_H
#define STORE_H

struct Store {
    int *values;
    char *label;
    int element_count;
};

int store_open(struct Store *s, int element_count, const char *label);
void store_close(struct Store *s);
int *store_take_values(struct Store *s);

#endif
// store.c — sequential opening, reverse-order rollback
#include <stdlib.h>
#include <string.h>
#include "store.h"

int store_open(struct Store *s, int element_count, const char *label) {
    s->values = NULL;
    s->label = NULL;
    s->element_count = 0;

    s->values = malloc((size_t)element_count * sizeof *s->values);
    if (s->values == NULL) {
        return 0;
    }

    s->label = malloc(strlen(label) + 1);
    if (s->label == NULL) {
        free(s->values);
        s->values = NULL;
        return 0;
    }
    strcpy(s->label, label);

    s->element_count = element_count;
    return 1;
}

void store_close(struct Store *s) {
    free(s->label);
    free(s->values);
    s->label = NULL;
    s->values = NULL;
}

int *store_take_values(struct Store *s) {
    int *moved = s->values;
    s->values = NULL;
    s->element_count = 0;
    return moved;
}

Opening and closing these two resources gives the same answer to the previous course’s (Memory and Pointers in C, K02) question of “who releases it”: whoever calls store_open is also responsible for calling store_close. This lesson is not about that contract — it is about how to measure that, when store_open stops halfway, the label field was never written to at all.

A Resource Closing Without Ever Reaching a Reader

The expression (size_t)element_count * sizeof *s->values inside store_open wraps around to an enormous value when element_count is negative, since it converts to an unsigned type — no malloc can satisfy a size that large, and the call is guaranteed to return NULL. values cannot be allocated, label is never opened at all, and store_open returns 0.

// failed_open.c — the first resource fails to open, the second never opens at all
#include <stdio.h>
#include "store.h"

int main(void) {
    struct Store s;
    int result = store_open(&s, -1, "measurement");

    printf("%s %d\n", "store_open result", result);
    printf("%s %d\n", "is label NULL because it was never opened", (int)(s.label == NULL));
    printf("%s %d\n", "is values also NULL because it could not be allocated", (int)(s.values == NULL));

    store_close(&s);
    return 0;
}
store_open result 0
is label NULL because it was never opened 1
is values also NULL because it could not be allocated 1

DS12. The -1 given as element_count wraps to an enormous value once (size_t)element_count converts it to an unsigned type; multiplying that value by sizeof(int) asks malloc for a size no environment can satisfy — the call returns NULL, and store_open exits immediately in its first if branch. malloc is never even called for the label field; s.label stays at the NULL it was assigned at the top of the function. The label resource disappears — without reaching a reader, without carrying a value — without ever having existed. The class is unobserved: the copy, shared, and handoff classes from earlier lessons all assumed a value reached somewhere; here it reaches nowhere at all. store_close is still called regardless — free(NULL) is well-defined and harmless, so the cleanup call can always be made unconditionally.

Rolling Back the First Resource While the Second Fails to Open

Now suppose the first resource (values) opens successfully, but allocation for the second (label) fails — store_open would then have to roll the first one back too. To observe this directly (since a real malloc failure cannot be triggered in this environment), we reproduce store_open’s rollback branch as a separate function, store_open_second_fails — the same rollback pattern, with the second allocation point forced to fail.

// store_open_second_fails.c — rolling back the first resource while the second fails to open
#include <stdlib.h>
#include "store.h"

int store_open_second_fails(struct Store *s, int element_count) {
    s->values = NULL;
    s->label = NULL;
    s->element_count = 0;

    s->values = malloc((size_t)element_count * sizeof *s->values);
    if (s->values == NULL) {
        return 0;
    }

    /* the second allocation is deliberately treated as having failed here */
    free(s->values);
    s->values = NULL;
    return 0;
}
// second_rollback.c — the call where the first resource opened is rolled back
#include <stdio.h>
#include "store.h"

int store_open_second_fails(struct Store *s, int element_count);

int main(void) {
    struct Store s;
    int result = store_open_second_fails(&s, 4);

    printf("%s %d\n", "store_open_second_fails result", result);
    printf("%s %d\n", "is the first resource NULL because it was rolled back", (int)(s.values == NULL));
    printf("%s %d\n", "is the second resource NULL because it never opened", (int)(s.label == NULL));

    return 0;
}
store_open_second_fails result 0
is the first resource NULL because it was rolled back 1
is the second resource NULL because it never opened 1

DS13. values genuinely opens here (malloc succeeds for a 4-element array), but the moment the second resource’s opening is treated as having failed, it is rolled back with free(s->values). s.values being NULL shows that the resource opened and then closed without ever reaching a reader — much like a single reading of a handoff value in the Ring Buffers and Queues lesson, except here there is no reader at all; the value is produced and then destroyed within the same function. This pattern is exactly what the goto cleanup idiom, covered in the previous course’s Error Handling Patterns lesson, does: there, closing in reverse order from a single exit point was built with goto; here the same ordering was written by hand with explicit if/free calls. This lesson does not re-teach that idiom — it only measures the result it produces: the resource disappearing unobserved.

The Order of exit, _exit, and atexit

The rollback so far was built with hand-written if/free calls. C’s atexit function offers another route: it registers a handler to “run when the process terminates normally.” But this registration depends on how the process terminates — the difference between exit and _exit reveals another face of the unobserved class here.

// atexit_order.c — exit runs atexit handlers in the reverse of their registration order
#include <stdio.h>
#include <stdlib.h>

static void cleanup_a(void) {
    printf("%s\n", "cleanup_a ran");
}

static void cleanup_b(void) {
    printf("%s\n", "cleanup_b ran");
}

int main(void) {
    atexit(cleanup_a);
    atexit(cleanup_b);

    printf("%s\n", "main's body finished");
    exit(0);
}
main's body finished
cleanup_b ran
cleanup_a ran
// exit_and_atexit_skipped.c — _exit never runs atexit handlers at all
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static void never_runs(void) {
    printf("%s\n", "this line is never printed");
}

int main(void) {
    atexit(never_runs);

    printf("%s\n", "right before calling _exit");
    fflush(stdout);
    _exit(0);
}
right before calling _exit

DS18. In the first program, atexit(cleanup_a) is registered first and atexit(cleanup_b) second; when exit(0) is called, the two run in the reverse of their registration order — cleanup_b first, then cleanup_a. This repeats store_open‘s reverse-order rollback pattern (this lesson’s first section), now automated through atexit. In the second program, never_runs is likewise registered with atexit, but the process terminates not with exit but with _exit — POSIX’s _exit function closes the process immediately, without running any atexit handler at all. Because the registered handler is never called, "this line is never printed" is never printed; the output consists only of the line before _exit. This is the unobserved class at the function level: never_runs was registered to reach a reader (an output), but was never run at all. The fflush(stdout) line makes a separate point too: unlike exit, _exit does not flush buffered stdio output — without that line, the "right before calling _exit" line could have disappeared as well, because the buffer vanishes along with _exit without ever being flushed.

Handing Off a Resource to Another Variable

store_close always kept ownership of s.values inside the Store struct. Ownership can also be handed off to another variable: store_take_values detaches the array from s and hands it to the caller, leaving s.values at NULL — s can no longer reach that array; access has moved, through a single path, to the new owner.

// data_handoff.c — ownership of the array inside the store handed off to another variable
#include <stdio.h>
#include <stdlib.h>
#include "store.h"

int main(void) {
    struct Store s;
    store_open(&s, 3, "temporary");

    int *taken = store_take_values(&s);
    taken[0] = 10;

    printf("%s %d\n", "is store.values NULL after the handoff", (int)(s.values == NULL));
    printf("%s %d\n", "value written through taken", taken[0]);

    free(taken);
    store_close(&s);
    return 0;
}
is store.values NULL after the handoff 1
value written through taken 10

DS20. Before store_take_values is called, the only path to the array is s.values. After the call, moved (taken on the caller’s side) carries the array, and s->values is set to NULL — there are never two access paths at the same time; one closes exactly as the other opens. This differs from this lesson’s other unobserved examples: there, the resource disappeared without ever reaching any reader; here, the resource reaches exactly one reader (its new owner) and becomes unreachable through the old path — the class is handoff. After the handoff, the free(s.values) line inside the store_close(&s) call is safe: s.values is already NULL, and free(NULL) does nothing; responsibility for freeing the array now belongs to whatever code holds taken, which is exactly what the separate free(taken) call carries out.

The Bounding Measurement

The unobserved class measures not that a value reached the wrong place, but that it reached no place at all. The checks s.label == NULL and s.values == NULL are the proof of this: the resource was disabled without ever being seen by a reader, assigned, or copied. This is fundamentally different from the previous four lessons’ measurements — copy, shared, and handoff all said “a value reached somewhere, we are measuring how it got there”; here the question is “a resource disappeared without reaching anywhere, how do we verify that.” The verification tool stays the same: pointer comparison (== NULL), without printing an address or a value.

Summary

  • When several resources are opened in sequence, a failure path must roll them back in reverse order — the one opened last is closed first.
  • When a resource fails to open, that resource never reaches any reader (staying NULL is the measurable proof of this); when a second resource fails to open, the first one already opened is rolled back the same way. This pattern produces the same result as the goto cleanup idiom in the Error Handling Patterns lesson; this lesson does not teach that idiom, only measures its result.
  • The class is unobserved: what is measured is not where a value reached, but which resource disappeared without ever reaching anywhere.
  • Handlers registered with atexit run in the reverse of their registration order when exit is called; _exit closes the process without running any of them and without flushing buffered stdio output either.
  • When a resource’s ownership is handed off to another variable, the old path is set to NULL and the only access stays with the new owner — the class is handoff, different from unobserved in that the resource reaches exactly one reader.

Next Step

These five lessons measured the question “who sees this change” across four classes: copy, shared, handoff, unobserved. The next topic takes up these classes not one structure at a time but together — in composite cases where more than one data structure is used together inside the same function, and figuring out which change falls into which class has to be worked out by hand.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close