Even simpler defer for direct integration

Jens Gustedt, INRIA and ICube, France

2024-12-23

target

integration into IS ISO/IEC 9899:202y

document history

document number date comment
n3434 202412 Original proposal

license

CC BY, see https://creativecommons.org/licenses/by/4.0

previous papers on defer

There have been several previous papers on defer features. This paper builds on n3199 and simplifies the approach presented there even more. For motivations of the different choices and for a general introduction please refer to that paper.

References to other works and online information are given throughout as links.

1 Motivation

Paper n3199 proposes a design of a defer feature that would be close to the existing practice of gcc’s [[gnu::cleanup(…)]] attribute. While the idea of providing this feature in a TS had even more traction than to integrate it directly into C2y, there was still possible consensus to bring it already forward for that milestone.

This paper tries to promote a even simpler version of the feature that has straight-forward and efficient implementations in existing compilers and that avoids the introduction of any ambiguities or even new UB when a compound statement with defer is terminated unexpectedly.

2 Approach

The idea is to simplify the proposed model even further than n3199. The main features of the proposed model are

With these restrictions it is then possible to implement the feature efficiently, with the full feature set, with most of the diagnostics and without bending existing keywords

with some restrictions concerning the accessible local variables

with full access to local variables, but some additional syntactic restrictions in any conforming C compiler when replacing the preprocessor with eĿlipsis.

2.1 Syntax

2.1.1 Defer blocks as block items

The first difference of this paper compared to n3199 is to neither have a defer block as a declaration nor as a statement but as a new type of block item within a compound statement, the anchor block. Thereby the association of the defer to the anchor block follows directly from the syntax; defer blocks are then excluded from other positions where declarations or statements may appear simply by the syntax.

So the possible position of a defer block is then different from

This also helps to clarify the semantics: there is a uniquely identified compound statement for which the termination triggers the execution of the deferred block.

In addition this approach makes implementation of the feature easier. A defer block, because it is known to be a block item of a specific compound statement, may be replaced by an implementation by a mix of declaration, statements and labels. Restricting the deferred block as well to a compound statement followed by a semicolon ensures that implementations may freely choose to provide this feature through a hidden variable with an initializer.

2.1.2 Library implementations

Although the feature itself is specified as language feature, we also want to enable implementations to just use their existing infrastructure by providing short wrapper macros, much as it is already done for offsetof or setjmp, for example.

Therefore we introduce a new header, <stddefer.h>, that must be present for portability reasons and that could be used by implementations to provide wrappers as will be described below. But we leave it unspecified if an implementation may provide the feature without the header, and effectively if the header must contain anything beyond a trivial macro definition

#define defer defer

2.2 Semantics

2.2.1 Other control flow

The major semantic change to previous proposals is to leave it explicitly unspecified if unexpected termination of the anchor block (by say, exit, longjmp or a signal) executes deferred blocks or not. Similarly, it leaves it unspecified if and which defer blocks are executed if goto or longjmp are used to execute the anchor block other than lexicographically.

This eases argumentation about the feature a lot: no new UB is introduced into the standard. For each defer block there is only one of two behaviors that is possible: the deferred block is executed or not. In general the worst that can happen in such situations is that some resources are not handled properly. This also isn’t new, that is basically the current situation if a longjmp jumps beyond the end of the scope of a VLA.

2.2.2 Termination of main and similar top level functions

n3199 already identified a case, where adding defer blocks needed special care, namely the main function. Here the current specification has it that a return statement and a call to exit are equivalent. With defer, special care has to be taken if that means that return also executes the deferred blocks or not in addition to what happens when calling exit.

In fact, a similar special case occurs for the top level function of each thread; it has to be clarified if return from that function executes all deferred blocks that have been met. For both cases (program termination and thread termination) we specify that in case of a return all deferred blocks are executed according to the model, and that it is unspecified if calling exit or thrd_exit has an influence if these are then executed as well. We also add a specific footnote for the case the main thread is terminated with thrd_exit, a case that is different from return and from exit for that function.

3 Implementation experience

For three of the five implementations (if we count C++ as an implementation) that have features that come close to what is proposed here we show how defer can be implemented as an object-like macro such that code that looks like

defer { some code };

expands to a sequence similar to

compiler magic; some other compiler magic { some code };

that has the required behavior. That is,

{ some code }

is skipped when met during execution, and then only executed later, when the current compound statement is left.

