Unified Function Syntax

ISO/IEC JTC1 SC22 WG21 N2825 = 09-0015 - 2009-02-08

Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org
Alisdair Meredith, public@alisdairm.net

This paper is a substantial revision of N2763.

Introduction
New Function Declaration Syntax
The Type of Empty–Capture-List Lambdas
New Type-ID and Parameter Syntax
Local Functions
Named Lambdas
Auto with Function Definitions
Proposed Wording
3.3.1 Point of declaration [basic.scope.pdecl]
3.3.2 Local scope [basic.scope.local]
3.5 Program and linkage [basic.link]
5.1.1 Lambda expressions [expr.prim.lambda]
7 Declarations [dcl.dcl]
7.1 Specifiers [dcl.spec]
7.1.1 Storage class specifiers [dcl.stc]
7.1.6.4 auto specifier [dcl.spec.auto]
8 Declarators [dcl.decl]
8.1 Type names [dcl.name]
8.3.5 Functions [dcl.fct]
8.4 Function definitions [dcl.fct.def ]
13 Overloading [over]

Introduction

The sytax for both the new function declarator syntax (N2541 New Function Declarator Syntax Wording) and lambda expressions (N2550 Lambda Expressions and Closures: Wording for Monomorphic Lambdas (Revision 4)) are similar. As suggested by Alisdair Meredith (N2511 Named Lambdas and Local Functions), the syntax for both could be made more similar, thus simplifying the view of the programmer. The British position (N2510 BSI Position on Lambda Functions) supports this work.

Such a unification would address the concerns of Daveed Vandevoorde (N2337 The Syntax of auto Declarations) that the auto keyword was too overloaded in its use for both a new function declarator syntax and for automatically deducing variable type (N1984 Deducing the type of variable from its initializer expression (revision 4)).

This paper proposes the syntactic unification of function declarations and lambda expressions. The key technical insight enabling this unification is that an empty lambda capture list means that no local environment is captured, which is exactly the semantics of a function. That is, a function is a special case of a lambda.

The paper takes the new lambda syntax (N2550) as the starting point for syntactic unifications, and specific syntactic suggestions in N2511 no longer apply. As a simplistic unification would introduce unfortunate irregularities in semantics, we also propose regularizing the semantics of such declarations.

This paper presents a unification that includes more capability than in prior papers. This change in approach addresses a concern that earlier proposals did not go far enough to justify any support. Rather than prematurely select those parts of the end state are acceptable for C++0x, we choose to propose the full end state and let the committee decide if and how to scale back the proposal. However, we believe that everything within this proposal is both desirable and implementable within C++0x given the existing presence of lambda expressions.

New Function Declaration Syntax

Based on direction from the September 2008 meeting, our general approach is to replace function declarations using an auto type specifier and a "->" return type (N2541) with new declaration syntax for functions.

The syntax for function declarations extends the syntax for lambda expressions with declaration specifiers and an id expression to name the function. A lambda expression is distinct from a function by the presence of an id expression.

lambda-expression:
lambda-introducer function-headingopt compound-statement
function-declaration:
func-decl-specifier-seqopt lambda-introducer id-expression function-heading

For functions at namespace scope, the lambda-introducer of the form [] is semantically correct. For functions at class scope, the lambda-introducer of the form [] is semantically correct, with the understanding that class-scope non-static functions still have an implicit this parameter. (That is, this is passed, not captured.)


int x=0, y=0;
[] f(int z)->int { return x+y+z; }
struct s {
    int k;
    [] g(int z)->int;
};
[] s::g(int z)->int { return k+x+z; }

In the process of unifying the syntax, we refactored and unified the grammar. There are, however, some context-dependent textual restrictions on the use of that grammar.

The Type of Empty–Capture-List Lambdas

To ensure that lambdas and functions have uniform semantics where possible, we change a lambda with an empty capture list to name a function rather than return a closure object. Naming a function rather than returning a function pointer is important to template specialization when the lambda is a template argument. The implicit conversion to function pointer also enables use of a lambda expression in existing interfaces requiring function pointers.

