1. Note
R0 is the initial, and only, revision of this paper. D3031R0 (draft revision 6 dated 2023-11-03) was discussed by EWG in Kona 2023 (minutes), and EWG wisely decided to take the "punt" option as presented. MSVC’s behavior is cool and nice, but requires a design paper if we actually want to pursue it. The post-Kona mailing contains P3031R0 (draft revision 7, i.e., including the note you are now reading) only for the official record; this paper demands no further action from anyone.
2. Background
"Deducing this" allows lambdas to have explicit object parameters. Microsoft and Clang both support the feature already, but with differing semantics.
struct Any { Any ( auto ) { puts ( "called" ); } }; auto a1 = []( int x ) { return x + 1 ; }; auto a2 = []( this Any self , int x ) { return x + 1 ; };
Here
’s closure type is certainly equivalent to
template < class T > using Just = T ; struct A1 { int operator ()( int x ) const { return x + 1 ; } operator Just < int ( * )( int ) > () const { return + []( int x ) { return A1 ()( x ); }; } };
But it’s unclear which of the following two options should correspond to
’s closure type:
struct A2_MSVC { int operator ()( this Any self , int x ) { return x + 1 ; } operator Just < int ( * )( int ) > () const { return + []( int x ) { return A2_MSVC ()( x ); }; } }; struct A2_Clang { int operator ()( this Any self , int x ) { return x + 1 ; } operator Just < int ( * )( Any , int ) > () const { return & A2_Clang :: operator (); } };
MSVC’s version is friendlier: it allows us to refactor an implicit-object-parameter lambda
into an explicit-object-parameter lambda (or vice versa) without changing the type of
the expression
. MSVC also allows us to write a recursive captureless lambda taking
itself as a function pointer; this is impossible on Clang, because the function pointer’s
type would recursively involve its own type.
Before | After |
---|---|
|
|
|
|
But if you try
as written, you’ll find that it is ambiguous:
A2_MSVC a2 ; a2 ( 1 ); // ill-formed, ambiguous
There are two candidates for this call: (1) call
and then
,
or (2) call
and then the built-in
on that function pointer.
We need new core wording to prefer (1) over (2).
Why don’t ordinary (implicit-object-parameter) lambdas suffer from this ambiguity? I think it’s because
the identity conversion binding
always wins over the user-defined conversion to function pointer.
But for the explicit-object-parameter lambda
, binding
to
is also a user-defined
conversion.
2.1. Is []( this auto ){}
generic?
According to [expr.prim.lambda.general]/6,
a lambda becomes "generic" when any of its parameters involve placeholder types — even when the
only placeholder type is the explicit object parameter’s! In other words,
below is technically
considered a generic lambda.
auto b = []( this auto self , int x ) { return x + 1 ; };
It is unclear which of the following three options should correspond to
’s closure type.
struct B_Clang { template < class T > int operator ()( this T self , int x ) { return x + 1 ; } template < class T > operator Just < int ( * )( T , int ) > () const { return & B_Clang :: operator (); } }; struct B_Generic { template < class T > int operator ()( this T self , int x ) { return x + 1 ; } template < class T > operator Just < int ( * )( int ) > () const { return + []( int x ) { return B_Generic ()( x ); }; } }; struct B_MSVC { template < class T > int operator ()( this T self , int x ) { return x + 1 ; } operator Just < int ( * )( int ) > () const { return + []( int x ) { return B_MSVC ()( x ); }; } };
MSVC’s version is friendliest:
auto b = []( this auto self , int x ) { return x + 1 ; }; int ( * pb1 )( int ) = b ; // Generic+MSVC: OK; Clang: error int ( * pb2 )( decltype ( b ), int ) = b ; // Clang: OK; Generic+MSVC: error auto pb3 = + b ; // MSVC: OK; Generic+Clang: error
This suggests that a
parameter shouldn’t suffice to make a lambda "generic."
But see the next section.
2.1.1. Is this auto
different from this T
?
Zhihao Yuan argues that it is counterintuitive for MSVC to treat these two spellings differently:
auto a = []( this auto ) {}; // "non-generic" on MSVC auto b = [] < class T > ( this T ) {}; // generic auto pa = + a ; // OK on MSVC auto pb = + b ; // error on MSVC void ( * qa )() = a ; // OK on MSVC void ( * qb )() = b ; // error on MSVC
If
is considered generic, then so should be
; we shouldn’t
carve out an exception for the latter.
2.1.2. Should we allow ( + c )()
to act differently from c ()
?
Gašper Ažman provides this example of a lambda that can’t be called directly:
auto c = []( this auto self ) -> int { return self . value ; }; using C = decltype ( c ); struct Derived : C { int value = 2 ; };
Here it is a (SFINAE-unfriendly) error to instantiate
,
but it is OK to instantiate
. Therefore both
and
are errors, but this is OK:
static_assert ( std :: is_convertible_v < C , int ( * )() > ); // OK on MSVC static_assert ( std :: is_convertible_v < C , int ( * )( C ) > ); // OK on Clang Derived d ; int two = d (); // OK int ( * p )() = d ; // error on Clang+MSVC int ( * p )( Derived ) = d ; // OK on Clang, error on MSVC
On Clang,
fails to deduce the template parameter to
’s conversion function template (SFINAE-friendly).
On MSVC,
unambiguously calls the non-template conversion function inherited from
,
which hard-errors during instantiation of
.
A more problematic variation is:
auto c2 = []( this auto self ) { return sizeof ( self ); }; struct Derived2 : decltype ( c2 ) { int pad ; } d2 ; assert ( d2 () == 4 ); assert (( + d2 )() == 1 );
Here
, but
points to a function that returns
.
This example suggests that the conversion function should
not exist for explicit-object lambdas (i.e., the "Punt" wording option below).
Two other problematic cases are
(no inheritance):
auto c3 = []( this auto && self ) { return std :: is_rvalue_reference_v < decltype ( self ) > ; }; assert ( c3 () == false); assert (( + c3 )() == true);
and
(a non-generic lambda):
struct Evil { int i ; Evil ( auto x ) : i ( sizeof ( x )) {} }; auto c4 = []( this Evil self ) { return self . i ; }; struct Derived4 : decltype ( c4 ) { int pad ; } d4 ; assert ( d4 () == 4 ); assert (( + d4 )() == 1 );
Maybe these are obscure enough problems that we don’t care? Or, on the other hand, maybe
we should make a solid rule that whenever
is well-formed, it is guaranteed to have the same behavior
as
. Explicit-object lambdas cannot provide that guarantee, and therefore we cannot give them conversion functions
(i.e., the "Punt" wording option below).
2.1.3. This lambda can only be passed its own type
Consider this case, which (having a template-parameter-list) is clearly a generic lambda:
auto c = [] < class T > ( this T self , T x ) { std :: cout << x ; };
It is unclear which of the following three options should correspond to
’s closure type:
struct C_Clang { template < class T > void operator ()( this T self , T x ) const { std :: cout << x ; } template < class T > operator Just < void ( * )( T , T ) > () const { return & C_Clang :: operator (); } }; struct C_MSVC { template < class T > void operator ()( this T self , T x ) const { std :: cout << x ; } template < class T > operator Just < void ( * )( T ) > () const { return + []( auto x ) { std :: cout << x ; }; } }; struct C_Constrained { template < class T > void operator ()( this T self , T x ) const { std :: cout << x ; } template < class T > operator Just < void ( * )( T ) > () const requires std :: is_same_v < C_Constrained , T > { return + []( auto x ) { std :: cout << x ; }; } };
Here MSVC’s version is friendly, but confusing, because MSVC rightly rejects
but accepts
! So the function pointer that MSVC returns from
is not
in fact "invoking the closure type’s function call operator on a default-constructed instance
of the closure type" — that wouldn’t compile! Do we need the conversion function template
to be constrained? and if so, should it be constrained as in
, or otherwise?
3. Implementation experience
As far as I can tell, we have implementation experience of both Clang’s approach (in Clang) and MSVC’s approach (in MSVC) — although I don’t fully understand what MSVC is doing internally to avoid the overload-resolution ambiguity. But MSVC’s approach seems to be implementable, since it’s been implemented.
4. Options for proposed wording
Note: Throughout, the Standard’s chosen examples rarely seem on-point. I’d like to add more relevant examples and eliminate some of the examples already present.
4.1. Punt for now
Modify [expr.prim.lambda.general]/6 as follows:
6․ A lambda is a generic lambda if the lambda-expression has any generic parameter type placeholders ([dcl.spec.auto]), or if the lambda has a template-parameter-list.
[Example 4:
— end example]int i = []( int i , auto a ) { return i ; }( 3 , 4 ); // OK, a generic lambda int j = []<class T>(T t, int i) { return i; }(3, 4); // OK, a generic lambda auto x = []( int i , auto a ) { return i ; }; // OK, a generic lambda auto y = []( this auto self , int i ) { return i ; }; // OK, a generic lambda auto z = [] < class T > ( int i ) { return i ; }; // OK, a generic lambda
Modify [expr.prim.lambda.closure]/9 as follows:
9․ The closure type for a non-generic lambda-expression with no lambda-capture and no explicit object parameter ([dcl.fct]) whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type’s function call operator. The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. If the function call operator is a static member function, then the value returned by this conversion function is the address of the function call operator. Otherwise, the value returned by this conversion function is the address of a function
that, when invoked, has the same effect as invoking the closure type’s function call operator on a default-constructed instance of the closure type.
F is a constexpr function if the function call operator is a constexpr function and an immediate function if the function call operator is an immediate function.
F 10․ For a generic lambda with no lambda-capture and no explicit object parameter, the closure type has a conversion function template to pointer to function. The conversion function template has the same invented template parameter list, and the pointer to function has the same parameter types, as the function call operator template. The return type of the pointer to function shall behave as if it were a decltype-specifier denoting the return type of the corresponding function call operator template specialization.
11․ [Note 4: If the generic lambda has no trailing-return-type or the trailing-return-type contains a placeholder type, return type deduction of the corresponding function call operator template specialization has to be done. The corresponding specialization is that instantiation of the function call operator template with the same template arguments as those deduced for the conversion function template. Consider the following:
auto glambda = []( auto a ) { return a ; }; int ( * fp )( int ) = glambda ; The behavior of the conversion function of
above is like that of the following conversion function:
glambda — end note]struct Closure { template < class T > auto operator ()( T t ) const { /* ... */ } template < class T > static auto lambda_call_operator_invoker ( T a ) { // forwards execution to operator()(a) and therefore has // the same return type deduced /* ... */ } template < class T > using fptr_t = decltype ( lambda_call_operator_invoker ( declval < T > ())) ( * )( T ); template < class T > operator fptr_t < T > () const { return & lambda_call_operator_invoker ; } }; [Example 6:
— end example]void f1 ( int ( * )( int )) { } void f2 ( char ( * )( int )) { }
void g ( int ( * )( int )) { } // #1 void g ( char ( * )( char )) { } // #2
void h ( int ( * )( int )) { } // #3 void h ( char ( * )( int )) { } // #4
auto glambda = []( auto a ) { return a ; }; f1 ( glambda ); // OK f2 ( glambda ); // error: ID is not convertible g ( glambda ); // error: ambiguous h ( glambda ); // OK, calls #3 since it is convertible from ID int & ( * fpi )( int * ) = []( auto * a ) -> auto & { return * a ; }; // OK 12․ If the function call operator template is a static member function template, then the value returned by any given specialization of this conversion function template is the address of the corresponding function call operator template specialization. Otherwise, the value returned by any given specialization of this conversion function template is the address of a function
that, when invoked, has the same effect as invoking the generic lambda’s corresponding function call operator template specialization on a default-constructed instance of the closure type.
F is a constexpr function if the corresponding specialization is a constexpr function and an immediate function if the function call operator template specialization is an immediate function.
F [Note 5: This will result in the implicit instantiation of the generic lambda’s body. The instantiated generic lambda’s return type and parameter types are required to match the return type and parameter types of the pointer to function. — end note]
[Example 7:
— end example]auto GL = []( auto a ) { std :: cout << a ; return a ; }; int ( * GL_int )( int ) = GL ; // OK, through conversion function template GL_int ( 3 ); // OK, same as GL(3) 13․ The conversion function or conversion function template is public, constexpr, non-virtual, non-explicit, const, and has a non-throwing exception specification.
[Example 8:
— end example]auto Fwd = []( int ( * fp )( int ), auto a ) { return fp ( a ); }; auto C = []( auto a ) { return a ; };
static_assert ( Fwd ( C , 3 ) == 3 ); // OK
// No specialization of the function call operator template can be constexpr (due to the local static). auto NC = []( auto a ) { static int s ; return a ; }; static_assert ( Fwd ( NC , 3 ) == 3 ); // error
4.2. Clang’s approach
Modify [expr.prim.lambda.general]/6 as follows:
6․ A lambda is a generic lambda if the lambda-expression has any generic parameter type placeholders ([dcl.spec.auto]), or if the lambda has a template-parameter-list.
[Example 4:
— end example]int i = []( int i , auto a ) { return i ; }( 3 , 4 ); // OK, a generic lambda int j = []<class T>(T t, int i) { return i; }(3, 4); // OK, a generic lambda auto x = []( int i , auto a ) { return i ; }; // OK, a generic lambda auto y = []( this auto self , int i ) { return i ; }; // OK, a generic lambda auto z = [] < class T > ( int i ) { return i ; }; // OK, a generic lambda
Modify [expr.prim.lambda.closure]/9 as follows:
9․ The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type’s function call operator, except that if the function call operator has an explicit object parameter of type
, then the function type has a leading parameter of type
T . The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. If the function call operator is a static member function or explicit object member function, then the value returned by this conversion function is the address of the function call operator. Otherwise, the value returned by this conversion function is the address of a function
T that, when invoked, has the same effect as invoking the closure type’s function call operator on a default-constructed instance of the closure type.
F is a constexpr function if the function call operator is a constexpr function and an immediate function if the function call operator is an immediate function.
F 10․ For a generic lambda with no lambda-capture, the closure type has a conversion function template to pointer to function. The conversion function template has the same invented template parameter list, and the pointer to function has the same parameter types, as the function call operator template, except that if the function call operator template has an explicit object parameter of type
, then the function type has a leading parameter of type
T . The return type of the pointer to function shall behave as if it were a decltype-specifier denoting the return type of the corresponding function call operator template specialization.
T 11․ [Note 4: If the generic lambda has no trailing-return-type or the trailing-return-type contains a placeholder type, return type deduction of the corresponding function call operator template specialization has to be done. The corresponding specialization is that instantiation of the function call operator template with the same template arguments as those deduced for the conversion function template. Consider the following:
auto glambda = []( auto a ) { return a ; }; int ( * fp )( int ) = glambda ; The behavior of the conversion function of
above is like that of the following conversion function:
glambda — end note]struct Closure { template < class T > auto operator ()( T t ) const { /* ... */ } template < class T > static auto lambda_call_operator_invoker ( T a ) { // forwards execution to operator()(a) and therefore has // the same return type deduced /* ... */ } template < class T > using fptr_t = decltype ( lambda_call_operator_invoker ( declval < T > ())) ( * )( T ); template < class T > operator fptr_t < T > () const { return & lambda_call_operator_invoker ; } }; [Example 6:
— end example]void f1 ( int ( * )( int )) { } void f2 ( char ( * )( int )) { }
void g ( int ( * )( int )) { } // #1 void g ( char ( * )( char )) { } // #2
void h ( int ( * )( int )) { } // #3 void h ( char ( * )( int )) { } // #4
auto glambda = []( auto a ) { return a ; }; f1 ( glambda ); // OK f2 ( glambda ); // error: ID is not convertible g ( glambda ); // error: ambiguous h ( glambda ); // OK, calls #3 since it is convertible from ID int & ( * fpi )( int * ) = []( auto * a ) -> auto & { return * a ; }; // OK 12․ If the function call operator template is a static member function template or explicit object member function, then the value returned by any given specialization of this conversion function template is the address of the corresponding function call operator template specialization. Otherwise, the value returned by any given specialization of this conversion function template is the address of a function
that, when invoked, has the same effect as invoking the generic lambda’s corresponding function call operator template specialization on a default-constructed instance of the closure type.
F is a constexpr function if the corresponding specialization is a constexpr function and an immediate function if the function call operator template specialization is an immediate function.
F [Note 5: This will result in the implicit instantiation of the generic lambda’s body. The instantiated generic lambda’s return type and parameter types are required to match the return type and parameter types of the pointer to function. — end note]
[Example 7:
— end example]auto GL = []( auto a ) { std :: cout << a ; return a ; }; int ( * GL_int )( int ) = GL ; // OK, through conversion function template GL_int ( 3 ); // OK, same as GL(3) 13․ The conversion function or conversion function template is public, constexpr, non-virtual, non-explicit, const, and has a non-throwing exception specification.
[Example 8:
— end example]auto Fwd = []( int ( * fp )( int ), auto a ) { return fp ( a ); }; auto C = []( auto a ) { return a ; };
static_assert ( Fwd ( C , 3 ) == 3 ); // OK
// No specialization of the function call operator template can be constexpr (due to the local static). auto NC = []( auto a ) { static int s ; return a ; }; static_assert ( Fwd ( NC , 3 ) == 3 ); // error
4.3. MSVC’s approach
Note: This proposed wording doesn’t explain why
should prefer to call the user-defined
instead of using the builtin operator on the result of the lambda’s non-template conversion function.
I’m hoping someone at Microsoft can shed light on how MSVC tiebreaks this internally.
Modify [expr.prim.lambda.general]/6 as follows:
6․ A lambda is a generic lambda if the lambda-expression has any generic non-object parameter type placeholders ([dcl.spec.auto]), or if the lambda has a template-parameter-list.
[Example 4:
— end example]int i = []( int i , auto a ) { return i ; }( 3 , 4 ); // OK, a generic lambda int j = []<class T>(T t, int i) { return i; }(3, 4); // OK, a generic lambda auto w = []( int i , auto a ) { return i ; }; // OK, a generic lambda auto x = []( this auto self , int i ) { return i ; }; // OK, a non-generic lambda auto y = []( this auto self , auto a ) { return i ; }; // OK, a generic lambda auto z = [] < class T > ( int i ) { return i ; }; // OK, a generic lambda
Modify [expr.prim.lambda.closure]/4 as follows:
The closure type for a lambda-expression has a public inline function call operator
(for a non-generic lambda)or function call operator template(for a generic lambda)([over.call]) whose parameters and return type are those of the lambda-expression’s parameter-declaration-clause and trailing-return-type respectively, and whose template-parameter-list consists of the specified template-parameter-list, if any. The requires-clause of the function call operator template is the requires-clause immediately followingtemplate-parameter-list
< , if any. The trailing requires-clause of the function call operator or operator template is the requires-clause of the lambda-declarator, if any.
> [Note 2: The function call operator template
for a generic lambdacan be an abbreviated function template ([dcl.fct]). — end note]
Modify [expr.prim.lambda.closure]/9 as follows:
9․ The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type’s function call operator (omitting the object parameter, if any). The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. If the function call operator is a static member function, then the value returned by this conversion function is the address of the function call operator. Otherwise, the value returned by this conversion function is the address of a function
that, when invoked, has the same effect as invoking the closure type’s function call operator on a default-constructed instance of the closure type.
F is a constexpr function if the function call operator is a constexpr function and an immediate function if the function call operator is an immediate function.
F 10․ For a generic lambda with no lambda-capture, the closure type has a conversion function template to pointer to function. The conversion function template has the same invented template parameter list as the function call operator template (omitting the invented template-parameter corresponding to the function call operator’s explicit object parameter, if any), and the pointer to function has the same parameter types
,as the function call operator template (omitting the object parameter, if any). The return type of the pointer to function shall behave as if it were a decltype-specifier denoting the return type of the corresponding function call operator template specialization.11․ [Note 4: If the generic lambda has no trailing-return-type or the trailing-return-type contains a placeholder type, return type deduction of the corresponding function call operator template specialization has to be done. The corresponding specialization is that instantiation of the function call operator template with the same template arguments as those deduced for the conversion function template. Consider the following:
auto glambda = []( auto a ) { return a ; }; int ( * fp )( int ) = glambda ; The behavior of the conversion function of
above is like that of the following conversion function:
glambda — end note]struct Closure { template < class T > auto operator ()( T t ) const { /* ... */ } template < class T > static auto lambda_call_operator_invoker ( T a ) { // forwards execution to operator()(a) and therefore has // the same return type deduced /* ... */ } template < class T > using fptr_t = decltype ( lambda_call_operator_invoker ( declval < T > ())) ( * )( T ); template < class T > operator fptr_t < T > () const { return & lambda_call_operator_invoker ; } }; [Example 6:
— end example]void f1 ( int ( * )( int )) { } void f2 ( char ( * )( int )) { }
void g ( int ( * )( int )) { } // #1 void g ( char ( * )( char )) { } // #2
void h ( int ( * )( int )) { } // #3 void h ( char ( * )( int )) { } // #4
auto glambda = []( auto a ) { return a ; }; f1 ( glambda ); // OK f2 ( glambda ); // error: ID is not convertible g ( glambda ); // error: ambiguous h ( glambda ); // OK, calls #3 since it is convertible from ID int & ( * fpi )( int * ) = []( auto * a ) -> auto & { return * a ; }; // OK 12․ If the function call operator template is a static member function template, then the value returned by any given specialization of this conversion function template is the address of the corresponding function call operator template specialization. Otherwise, the value returned by any given specialization of this conversion function template is the address of a function
that, when invoked, has the same effect as invoking the generic lambda’s corresponding function call operator template specialization on a default-constructed instance of the closure type.
F is a constexpr function if the corresponding specialization is a constexpr function and an immediate function if the function call operator template specialization is an immediate function.
F [Note 5: This will result in the implicit instantiation of the generic lambda’s body. The instantiated generic lambda’s return type and non-object parameter types are required to match the return type and parameter types of the pointer to function. — end note]
[Example 7:
— end example]auto GL = []( auto a ) { std :: cout << a ; return a ; }; int ( * GL_int )( int ) = GL ; // OK, through conversion function template GL_int ( 3 ); // OK, same as GL(3) 13․ The conversion function or conversion function template is public, constexpr, non-virtual, non-explicit, const, and has a non-throwing exception specification.
[Example 8:
— end example]auto Fwd = []( int ( * fp )( int ), auto a ) { return fp ( a ); }; auto C = []( auto a ) { return a ; };
static_assert ( Fwd ( C , 3 ) == 3 ); // OK
// No specialization of the function call operator template can be constexpr (due to the local static). auto NC = []( auto a ) { static int s ; return a ; }; static_assert ( Fwd ( NC , 3 ) == 3 ); // error
5. Straw polls taken in EWG, 2023-11-07
SF | F | N | A | SA | |
---|---|---|---|---|---|
Move forward with the option to "punt" from D3031, forwarding to CWG for C++26. A subsequent paper can explore other solutions. | 9 | 11 | 3 | 0 | 0 |