Wording for “Local functions”

2026-06-04

Preamble

contributing

Thiago Adams (rationale, history)

Thiago Adams, Robert Seacord, Jens Gustedt, Joseph Myers (wording)

number Title Authors Remarks
n3678 Local functions Thiago R Adams rationale
n3854 Working Draft JeanHeyd Meneide base for diff
n3883 Wording for “discarded” Wording group base for access semantics
n3884 <this paper> Wording group

LaTeX document branch

none

Liaison

none

Relevant polls

Does WG14 think a proposal along the lines of N3678 and N3679 without captures is useful? 16-5-7

meeting date for against abstain
Winter 2026 Feb 2-6, 2026 16 5 7

Observations and choices made for the wording

The goal of this paper is to make forward progress on the possibility to locally declare and define functions within other functions, for which different implementations already offer several tools. The emphasis in the poll cited above is here “without captures” that is a mode for local function definitions that does not need access to any execution state of the function that is enclosing the definition of the local function. This means in particular

Up to now formulating consistently what it means that a local function would access the execution state of another function was surprisingly difficult. Luckily this recently changed with the introduction of the “discarded” terminology and this wording proposal entirely relies on this new concept.

Another important aspect of this proposal is that it should not invalidate code:

  1. Existing syntax of forward declaring or re-declaring file scope functions have to remain valid and keep their semantics.
  2. Similarly, the use of the gcc extension for nested functions should remain valid, there.
  3. The clang extension for “blocks” (imported from Objective C) should equally remain valid.

For 1. this means in particular that local declarations of the form

int fun(void);
extern double happiness(double);

have to keep their current meaning, namely in referring to symbols fun and happiness that have linkage and that refer to functions.

For 2. this means that any current code that defines a local function should remain valid for the gcc implementation.

For 3. this means that we cannot reuse the same syntax as used there, because their handling of automatic state is different that what is anticipated above. Otherwise, existing code would become either invalid or change semantics for the clang implementation.

The choice that was made for n3678 was to extend the syntax in a different way than 2. and 3. by extending the concept of local static identifiers (that have no linkage) from objects to functions, but otherwise to keep defaults as they have been before.

All of this integrates into the current draft quite nicely, provided that we are able to regulate visibility and use of labels, automatic variables and VM types. Since access to these features (or not) is currently implemented by constraints we decided to continue with this design choice and to avoid to invent new undefined behavior. So we have the general property

Restrictions in the presence of local functions are expressed as constraints.

Proposed wording

Legend

Deletions in the shown standard text are as shown here, additions, as shown here. These may render differently according to the style in which the document is shown by your browser, but should always be well distinguishable. In the provided style there are two visual distinctions:

Close to each other proposed changes resemble like this.

Modification in subclause 6.2.1 “Scopes of identifiers, type names, and compound literals”

3 A label name is the only kind of identifier that has function scope. It can be used (in a goto statement) anywhere in the closest enclosing function body (the compound statement of the innermost enclosing function definition that contains the construct) in which it appears, and is declared implicitly by its syntactic appearance (followed by a :).

Modification in subclause 6.2.2 “Linkages of identifiers”

3 In the set of translation units and libraries that constitutes an entire program, each declaration of a particular identifier with external linkage denotes the same object or function. Within one translation unit, each declaration of an identifier with internal linkage denotes the same object or function. Each declaration of an identifier with no linkage denotes a unique entity; if declared with the storage class specifier static, during the same program execution the designated object or function and its address are the same for all calls to the enclosing function.

5 NOTE 2 A function declaration can contain the storage-class specifier static only if it is at file scope; see 6.7.2.

6 For an identifier declared with the storage-class specifier extern the linkage is external, unless there is a visible declaration of that identifier in file scope that has internal linkage, in which case the linkage is internal. in a scope in which a prior declaration of that identifier is visible,18) if the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration. If no prior declaration is visible, or if the prior declaration specifies no linkage, then the identifier has external linkage.

18) As specified in 6.2.1, the a later declaration can hide the a prior declaration.

7 …

8 The following identifiers have no linkage: an identifier declared to be anything other than an object or a function; an identifier declared to be a function parameter; a block scope identifier for an object declared without the storage-class specifier extern; a block scope identifier for a function declared with the storage-class specifier static.