New Type-ID and Parameter Syntax

The auto-> syntax was usable in type-ids and parameters, and so the equivalent new syntax must be added.

type-id:
type-specifier-seq attribute-specifieropt abstract-declaratoropt
lambda-introducer function-heading
parameter-declaration:
decl-specifier-seq attribute-specifieropt declarator parameter-defaultopt
decl-specifier-seq attribute-specifieropt abstract-declaratoropt parameter-defaultopt
lambda-introducer declarator-idopt function-heading parameter-defaultopt

The primary likely concern with this syntax of type-ids is ambiguity with lambda expressions. These are unambiguously resolved by the presence or absence of a following compound statement. While the common prefix is non-trivial, it shares the same grammar rules and provides the same information.

Local Functions

The new function syntax enables defining functions at local scope. Local functions can improve program clarity by not affecting containing scopes and by keeping the definition and use of functions closer together.


bool halvable(int n) {
    [] even(unsigned n)->bool;
    [] odd(unsigned n)->int { return n == 0 ? false : even(n-1); }
    [] even(unsigned n)->bool { return n == 0 ? true : odd(n-1); }
    return even(n);
}

Note that we preserve the existing non-local interpretation of the existing local function declarations.


void halvable(int n) {
    int even( int n ); // implicit extern declaration
    return even(n);
}

Named Lambdas

Local functions with non-empty capture lists are simply named lambdas.


int h(int b) {
    [] m(int z)->int // local function
        { return x+z; } // b is not in scope
    [&] n(int z)->int // named lambda
        { return b+x+z; } // b is in scope, by reference
    return m(b) + n(b);
}

Named lambdas provide a way to use a lambda at multiple places, though this could be done with a variable holding the closure object instead.


int h(int b) {
    auto m = [&](int z)->int { return b+x+z; };
    return m(b) + m(b);
}

Named lambdas provide two advantages over this variable approach. First, they can enter into overload sets.


double a(int n) {
    [n] p(int m) { return m+n; }
    [n] p(double m) { return m+n; }
    return p(3.0);
}

Second, they can enable forward declaration. The point of capture is at the elaboration of the definition, not the declaration, so named lambdas must not be invoked or copied before the elaboration of their definition. As a consequence, mutually recursive named lambdas must capture the other by reference.


int a(int n) {
    [&,n] p(int m)->int;
    [&,n] q(int m) { return p(m-1)+n; }
    // okay, use of p in a lambda body is not yet invoked
    int x = p(3);
    // error, the definition of p has not been elaborated
    [&,n] p(int m)->int { return m>0 ? q(m-1)+n : 0; }
    int y = p(4);
    // okay, p has been defined
}

Auto with Function Definitions

Since the auto keyword is not longer used to specify late return types, there is a choice in whether or not it applies to function definitions. The simplest approach is to reserve it strictly to object definitions. However, the "infer from initializer" interpretation can also permit infering a return type of a function definition.


auto twice( int x ) { return x+x; }

As with lambdas, we require that the body consist only of a return statement.

Likewise, one can infer the return type of function defintions using auto within the new syntax.


[] twice( int x ) -> auto { return x+x; }

Proposed Wording

The proposed wording shows changes from working draft N2798.

3.3.1 Point of declaration [basic.scope.pdecl]

Edit paragraph 9 as follows. The intent is to enable local functions while preserving existing semantics for existing code. This edit should remain even if local functions are not adopted now, so as to preserve future possible standardization.

[Note: friend declarations refer to functions or classes that are members of the nearest enclosing namespace, but they do not introduce new names into that namespace (7.3.1.2). Function declarations via a simple-declaration at block scope and function or object declarations with the extern specifier at block scope refer to delarations that are members of an enclosing namespace, but they do not introduce new names into that scope (3.5 [basic.link]). —end note]

3.3.2 Local scope [basic.scope.local]

Edit paragraph 2 as follows. Note the added comma in the second sentence. This edit simply adjusts to grammar simplification.

