Unified Function Syntax

ISO/IEC JTC1 SC22 WG21 N2954 = 09-0144 - 2009-09-25

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

This paper is a revision of N2931 = 09-0121.

Introduction
New Function Declaration Syntax
Named Lambdas
The Type of Empty–Lambda-Capture Lambdas
Auto with Function Definitions
Proposed Wording
    3.3.2 Point of declaration [basic.scope.pdecl]
    3.5 Program and linkage [basic.link]
    5.1.2 Lambda expressions [expr.prim.lambda]
    7 Declarations [dcl.dcl]
    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]

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), U.K. comment 42, and U.S. comment 43 support 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 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 the earliest papers. This change in approach addresses a concern that earlier proposals did not go far enough to justify any support. This paper presents a unification that includes less capability than in some later papers. This change in approach addresses a concern that those later papers presented too much technical risk.

New Function Declaration Syntax

Based on direction from the September 2008 ISO C++ 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 lambda-declaratoropt compound-statement
function-declaration:
decl-specifier-seqopt lambda-introducer id-expression lambda-declarator

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 still 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.

Named Lambdas

The new function syntax at block scope named lambdas.


int h(int b) {
    [] m(int z)->int
        { return x+z; } // b is not in scope
    [&] n(int z)->int
        { 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);
}

The advantage to named lambdas over this auto variable approach is that it is possible to do forward declarations and overloading. However, due to technical risk, those possibilities are not supported at this time.

Due to concerns about the effects of argument-dependent lookup, no operator names are permitted on named lambdas.

The Type of Empty–Lambda-Capture Lambdas

The semantics of a lambda with an empty capture list and a function are nearly uniform. This uniformity was noted in U.K. national body comment 226. Exposing this uniformity would allow programmers to use lambdas and exploit existing function-based interfaces.

Problem: A lambda with an empty capture list has identical semantics to a regular function type. By requiring this mapping we get an efficient lambda type with a known API that is also compatible with existing operating system and C library functions.

Resolution: Add a new paragraph: "A lambda expression with an empty capture set shall be convertible to pointer to function type R(P), where R is the return type and P is the parameter-type-list of the lambda expression." Additionally it might be good to (a) allow conversion to function reference and (b) allow extern "C" function pointer types.

We adopt the suggested resolution.

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 and constexpr functions, 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; }

Due to technical risk, the return type must consist only of auto.

Proposed Wording

The proposed wording shows changes from working draft N2914, presuming that the edits from N2927 New wording for C++0x Lambdas (rev 2) have been already applied.

3.3.2 Point of declaration [basic.scope.pdecl]

Edit paragraph 9 as follows. The intent is to enable named lambdas while preserving existing semantics for existing code.

[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.5 Program and linkage [basic.link]

Edit within 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 that are not a 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.

5.1.2 Lambda expressions [expr.prim.lambda]

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

lambda-expression:
lambda-introducer lambda-declaratoropt 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-declarator:
( parameter-declaration-clause ) attribute-specifieropt mutableopt exception-specificationopt trailing-return-typeopt
parameters-and-qualifiers trailing-return-typeopt

Edit paragraph 4 as follows.

If a lambda-expression does not include a lambda-declarator, it is as if the lambda-declarator were (). If a lambda-expression does not include a trailing-return-type, or if the trailing-return-type is ->auto, it is as if the trailing-return-type denotes the following type:

[Example:

auto x1 = [](int i){ return i; };
// OK: return type is int
auto x2 = []{ return { 1, 2 }; };
// error: the return type is void (a braced-init-list is not an expression)

end example]

Edit paragraph 5 as follows. The intent of this edit is to obtain a closure-to-function-pointer conversion with no lambda-capture and to restrict the qualifiers available from the now more general syntax for parameters-and-qualifiers.

The closure type for a lambda-expression has a public inline function call operator (_over.call_ 13.5.4) whose parameters and return type are described by the lambda-expression's parameter-declaration-clause and trailing-return-type respectively. A program taking the address of this function call operator is ill-formed, diagnostic required. This function call operator is declared const (_class.mfct.non-static_ 9.3.1) if and only if the lambda-expression's parameter-declaration-clause is not followed by mutable. When there is no lambda-capture, the mutable qualifier shall not occur. There shall be no cv-qualifier-seq or ref-qualifier following the parameter-declaration-clause. It The function call operator is not declared volatile. Default arguments (_decl.fct.default_ 8.3.6) shall not be specified in the parameter-declaration-clause of a lambda-declarator. Any exception-specification specified on a lambda-expression applies to the corresponding function call operator. Any attribute-specifiers appearing immediately after the lambda-expression's parameter-declaration-clause appertain to the type of the corresponding function call operator. [Note: Names referenced in the lambda-declarator are looked up in the context in which the lambda-expression appears. —end note]

Add a new paragraph after paragraph 5.

The closure type for a lambda-expression with no lambda-capture has a public conversion operator whose conversion-type-id is a pointer to a function having the same parameter and result types as the closure type's function call operator. The value returned by this conversion operator shall be the address of a function that, when invoked, has the same effect as invoking the closure type's function call operator.

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
empty-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.6.4 auto specifier [dcl.spec.auto]

Edit paragraph 1 as follows. The intent of this edit is to remove the auto-> syntax of N2541 New Function Declarator Syntax Wording.

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.

The auto type-specifier may appear with as the return type of 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]) and signifies that the return type shall be deduced from the function-body.