Modification in subclause 6.4.3.2 “Predefined identifiers”

Semantics

1 The identifier __func__ shall be implicitly declared by the translator as if, immediately following the opening brace of each function definition, the declaration

static const char __func__[] = "function-name";

appeared, where function-name is the name of the lexically-enclosing function closest enclosing function.50)

Modification in subclause 6.7.1 “Declarations/General”

Constraints

4 If an identifier has no linkage, there shall be no more than one declaration of the identifier (in a declarator or type specifier) with the same scope and in the same name space, except that:

4′ For the declaration of a function F with no linkage declared within the closest enclosing function definition G: if F is used in an expression that is not discarded by G, there shall be a definition of F in the same scope as the declaration of F.

Modification in subclause 6.7.2 “Storage-class specifiers”

Constraints

8 The For the declaration of an identifier for a function that has block scope shall have no explicit storage-class specifier other than extern. the storage-class specifier, if any, in the declaration specifiers shall be either extern or static.

Modification in subclause 6.7.5 “Function specifiers”

Semantics

7 Any function with internal or no linkage can be an inline function. …

Modification in subclause 6.8.2 “Labeled statements”

Constraints

2 A case or default label shall appear only in a switch statement and only if the closest enclosing function body (the compound statement of the innermost enclosing function definition that contains the construct) of the label and the switch statement are the same. Further constraints on such labels are discussed under the switch statement.

Modification in subclause 6.8.3 “Compound statement”

Add to the syntax production

block-item:

declaration
function-definition
unlabeled-statement
label

Modification in subclause 6.8.7.1 “General” of “Jump statements”

Add a new paragraph

Constraints

1′ The identifier of a goto, continue or break statement shall name a label for which the closest enclosing function body is the same as for the statement itself.

Modification in subclause 6.8.7.2 “The goto statement”

Constraints

1 The identifier in a goto statement shall name a label located somewhere in the enclosing function. A goto statement shall not jump from outside the scope of an identifier having a variably modified type to inside the scope of that identifier.

Modification in subclause 6.8.7.3 “The continue statement”

Constraints

1 A continue statement shall appear only in or as a loop body and such that the closest enclosing function body of the continue statement and of the corresponding iteration statement are the same.

Modification in subclause 6.8.7.4 “The break statement”

Constraints

1 A break statement shall appear only in or as a switch body or loop body and such that the closest enclosing function body of the break statement and of the corresponding switch or iteration statement are the same.

Modifications in subclause 6.9.2 “Function definitions”

Constraints

2 …

3 The return type of a function shall be void or shall be a complete object type other than that is neither an array type nor a VM type.

4 The storage-class specifier, if any, in the declaration specifiers shall be either extern or static. For a function definition in block scope the storage-class specifier shall be static.

5 …

5′ If an identifier ID of a scope enclosing a function definition F is declared with VM type or as an object with automatic storage duration: ID shall only be used in F in a context that is discarded by F.FTN)

FTN) For example, this constraint is violated if a named constant is used as the array operand of array subscripting, as the operand of the address operator & or as the operand of an implicit array to pointer conversion.

16 EXAMPLE 3 A function that is defined in block scope does not have access to execution-specific state of the enclosing function but it may access translation-time type information as long as the access to the feature is discarded.

int main() {
    int i;
    static void local()
    {
        typeof(i) j = 0;    /* valid */
        auto k = sizeof(i); /* valid */
        int n = i;          /* constraint violation */
    }
}

17 EXAMPLE 4 Named constants of automatic storage duration of an enclosing scope play a special role as they are already evaluated at translation time. Therefore, accessing their value is valid, but an attempt to access their address is a constraint violation.

int main() {
    constexpr int i = 1;
    static constexpr int j = 0;
    static double array[2] = { 2.0, 3.0 };
    static void local()
    {
        printf("%d %d\n", i, j);  /* valid, prints "1 0" */
        &i;                       /* constraint violation */
        &j;                       /* valid */
        static double* p = array; /* valid, using an address constant */
        double* q = &array[0];    /* valid, same, but q is automatic */
        double* r = &array[j];    /* valid, same */
        double* s = &array[i];    /* valid, another address constant */
        double brray[2] = { 4.0, 5.0 };
        double* t = &brray[0];    /* valid, both automatic */
        double* u = &brray[j];    /* valid, same */
        double* v = &brray[i-1];  /* valid, same, i used for its value */
    }
}