The potential scope of a function parameter name (including one appearing in a lambda-parameter-declaration-clause parameter-declaration-clause) or of a function-local predefined variable in a function definition (8.4) begins at its point of declaration. If the function has a function-try-block, the potential scope of a parameter or of a function-local predefined variable ends at the end of the last associated handler, otherwise it ends at the end of the outermost block of the function definition. A parameter name shall not be redeclared in the outermost block of the function definition nor in the outermost block of any handler associated with a function-try-block.

3.5 Program and linkage [basic.link]

Edit paragraph 6 as follows. The intent is to enable local functions while preserving existing semantics for existing code. This edit should remain even if local functions are not adopted now, so as to preserve future possible standardization.

The name of a function declared in block scope via a simple-declaration [Footnote: See [dcl.fct] for function declarations not via simple-declaration. —end footnote], and the name of a function or an object declared by a block scope extern declaration, have linkage. If there is a visible declaration of an entity with linkage having the same name and type, ignoring entities declared outside the innermost enclosing namespace scope, the block scope declaration declares that same entity and receives the linkage of the previous declaration. If there is more than one such matching entity, the program is ill-formed. Otherwise, if no matching entity is found, the block scope entity receives external linkage.

Note that if local function definitions are not permitted, the following sentence needs to be added to this paragraph.

If a declared function has no linkage, the program is ill-formed.

5.1.1 Lambda expressions [expr.prim.lambda]

Edit the syntax as follows. This edit refactors the lambda syntax to reuse the function syntax, which includes modifications to support lambda.

lambda-expression:
lambda-introducer lambda-parameter-declarationopt compound-statement
lambda-introducer function-headingopt compound-statement
lambda-introducer:
[ lambda-captureopt ]
lambda-capture:
capture-default
capture-list
capture-default , capture-list
capture-default:
&
=
capture-list:
capture
capture-list , capture
capture:
identifier
& identifier
this
lambda-parameter-declaration:
( lambda-parameter-declaration-listopt ) mutableopt attribute-specifieropt exception-specificationopt lambda-return-type-clauseopt
lambda-parameter-declaration-list:
lambda-parameter
lambda-parameter , lambda-parameter
lambda-parameter:
decl-specifier-seq attribute-specifieropt declarator
lambda-return-type-clause:
-> attribute-specifieropt type-id

Edit paragraph 1 as follows. The intent of this edit is to follow the unification of the grammar, and then add restrictions to remain consistent with the existing lambda syntax.

In a lambda-parameter-declaration the function-qualifiers of a function-heading ([dcl.fct]) the attribute-specifier appertains to the lambda. In a lambda-return-type-clause return-type-clause the attribute appertains to the lambda return type. The mutable qualifier shall occur only when the lambda-introducer is not empty. The cv-qualifier-seq or ref-qualifier shall not be present. Each parameter in the function-heading shall have a declarator-id.

Edit paragraph 7 as follows. The intent of this edit is make empty–capture-list lambdas name functions.

If the effective capture set is empty, the lambda-expression names an anonymous function of type R(P), where R is the return type and P is the parameter-type-list of the lambda expression. The corresponding function pointer [conv.func] is the closure object. The Otherwise, the type of the closure object is a class with a unique name, call it F, considered to be defined at the point where the lambda expression occurs.

Edit paragraph 10 as follows. Note the insertion of a comma. The intent of this edit is to follow the changes to the grammar.

Edit paragraph 12 as follows. The intent of this edit is to follow the changes to the grammar.