One implementation (in a dedicated preprocessor) reduces the problem of implementing defer to a generation of unique labels and of structured goto statements that have the same effect as the defer feature.

Last but not least, VS’s __try/__finally is unfortunately syntactically too different that we would be able to mask the difference behind a macro; nevertheless the feature that is provided is semantically very close. Nevertheless it has an interesting secondary feature __leave that could also be integrated into C.

3.1 gcc: the cleanup attribute and nested functions

With gcc, an implementation of the described defer feature is possible with compiler versions of at least two decades that provide nested functions and the cleanup attribute as extensions. Written with C23’s attribute feature the inner macro looks as simple as the following:

#define __DEFER__(F, V)      \
  auto void F(int*);         \
  [[gnu::cleanup(F)]] int V; \
  auto void F(int*)

Here

For this to work we have to provide unique names F and V such that several defer blocks may appear within the same anchor block. This is ensured by another very common extension __COUNTER__

#define defer __DEFER(__COUNTER__)
#define __DEFER(N) __DEFER_(N)
#define __DEFER_(N) __DEFER__(__DEFER_FUNCTION_ ## N, __DEFER_VARIABLE_ ## N)

That is basically it, a straight application of the [[gnu::cleanup]] feature that

Indeed, when adding a bit more magic (such as [[gnu::always_inline]]) the assembly that is produced is very efficient and avoids function calls, trampolines and indirections. (See also Omar Anson’s blog entry on how efficient the cleanup attribute seems to be implemented in gcc.)

Note that in this implementation of the defer feature the chosen construct is that of a function definition. Therefore the fact that the defer syntax requires a compound statement for the deferred block is important: it becomes the function body of a nested function.

Note also, that the implementation as presented above does not provide all diagnostics that would be recommended by the wording as proposed. In particular, the gnu compilers do not diagnose jumps that would jump over declarations with the [[gnu::cleanup]] attribute. Nevertheless, a similar effect can already be easily be achieved by some addition to the presented macros, in particular by using an auxiliary variable of VM type; this is then diagnosed by the compiler if e.g a goto jumps over it. This shows that there is probably already enough infrastructure present in the compiler that would allow to detect jumps over [[gnu::cleanup]]. It would perhaps be good if the gnu compilers could add such a diagnostic in the future.

3.2 clang: the cleanup attribute and ObjectiveC’s “blocks”

Clang does not implement gcc’s nested functions and probably never will. In contrast to that, it implements so-called blocks from ObjectiveC. Here our strategy to implement defer is then a bit different, since “blocks” are expressions that can be assigned to variables with special type. We use a typedef called __df_t as the type of an auxiliary variable and a static function __df_cb that just executes the stored block at the termination of the scope.

// We need the "blocks" extension
typedef void (^const __df_t)(void);

[[maybe_unused]]
static inline
void __df_cb(__df_t* __fp) {
  (*__fp)();
}

#define __DEFER__(V) [[gnu::cleanup(__df_cb))]] __df_t V = ^void(void)

The wrapper that provides the unique name is then similar to the above, clang also has the __COUNTER__ extension:

#define defer __DEFER(__COUNTER__)
#define __DEFER(N) __DEFER_(N)
#define __DEFER_(N) __DEFER__(__DEFER_VARIABLE_ ## N)

Unfortunately a block such as

^void(void) { some code }

has not the same properties as a nested function: in general access to outside variables is restrict to be read-only and provides the value of the variable at the point where the defer is met, not when it is executed. To be conforming with this proposal, clang would need to make some progress, here.

Note that in this implementation of the defer feature the chosen construct is that of a variable with an initializer. Therefore the ; that terminates the defer syntax is important.

3.3 Portability to C++

Because the execution model for defer is very similar to the execution model for destructors in C++, implementing the defer feature with the properties as proposed in C++ is a student excercise. It can be done with a template class and lambdas.

template<typename T>
struct __df_st  : T {
  [[gnu::always_inline]]
  inline
  __df_st(T g) : T(g) {
    // empty
  }
  [[gnu::always_inline]]
  inline
  ~__df_st() {
    T::operator()();
  }
};

#define __DEFER__(V)  __df_st const V = [&](void)->void

Similar to the above implementation with ObjectiveC’s blocks, lambdas are not declarations but expressions. These expressions have a unique type for each lambda, which is taken here as a template parameter to the constructor __df_st<T>::__df_st(T). The destructor __df_st<T>::~__df_st() of the variable then invokes the lambda when V leaves its scope. But in contrast to the version with ObjectiveC’s blocks, the [&] in

