All standard references below, unless explicitly noted otherwise, refers to N4861 (March 2020 post-Prague working draft/C++20 DIS).
The type of a captureless lambda (its closure type) is a structural type
Henceforth, we will refer to the type of the lambda solely as the closure type.
As shown through the standard passages below, the closure type of a captureless lambda:
- fulfills the requirements for it to be a literal (class) type, and moreover
- fulfills the requirements on a literal type for it to be a structural type,
and may thus be used as the type for a non-type template parameter, such that the example snippet
template<auto v>
constexpr auto identity_v = v;
constexpr auto l1 = [](){};
constexpr auto l2 = identity_v<l1>;
is indeed well-formed.
The closure type of a lambda is a non-union class type
As governed by [expr.prim.lambda.closure]/1 [emphasis mine]
The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type, called the closure type, whose properties are described below.
the closure type is a non-union class type.
The closure type of a captureless lambda is a literal (class) type
As governed by [basic.types]/10 [extract, emphasis mine]
A type is a literal type if it is:
- [...]
- a possibly cv-qualified class type that has all of the following properties:
- it has a constexpr destructor ([dcl.constexpr]),
- it is either a closure type ([expr.prim.lambda.closure]), an aggregate type ([dcl.init.aggr]), or has at least one constexpr
constructor or constructor template (possibly inherited from a base
class) that is not a copy or move constructor,
- if it is a union, at least one of its non-static data members is of non-volatile literal type, and
- if it is not a union, all of its non-static data members and base classes are of non-volatile literal types.
the closure type is a literal type if
- has a constexpr destructor, and if
- all of its non-static data members are of non-volatile literal types.
The closure type of a captureless lambda has no non-static data members, so the latter requirement is fulfilled. What about the former, a constexpr destructor?
Implicitly generated constexpr destructor
As governed by [expr.prim.lambda.closure]/14
The closure type associated with a lambda-expression has an implicitly-declared destructor ([class.dtor]).
the destructor of the closure type is declared implicitly. Furthermore, [/dcl.fct.def.default]/5 describes [extract, emphasis mine]
Explicitly-defaulted functions and implicitly-declared functions are collectively called defaulted functions, and the implementation shall provide implicit definitions for them ([class.ctor], [class.dtor], [class.copy.ctor], [class.copy.assign]), [...]
that the collective term defaulted functions also include implicitly-declared destructors.
Finally, [class.dtor]/9
A defaulted destructor is a constexpr destructor if it satisfies the requirements for a constexpr destructor ([dcl.constexpr]).
describe that defaulted destructors are constexpr destructors if they fulfill the requirements of [dcl.constexpr], particularly [dcl.constexpr]/3 and [dcl.constexpr]/5 [extracts, emphasis mine]
[dcl.constexpr]/3 The definition of a constexpr function shall satisfy the following requirements:
- [...]
- if the function is a constructor or destructor, its class shall not have any virtual base classes;
- [...]
[dcl.constexpr]/5 The definition of a constexpr destructor whose function-body is not = delete shall additionally satisfy the following requirement:
- for every subobject of class type or (possibly multi-dimensional) array thereof, that class type shall have a constexpr destructor.
all of which are fulfilled for the closure type of a captureless lambda (no base classes, and no subobjects; see [intro.object]/2 for the latter).
Thus, the closure type of captureless lambda is a literal type.
The closure type of a captureless lambda is a structural type
As per [temp.param]/6 and [temp.param]/7 [extract, emphasis mine]
[temp.param]/6 A non-type template-parameter shall have one of the following (possibly cv-qualified) types:
- a structural type (see below),
- [...]
[temp.param]/7
A structural type is one of the following:
- a scalar type, or
- an lvalue reference type, or
- a literal class type with the following properties:
- all base classes and non-static data members are public and non-mutable and
- the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof.
a literal class type is trivially a structural type if it has no base classes and no non-static data members. Both of these holds for a captureless lambda, and thus, the closure type of a captureless lambda is a structural type.
Some notes on the original intent of allowing the closure type of a lambda to be a literal type
N4487 proposed allowing certain lambda-expressions and operations on certain closure objects to appear within constant expressions, and contained a dedicated section to the topic of a closure type being a literal type:
The closure object should be a literal type if the type of each of its data members is a literal type.
A closure type in C++14 can never be a literal type – even if all its
data members are literal types – because it lacks a constexpr
constructor that is not a copy or move constructor. If such a closure
type was allowed to have an implicitly defined default constructor it
would be constexpr, making it a literal type. But, because closure
types, by definition, must have their default constructors deleted,
the implementation is prohibited from implicitly defining one. [...]
P0170R1, containing the core wording from N4487, was accepted and implemented for C++17.
At this time (C++14 and C++17), however, a destructor could not be constexpr, and thus there naturally existed no requirement for a literal type to have a constexpr destructor; [basic.types]/10.5.1 in N4140 (C++14) as well as [basic.types]/10.5.1 in N4659 (C++17) instead required destructor to be trivial:
A type is a literal type if it is:
- [...]
- a class type (Clause [class]) that has all of the following properties:
- it has a trivial destructor,
- [...]
P1907R1, accepted for C++20, expanded the requirement for template parameter objects to have constant destruction; [temp.param]/8 [emphasis mine]:
An id-expression naming a non-type template-parameter of class type T denotes a static storage duration object of type const T, known as a template parameter object, whose value is that of the corresponding template argument after it has been converted to the type of the template-parameter. All such template parameters in the program of the same type with the same value denote the same template parameter object. [...] A template parameter object shall have constant destruction.
and, P0784R7, also accepted for C++20, particularly contained the introduction of constexpr destruction, including the update to the requirement for a type to be a literal type; particularly described in an earlier version of the paper, P0784R1:
The proposed rules for constexpr destructors are:
- [...]
- A literal type requires a constexpr destructor (previously, the stronger requirement of a trivial destructor was made)