Edit within 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]

The edits in this section apply to the text after applying N2927.

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 trailing-return-type
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
function-qualifiers:
attribute-specifieropt mutableopt cv-qualifier-seqopt ref-qualifieropt exception-specificationopt
trailing-return-type:
-> attribute-specifieropt trailing-type-specifier-seq attribute-specifieropt abstract-declaratoropt
ptr-operator:
* attribute-specifieropt cv-qualifier-seqopt
&
&&
::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]

The edits in this section apply to the text after applying N2927.

Edit within paragraph 1 as follows. The intent of this edit is to remove the auto-> syntax.

type-id:
type-specifier-seq attribute-specifieropt abstract-declaratoropt
abstract-declarator:
ptr-abstract-declarator
noptr-abstract-declaratoropt parameters-and-qualifiers -> trailing-return-type
...
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]

The edits in this section apply to the text after applying N2927.

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 parameters-and-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 parameters-and-qualifiers returning T". The optional attribute-specifier of the function-qualifiers of the parameters-and-qualifiers 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 trailing-return-type

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". The optional attribute-specifier appertains to the function type.

Delete paragraph 3 as deleted by N2927. 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.

Function declarations with trailing-return-type have the form:

function-declaration:
decl-specifier-seqopt lambda-introducer id-expression lambda-declarator

[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 is to restrict use of decl-specifiers; to restrict the use of mutable in function-qualifiers; to prohibit operators; and to prohibit forwarding.

The decl-specifier-seq of a function-declaration may contain only those that apply to functions: static, extern, friend, typedef, constexpr, and function-specifier. The function-qualifiers shall contain mutable only when part of a function-declaration or lambda-expression with a non-empty lambda-capture. The id-expression of a function-declaration within block scope shall contain neither a operator-function-id nor a conversion-function-id. A non-extern block-scope function-declaration shall be part of a function-definition. [Footnote: This restriction prohibits forward declarations. —end footnote]

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

Within a lambda-declarator of a function-declaration, a missing trailing-return-type indicates that the return type is void, unless the heading is part of a definition with a function-body of the form { return expression ; }, in which case the return type is deduced as in (5.1.2 [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.

Function-definitions may be local to another function. These definitions are named lambdas. A named lambda defines a variable of closure type equivalent to the corresponding lambda expression. [Example:


[] halvable(int k)->bool {
    [] even(unsigned n)->bool { return (n & 1) == 0; }
    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-capture are described in ([expr.prim.lambda]). A non-empty lambda-capture shall only appear in block scope, but may not appear in extern declarations.

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

[Note: typedefs and trailing-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 trailing-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 simple-type-specifier of the return type of a function or lambda definition may contain the auto type specifier, provided that the form of the function-body is { return expr; }, and that the return type specification contains only the auto specifier. The return type is deduced as in (5.1.2 [expr.prim.lambda]). [Example:


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

end example]

Within paragraph 9, edit as follows.

A function definition of the form: If a function definition has one of the forms:

decl-specifier-seqopt attribute-specifieropt declarator = default ;
function-declaration = default ;

is called an explicitly-defaulted definition. ...

Within paragraph 10, edit as follows.

A function definition of the form: If a function definition has one of the forms:

decl-specifier-seqopt attribute-specifieropt declarator = delete ;
function-declaration = delete ;

is called a deleted definition. ...

13 Overloading [over]

Edit paragraph 1 as follows. The intent is to prohibit 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 declarations can be overloaded; object and type declarations cannot be overloaded. Futhermore, non-extern block-scope function-declarations shall not be overloaded. [Example:

double a(int n) {
    [n] p(int m) { return m+n; }
    [n] p(double m) { return m+n; } // error: local overload
    return p(3.0);
}

end example]