[&](void)->void { some code }

ensures that all outer variables are fully accessible at the point of execution of the lambda. Therefore, such an implementation provides the full functionality.

Note also, that the __COUNTER__ pseudo-macro is also quite commonly implemented by C++ compilers and is proposed as an addition to C++26.

3.4 eĿlipsis: a preprocessor approach

eĿlipsis is an enhanced preprocessor that can be used as a drop-in replacement for the compilation phases 1-4. It is tested currently for gcc and clang.

One of the enhancements of eĿlipsis is that it can be used to instrument bracket constructs such as {} to count nestedness and in general to use counters to monitor progress in the source code. This allows to implement infrastructure for defer that is otherwise only dependent of standard C features such as labels, goto and the setting of some auxiliary variables.

Being a “preprocessor-only” implementation of defer, this approach has more restrictions than the others that have been presented above:

These restrictions could be easily removed when implementing a similar strategy in a compiler frontend.

Although the code after replacement is a braid of labels and goto, inspecting the generated assembly shows that modern C compilers such as gcc and clang handle such code well.

3.5 __try and __finally

The VS compilers have another interesting extension using two successive compound statements, the first (called “guarded section”) labeled with __try the second (called “termination handler”) with __finally. The semantics are similar to what is described here, namely the guarded section would be our anchor block and the termination handler would be our deferred block. Besides the syntax, there are several differences, though, that make this extension more difficult to use and more difficult to standardize:

In addition to the features for defer that we specify here, the VS extension is also able to do stack unwinding that is similar to exception handling in C++. Nevertheless, the strategies that are applied for __try/__finally are meant to be compatible with the semantics that we propose here, because it is unspecified if deferred blocks are executed whence an anchor is terminated by other constructs than by a normal jump.

So with this new feature integrated into C2y, VS compilers would have to work at their frontend, but they should already have most of the infrastructure in place such that later compiler phases should be able to integrate this feature efficiently.

The VS extension offers one other feature, though, that could be interesting to integrate into the standard at the same time as the defer feature, namely a __leave jump statement. This statement is meant to terminated the innermost guarded section that contains it such that the execution of the termination handler then follows immediately. Below we propose an option for a _Leave statement that has similar properties.

4 Suggested wording.

New text is underlined green, removed text is stroke-out red.

4.1 6.4.2 Keywords

Add defer and _Leave to the list of keywords and modify p2, last sentence

The spelling of these keywords, their alternate forms, and of defer, false and true inside expressions that are subject to the # and ## preprocessing operators is unspecified.61)

4.2 6.8.1 Statements and blocks, general

Modify the beginning of p3:

3 A block is either a primary block, a secondary block, the block associated with a function definition or a defer block; it allows a set of declarations and statements to be grouped into one syntactic unit.

4.3 6.8.3 Compound statement

In p1, add the syntax derivation defer-block to block-item.

4.4 6.8.8 The defer block

Add a new clause 6.8.8 with the following contents:

6.8.8 The defer block

Syntax
defer-block:

defer deferred-block ;

deferred-block:

compound-statement

Description
2 Through the syntax each defer block is uniquely associated to a enclosing compound statement (called its anchor block) for which it is an block item of the corresponding block item list; an anchor block may have multiple defer blocks (and thus deferred blocks) that are associated to it. The purpose of a defer block is to defer the execution of the deferred block to the end of the execution of the anchor block; this not withstanding the defer block has the lexical properties induced by its lexical position.
Semantics
3 A deferred block is not executed when the execution of the anchor block meets a defer block; whenever the execution of an instance of the anchor block ends by a jump statement or because the closing } is met, all its deferred blocks up to the last that has been been met during its execution (before a jump) are executed in the reverse lexicographical order in which they have been specified. It is unspecified if and when deferred blocks are executed
4 As an exception of the provisions in 5.2.2.3.4 and 7.30.5.5 deferred blocks of the top level function of the execution (usually called main, see 3.2.2.3.2) or of a thread (for example started from a call to thrd_create, see 7.30.5.1), are executed according to these rules if the execution of the function is terminated by an explicit return statement.FTN)
FTN) In particular, is is unspecified if defer blocks that are associated to the body of main (or similar) are executed if the corresponding thread is detached with thrd_detach and then terminated with thrd_exit.
5 It is unspecified if defer blocks are available to the application if the <stddefer.h> header (7.22) is not included prior to the first use of the construct.
Recommended practice