If every name in the effective capture set is preceded by & and the lambda expression is not mutable, F is publicly derived from std::reference_closure<R(P)> (20.6.18), where R is the return type and P is the parameter-type-list parameter-declaration-clause of the lambda expression. Converting an object of type F to type std::reference_closure<R(P)> and invoking its function call operator shall have the same effect as invoking the function call operator of F. [Note: This requirement effectively means that such F's must be implemented using a pair of a function pointer and a static scope pointer. —end note]

7 Declarations [dcl.dcl]

Edit paragraph 1 as follows. The intent of this edit is to enable function-local function declarations and definitions.

....
declaration:
block-declaration
function-definition
template-declaration
explicit-instantiation
explicit-specialization
linkage-specification
namespace-definition
concept-definition
concept-map-definition
attribute-declaration
block-declaration:
simple-declaration
function-declaration
function-definition
asm-definition
namespace-alias-definition
using-declaration
using-directive
static_assert-declaration
alias-declaration
opaque-enum-declaration
....

7.1 Specifiers [dcl.spec]

Edit paragraph 1 as follows. The intent of this edit is provide a grammar subroutine for function declarations.

The specifiers that can be used in a declaration are

decl-specifier:
storage-class-specifier
type-specifier
function-specifier
friend
typedef
constexpr
alignment-specifier
func-decl-specifier
decl-specifier-seq:
decl-specifier-seqopt decl-specifier
func-decl-specifier:
func-storage-class-specifier
function-specifier
friend
typedef
constexpr
func-decl-specifier-seq:
func-decl-specifier-seqopt func-decl-specifier

7.1.1 Storage class specifiers [dcl.stc]

Edit paragraph 1 as follows. The intent of this edit is provide a grammar subroutine for function declarations.

The storage class specifiers are

storage-class-specifier:
register
static
thread_local
extern
mutable
func-storage-class-specifier:
func-storage-class-specifier:
static
extern

....

7.1.6.4 auto specifier [dcl.spec.auto]

Remove paragraph 1. The intent of this edit is to remove the auto-> syntax.

The auto type-specifier signifies that the type of an object being declared shall be deduced from its initializer or specified explicitly at the end of a function declarator.

Edit paragraph 2 as follows. The intent of this edit is to remove the auto-> syntax and to enable auto with function definitions. If that latter feature is not desired, this paragraph would be removed.

The auto type-specifier may appear with a function declarator with a late-specified return type (8.3.5) in any context where such a declarator is valid, and the use of auto is replaced by the type specified at the end of the declarator. definition ([dcl.fct.def])

Edit paragraph 3 as follows. The intent of the edit is ensure flow of text from the above.

Otherwise, the type of the object is auto type-specifier signifies that the type of an object being declared shall be deduced from its initializer. The name of the object being declared shall not appear in the initializer expression. The auto type-specifier is allowed when declaring objects in a block (6.3), in namespace scope (3.3.5), and in a for-init-statement (6.5.3). The decl-specifier-seq shall be followed by one or more init-declarators, each of which shall have a non-empty initializer of either of the following forms:

= assignment-expression
( assignment-expression )

8 Declarators [dcl.decl]

Edit paragraph 4 as follows. The intent of the edit is to remove the auto-> syntax. In the process, the ptr-declarator rule becomes redundant and is eliminated. Furthermore, the edit refactors the grammar.

Declarators have the syntax

declarator:
ptr-declarator
noptr-declarator parameters-and-qualifiers -> attribute-specifieropt type-id
ptr-declarator:
noptr-declarator
ptr-operator ptr-declarator
noptr-declarator:
declarator-id attribute-specifieropt
noptr-declarator parameters-and-qualifiers
noptr-declarator [ constant-expressionopt ] attribute-specifieropt
( ptr-declarator )
parameters-and-qualifiers:
( parameter-declaration-clause ) attribute-specifieropt cv-qualifier-seqopt ref-qualifieropt exception-specificationopt
( parameter-declaration-clause ) function-qualifiers
ptr-operator:
* attribute-specifieropt cv-qualifier-seqopt
&
&&
ref-qualifier
::opt nested-name-specifier * attribute-specifieropt cv-qualifier-seqopt
cv-qualifier-seq:
cv-qualifier cv-qualifier-seqopt
cv-qualifier:
const
volatile
ref-qualifier:
&
&&
declarator-id:
...opt id-expression
::opt nested-name-specifieropt class-name

A class-name has special meaning in a declaration of the class of that name and when qualified by that name using the scope resolution operator :: (5.1, 12.1, 12.4).

8.1 Type names [dcl.name]

Edit paragraph 1 as follows. The intent of this edit is to remove the auto-> syntax and add the new function syntax.

type-id:
type-specifier-seq attribute-specifieropt abstract-declaratoropt
lambda-introducer function-heading
abstract-declarator:
ptr-abstract-declarator
noptr-abstract-declaratoropt parameters-and-qualifiers -> attribute-specifieropt type-id
...
ptr-abstract-declarator:
noptr-abstract-declarator
ptr-operator ptr-abstract-declaratoropt
noptr-abstract-declarator:
noptr-abstract-declaratoropt parameters-and-qualifiers
noptr-abstract-declaratoropt [ constant-expression ] attribute-specifieropt
( ptr-abstract-declarator )

8.3.5 Functions [dcl.fct]

Edit paragraph 1 as follows. The intent of this edit is to follow the grammar refactoring.

In a declaration T D where D has the form

D1 ( parameter-declaration-clause ) attribute-specifieropt cv-qualifier-seqopt ref-qualifieropt exception-specificationopt
D1 ( parameter-declaration-clause ) function-qualifiers

and the type of the contained declarator-id in the declaration T D1 is "derived-declarator-type-list T", the type of the declarator-id in D is "derived-declarator-type-list function of ( parameter-declaration-clause ) cv-qualifier-seqopt ref-qualifieropt returning T". The optional attribute-specifier appertains to the function type.

Delete paragraph 2. The intent of this edit is to remove the auto-> syntax.

In a declaration T D where D has the form

D1 ( parameter-declaration-clause ) cv-qualifier-seqopt ref-qualifieropt exception-specificationopt -> type-id

and the type of the contained declarator-id in the declaration T D1 is "derived-declarator-type-list T," T shall be the single type-specifier auto and the derived-declarator-type-list shall be empty. Then the type of the declarator-id in D is "function of (parameter-declaration-clause) cv-qualifier-seqopt ref-qualifieropt returning type-id". Such a function type has a late-specified return type.

Delete paragraph 3. The intent of this edit is to remove the auto-> syntax.

The type-id in this form includes the longest possible sequence of abstract-declarators. [Note: This resolves the ambiguous binding of array and function declarators. [Example:

auto f()->int(*)[4]; // function returning a pointer to array[4] of int
// not function returning array[4] of pointer to int

end example] —end note]

Insert a new paragraph 2. The intent of this edit is to add the new function syntax. Note that the position of the mutable keyword seems out of place. It is consistent with the existing grammar.

Function declarations with late-specified return type have the form:

function-declaration:
func-decl-specifier-seqopt lambda-introducer id-expression function-heading
function-heading:
( parameter-declaration-clause ) function-qualifiers return-type-clauseopt
function-qualifiers:
mutableopt attribute-specifieropt cv-qualifier-seqopt ref-qualifieropt exception-specificationopt
return-type-clause:
-> attribute-specifieropt type-id

[Example:


int x=0, y=0;
[] f(int z)->int { return x+y+z; }
struct s {
    int k;
    [] g(int z)->int;
};
[] s::g(int z)->int { return k+x+z; }

end example]

Insert a new paragraph after the above paragraph. The intent of this edit is to define the semantics of a missing return-type-clause.

Within a function-heading, a missing return-type-clause indicates that the return type is void, unless the heading is part of a lambda or function definition with a compound-statement of the form { return expression ; }, in which case the return type is the type of expression. [Note: See also (5.1.1 [expr.prim.lambda]). —end note] [Example:


[] p(int m); // return type is void
[] q(int m) { return m; } // return type is int
[] r(double m) { p(m); } // return type is void

end example]

Insert a new paragraph after the above paragraph. The intent of this edit is to reinforce the local functions implicit in the grammar change.

Functions using the late-specified return type may be local to another function. [Example:


[] halvable(int k)->bool; {
    [] even(unsigned n)->bool;
    [] odd(unsigned n)->int { return n == 0 ? false : even(n-1); }
    [] even(unsigned n)->bool { return n == 0 ? true : odd(n-1); }
    return even(k);
}

end example]

Insert a new paragraph after the above paragraph. The intent of this edit is to tie the descriptions of lambda and function together. Furthermore, we require capture lists not in local scope to reflect the fact that they have nothing to capture.

The semantics of a non-empty lambda-introducer are described in ([expr.prim.lambda]). A non-empty lambda-introducer shall only appear in block scope with non-extern declarations. These definitions are named lambdas. The capture list for a named lambda declaration and its definition must be identical. The point-of-capture for a named lambda is its definition. The invokation of a named lambda before the elaboration of its definition is an error, no diagnostic required. [Example:


int a(int n) {
    [&,n] p(int m)->int;
    [&,n] q(int m) { return p(m-1)+n; }
    // okay, use of p in a lambda body is not yet invoked
    int x = p(3);
    // error, the definition of p has not been elaborated
    [&,n] p(int m)->int { return m>0 ? q(m-1)+n : 0; }
    int y = p(4);
    // okay, p has been defined
}

end example]

Edit the existing paragraph 4 as follows. The intent of this edit is to add the new function syntax for parameters. In the process, we refactor the grammar for default parameters.

A type of either form is a function type. [Footnote: As indicated by syntax, cv-qualifiers are a signficant component in function return types. —end footnote]

parameter-declaration-clause:
parameter-declaration-listopt ...opt
parameter-declaration-list , ...
parameter-declaration-list:
parameter-declaration
parameter-declaration-list , parameter-declaration
parameter-declaration:
decl-specifier-seq attribute-specifieropt declarator parameter-defaultopt
decl-specifier-seq attribute-specifieropt declarator = assignment-expression
decl-specifier-seq attribute-specifieropt abstract-declaratoropt parameter-defaultopt
decl-specifier-seq attribute-specifieropt abstract-declaratoropt = assignment-expression
lambda-introducer declarator-idopt function-heading parameter-defaultopt
parameter-default:
= assignment-expression

Edit within paragraph 12 as follows. The intent of this edit is to make the examples follow the new syntax.

[Note: typedefs and late-specified return types are sometimes convenient when the return type of a function is complex. For example, the function fpif above could have been declared


typedef int IFUNC(int);
IFUNC* fpif(int);

or


auto [] fpif(int)->int(*)(int)

A late-specified return type is most useful for a type that would be more complicated to specify before the declarator-id:


template <class T, class U>
auto [] add(T t, U u) -> decltype(t + u);

rather than


template <class T, class U>
decltype((*(T*)0) + (*(U*)0)) add(T t, U u);

end note]

8.4 Function definitions [dcl.fct.def]

Edit paragraph 1 as follows. The intent of the edit is to add the new function syntax. In the process, we refactor the grammar.

Function definitions have the form

function-definition:
decl-specifier-seqopt attribute-specifieropt declarator function-body
decl-specifier-seqopt attribute-specifieropt declarator = default ;
decl-specifier-seqopt attribute-specifieropt declarator = delete ;
function-declaration function-body
function-body:
ctor-initializeropt compound-statement
function-try-block
= default ;
= delete ;

After paragraph 8, insert a new paragraph. The intent of this edit is to allow auto in return types for function definitions.

The type-id of a function or lambda definition may contain the auto type specifier, provided that the form of the compound-statement is { return expr; } and the return type is determined from the type of the expression using the rules for template argument deduction, as in (7.1.6.4 [dcl.spec.auto]). [Example:


[] m( int a ) -> auto { return a; }
auto m( int a ) { return a; }

end example]

13 Overloading [over]

Edit paragraph 1 as follows. The intent is to clarify overloading of named lambdas.

When two or more different declarations are specified for a single name in the same scope, that name is said to be overloaded. By extension, two declarations in the same scope that declare the same name but with different types are called overloaded declarations. Only function and named lambda declarations can be overloaded; object and type declarations cannot be overloaded. [Example:


double a(int n) {
    [n] p(int m) { return m+n; }
    [n] p(double m) { return m+n; }
    return p(3.0);
}

end example]