June 15, 2025·5 min read·engineeringcdeferstandardsresources

The C defer draft specifies cleanup at block exit

N3589 defines deferred blocks, reverse cleanup order, and return-value evaluation. Its control-flow restrictions make resource lifetime the central design issue.

Resource cleanup in C becomes difficult when acquiring a resource and leaving its scope are far apart in the control flow. The June revision of the proposed defer technical specification addresses that relationship directly. A deferred statement is reached during normal execution, but its body runs when the enclosing block ends or exits through specified control-flow operations. The cleanup can therefore appear beside the acquisition it belongs to.

WG14's document log lists N3589 as revision 2 of the committee draft. The draft itself names JeanHeyd Meneide as project editor and presents extensions to ISO/IEC 9899:2024. This is proposed language support in a separate technical specification. It is not a feature that portable C23 implementations must already accept.

I value C's explicit treatment of resources, but explicit cleanup does not require repeating the release operation at every exit. A lexical mechanism is appealing when it reduces the distance between ownership and cleanup. The important question is exactly which scope and which exits it covers, because a familiar keyword can conceal different answers in different languages.

Reaching defer activates later execution

N3589 defines a defer statement as defer followed by a deferred block. The grammar permits an unlabeled statement there, including a compound statement. When execution reaches the defer statement, the deferred body does not immediately execute. It runs just before leaving the enclosing block under the conditions the draft specifies.

A small example illustrates the intended relationship. This is draft syntax, not a claim about support in an ordinary C23 compiler.

#include <stdlib.h>

int use_memory(void)
{
    void *p = malloc(64);
    if (p == NULL)
        return 1;
    defer { free(p); }
    return 0;
}

The failed allocation returns before reaching the defer statement. On the successful path, the deferred body runs when the function body exits. The source places free after the successful acquisition without executing it at that point. This arrangement gives the release operation a visible association with the lifetime of the local resource.

Only reached statements participate. A defer written after an unconditional return remains unreachable and does not release anything. The draft includes that case explicitly. Placement in the source is insufficient on its own, because activation depends on execution reaching the statement.

Returning computes the result before cleanup

The draft specifies that return expressions and conversion to the return type occur before deferred bodies execute. This ordering lets the returning computation still use the resource that the deferred body will release. It also prevents a deferred assignment from retroactively changing a return value that has already been computed.

For example, the draft illustrates a local integer changed through a pointer in a deferred body. Returning the integer first computes its current value. Cleanup then changes the local object, after the result has been determined. The distinction is observable even though both operations occur while the function is leaving its body.

This order matters for more than a simple integer expression. N3589 explicitly mentions the conversion to the return value. A deferred operation can observe effects of that conversion because it runs afterwards. Describing defer merely as code that runs before return would omit the part that determines which value the caller receives.

Multiple defers in the same enclosing block execute in reverse order of appearance. A deferred body can itself contain defers, whose execution follows the same rule within that body. This supports cleanup where a resource acquired later depends on an earlier resource still being available during its release.

Block scope determines the lifetime

The enclosing block is central to the proposal. A defer inside a nested block belongs to that block, so an ordinary exit from it triggers cleanup even if the function continues afterwards. This is more precise than associating all cleanup with function return.

The draft gives examples involving conditionals and loops to explain that relationship. Reading the grammar and the enclosing block together is necessary. A short statement that follows a control structure does not automatically acquire the same cleanup lifetime as a statement placed elsewhere in the function body. The syntax determines the relevant block.

That is useful for resources whose lifetime should be smaller than the function. The programmer can delimit a section of work with a compound statement, acquire a resource there, and attach cleanup within that scope. The proposal supplies the exit behavior for the block, without requiring every resource to survive until the final return.

Control-flow restrictions protect cleanup structure

N3589 constrains jumps involving deferred statements. A goto or switch cannot jump into a defer statement, and the specified jumps cannot skip over a defer in the enclosing block. These rules prevent control flow from entering a region whose cleanup activation would be ambiguous under the lexical model.

The deferred body has restrictions of its own. return, break, continue, and goto cannot exit it. Cleanup consequently cannot use those operations to replace the surrounding control transfer with an unrelated exit. The draft's examples distinguish illegal jumps out of cleanup from legal jumps that leave an enclosing block and thereby cause cleanup to execute.

Nonlocal jumps require separate rules. The draft does not make ordinary lexical cleanup a universal unwinding guarantee. It also says deferred bodies are not generally run when the program terminates or leaves the block through means outside the listed exits, unless the implementation specifies otherwise. Signals and calls to non-returning functions appear in that discussion.

I prefer those limits to be explicit in the language description. A resource-release mechanism is useful only when its guarantees are clear enough to support ownership reasoning. In this draft, normal block exits and specified jumps have defined cleanup behavior, while arbitrary termination does not inherit that guarantee.

N3589 also addresses return from the initial call to main and from a thread start function. Those clauses connect the language mechanism to library-defined termination behavior. They prevent the proposed cleanup model from depending solely on an informal analogy with other languages. The committee still has a draft to refine, but its central contract is already concrete enough to discuss in terms of activation, scope, and the ordering of a computed return value.