6 It is recommended that implementations diagnose the following:

7 NOTE 1 This clause only specifies a model of execution where defer blocks of an anchor block are met contiguously in lexicographic order until its execution ends; otherwise it is unspecified if the deferred block of a defer block
In the model, the possibilities to end the execution of an anchor block are as follows:
If the execution ends by a jump statement all deferred blocks of all anchor blocks of the function for which execution has started and is ended by the jump statement are executed in reverse lexicographical order.
8 NOTE 2 If the execution of an anchor block ends because one of the termination functions for the whole execution (for example exit) or for a thread is called, or because longjmp is used to transfer execution to a point outside the anchor, it is unspecified if deferred blocks that had been met are executed. Thus, in such a case the corresponding resource cleanup may be missed and ressources such as allocated memory may leak.
9 NOTE 3 By the syntax, the placement of defer blocks is more restricted than for statements.
{                                   // anchor block
    ...
    defer doit();                   // syntax error, not a compound statement
    if (something) defer {          // syntax error, no direct anchor for defer
        doit();
    };
    if (something)  {               // anchor block
        defer {                     // valid, but defer is useless
            doit();
        };
    }
    defer {                         // valid, evaluation of condition is deferred
        if (something)  doit();
    };
    defer BLA: {                    // syntax error, label not permitted
        if (something) doit();
    };
    defer {
        if (something) BLU: doit(); // valid, label only useful in the deferred block
    };
}
In particular, the syntax inhibits that a defer block is a secondary block of a selection statement or iteration statement, whereas a deferred block can contain such a statement.
10 EXAMPLE Consider the following anchor block
{                             // anchor block
    void* p = malloc(25);
    defer { free(p); };          // 1st defer

    mtx_lock(&mut);
    defer { mtx_unlock(&mut); }; // 2nd defer

    static uint64_t critical = 0;
    critical++;
    defer { critical--; };       // 3rd defer

    while (something) cnd_wait(&cond, &mut);
    ... use p under protection of mut ...
}
It is equivalent to this code without defer,
{
    void* p = malloc(25);

    mtx_lock(&mut);

    static uint64_t critical = 0;
    critical++;

    while (something) cnd_wait(&cond, &mut);
    ... use p under protection of mut ...


    { critical--;       } // from 3rd defer
    { mtx_unlock(&mut); } // from 2nd defer
    { free(p);          } // from 1st defer
}
only that the deferred blocks are even executed when the anchor block is left by a jump statement. Thus using defer here ensures that
Forward references: Non-local jumps (7.13), the defer construct <stddefer.h> (7.22), communication with the environment (7.25.5), the thrd_exit function (7.30.5.5).

4.5 new 7.22 for integration as a library feature

Add a new clause 7.22 with the following contents:

7.22 The defer construct <stddefer.h>
1 The header <stddefer.h> shall ensure that defer blocks are available to the application and provides the macro defer which expands to an implementation-defined value with the same functionality as the corresponding keyword.
Recommended practice
2 As specified in 6.8.8, implementations may or may not provide facilities that enable defer blocks through this header. For portability reasons it is recommended that applications that use the defer construct include this header unconditionally.

4.6 A new jump construct _Leave

As mentionned above, the _Leave keyword comes from the VS compiler and the __try/__finally clauses. There it can be used to terminate the __try clause by jumping directly to the __finally clause. This construct is not implemented by the other four implementations, yet. But if WG14 thinks that this could be a helpful addition, the point in time would be to add it now and not to wait for yet another round of standardization.

If WG14 finds it usefull the following text should be added to C2y; if not any mention of _Leave in the above wording should be omitted.

4.6.1 keywords

add _Leave to the keywords

4.6.2 Add a new clause

In 6.8.7.1 (Jump statements, General) p1, add the syntax derivation

_Leave ;

to jump-statement.

Add a new clause

6.8.7.6 The _Leave statement
Constraints
1 A _Leave statement shall be enclosed in a compound statement with an associated defer block (6.8.8).
Semantics
2 A _Leave statement terminates the execution of the innermost enclosing compound statement with an associated defer block. All deferred blocks of defer blocks that are associated with that compound statement and that have been met during that execution are executed as specified in clause 6.8.8.