2026-06-04
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 |
none
none
| meeting | date | for | against | abstain |
|---|---|---|---|---|
| Winter 2026 | Feb 2-6, 2026 | 16 | 5 | 7 |
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
goto when the local
function returns.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:
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.
static function
declaration that is no definition, forward declares such a function,
much as such a declaration in file scope.static function
definition defines the function that it names but does not introduce
linkage.static function
definition has access to symbols from the enclosing scope(s) as long as
this would not imply access to execution state. Basically, using all
features that would be allowed in a static object
definition (type specification and initializer) at the same
place are allowed.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.
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.
3 A label name is the only kind of identifier that has function scope. It can be used (in a
gotostatement)anywherein 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:).
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
externthe 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,
thea later declaration can hidethea 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 specifierstatic.
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 functionclosest enclosing function.50)
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:
- a typedef name can be redefined to denote the same type as it currently does, provided that type is not a variably modified type;
- enumeration constants and tags can be redeclared as specified in 6.7.3.3 and 6.7.3.4, respectively.
- a block scope identifier for a function may be declared more than once, provided that at most one declaration is a definition, that all declarations have compatible types and have no linkage; the type of the identifier at a later declaration becomes the composite type of the declaration and the type of the identifier prior to the declaration.
4′ For the declaration of a functionFwith no linkage declared within the closest enclosing function definitionG: ifFis used in an expression that is not discarded byG, there shall be a definition ofFin the same scope as the declaration ofF.
Constraints
…
8
TheFor the declaration of an identifier for a functionthat has block scope shall have no explicit storage-class specifier other thanthe storage-class specifier, if any, in the declaration specifiers shall be eitherextern.externorstatic.
Semantics
…
7 Any function with internal or no linkage can be an inline function. …
Constraints
2 A
caseordefaultlabel shall appear only in aswitchstatement 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 theswitchstatement are the same. Further constraints on such labels are discussed under theswitchstatement.
Add to the syntax production
block-item:
declaration
function-definition
unlabeled-statement
label
Add a new paragraph
Constraints
1′ The identifier of a
goto,continueorbreakstatement shall name a label for which the closest enclosing function body is the same as for the statement itself.
Constraints
1
The identifier in aAgotostatement shall name a label located somewhere in the enclosing function.gotostatement shall not jump from outside the scope of an identifier having a variably modified type to inside the scope of that identifier.
Constraints
1 A
continuestatement shall appear only in or as a loop body and such that the closest enclosing function body of thecontinuestatement and of the corresponding iteration statement are the same.
Constraints
1 A
breakstatement shall appear only in or as a switch body or loop body and such that the closest enclosing function body of thebreakstatement and of the correspondingswitchor iteration statement are the same.
Constraints
2 …
3 The return type of a function shall be
voidor shall be a complete object typeother thanthat is neither an array type nor a VM type.
4
The storage-class specifier, if any, in the declaration specifiers shall be eitherFor a function definition in block scope the storage-class specifier shall beexternorstatic.static.
5 …
5′ If an identifier
IDof a scope enclosing a function definitionFis declared with VM type or as an object with automatic storage duration:IDshall only be used inFin a context that is discarded byF.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
local0andlocal1access identifiers from an enclosing scope with VM type in the function body. Then,local2andlocal4evaluate the objectnwhich is from the enclosing scope and has automatic storage duration in the parameter list and return type, respectively. In contrast to that,local3only accesses an identifier with VM type that is declared in the scope of the function itself, so this access is valid. Forlocal5, 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
handlerdeclared in block scope remains valid after the enclosing functionregister_handlerreturns. During the same program execution, all calls toregister_handlerreturn 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
gotoorswitchstatements 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
gotostatements labelsL1andL2are not defined in the same nearest enclosing function. Similarly thedefaultlabel and thebreakstatement 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
demo1the block scope declaration offuncrefers to the file scope definition with internal linkage. Theexternspecifier 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 indemo2declares a block scope function with no linkage, which is then called and consistently defined at the end.
assert
macro”…
2 The
assertmacro puts diagnostic tests into programs; it expands to a void expression. When it is executed, ifexpression(which shall have a scalar type) is false (that is, compares equal to0), theassertmacro 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 theabortfunction.
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.