18 EXAMPLE 5 Another special case are VM types that occur in the definition of a function in block scope. Even if an object with VM type has static storage duration, there is a hidden execution-specific state that is not known prior to execution.

void f(int n) {
    int ar[n] = {};
    static int (*p)[n];
    p = &ar;
    static void local0()                        { typeof(ar) b; }   /* constraint violation */
    static void local1()                        { typeof(p) bp; }   /* constraint violation */
    static void local2(int (*ap)[n])            { typeof(*ap) b; }  /* constraint violation */
    static void local3(int n, int (*ap)[n])     { typeof(*ap) b; }  /* valid */
    static typeof(int (*)[n])          local4() { return nullptr; } /* constraint violation */
    static typeof(int (*)[(int){ 1 }]) local5() { return nullptr; } /* constraint violation */
}

Here local0 and local1 access identifiers from an enclosing scope with VM type in the function body. Then, local2 and local4 evaluate the object n which is from the enclosing scope and has automatic storage duration in the parameter list and return type, respectively. In contrast to that, local3 only accesses an identifier with VM type that is declared in the scope of the function itself, so this access is valid. For local5, although there is no access to an identifier in an enclosing scope, the compound literal enforces that the return type is still a VM type and so the definition violates a constraint.

19 EXAMPLE 6 Similar as for a block scope object with static storage duration, the pointer to the function handler declared in block scope remains valid after the enclosing function register_handler returns. During the same program execution, all calls to register_handler return the same function pointer value.

typedef void (*callback_t)(void);
callback_t register_handler() {
    static void handler() { /* ... */ }
    return &handler;  /* valid */
}

20 EXAMPLE 7 Constraints are put in place such that targets of local jumps by goto or switch statements are within the same function body, and thus that during execution all such jumps stay within the same function call.

int main(int argc, char* argv[]) {
    switch (argc) {
      case 1:
        static void local()
        {
           L2:
           goto L1; /* constraint violation */
           default: /* constraint violation */
           break;   /* constraint violation */
        }
        local();
        break;
      case 2:
        goto L2;    /* constraint violation */
    }
    L1:
}

Here, for the goto statements labels L1 and L2 are not defined in the same nearest enclosing function. Similarly the default label and the break statement refer to constructs where the nearest enclosing function is different.

21 EXAMPLE 8 This code shows the disambiguation between block scope and file scope function definitions.

#include <stdio.h>

static void func() { printf("external definition\n"); }

void demo1()
{
  extern void func();                         /* refers to file scope definition */
  func();                                     /* prints "external definition" */
  static void func() { printf("local 1\n"); } /* invalid, redeclaration with different linkage */
}

void demo2()
{
  static void func();                         /* hides file scope definition */
  func();                                     /* prints "local 2" */
  static void func() { printf("local 2\n"); } /* valid, compatible definition in same scope */
}

Here in demo1 the block scope declaration of func refers to the file scope definition with internal linkage. The extern specifier is redundant. The function definition then reuses the same name for a block scope function, and thus redeclares the same identifier inconsistently without linkage. In contrast to that, the declaration in demo2 declares a block scope function with no linkage, which is then called and consistently defined at the end.

Modifications in subclause 7.2.2.1 “The assert macro”

2 The assert macro puts diagnostic tests into programs; it expands to a void expression. When it is executed, if expression (which shall have a scalar type) is false (that is, compares equal to 0), the assert macro writes information about the particular invocation that failed (including the text of the argument, the name of the source file, the source line number, and the name of the closest enclosing function — the latter are respectively the values of the preprocessing macros __FILE__ and __LINE__ and of the identifier __func__) on the standard error stream in an implementation-defined format.203) It then calls the abort function.

Acknowledgments

Thanks are extended to Jens Gustedt for assistance with the wording and revisions, and to Martin Uecker and Robert Seacord for their valuable feedback during the revision of this paper.