P1907R1, accepted for C++20, introduced structural types, which are a valid types for non-type template parameter.
GCC and Clang both accepts the following snippet for C++2a:
template<auto v>
constexpr auto identity_v = v;
constexpr auto l1 = [](){};
constexpr auto l2 = identity_v<l1>;
implying that the type of a captureless lambda is a structural type.
As of the resolution to CWG 2845, captureless a lambdas are structural types.
2845. Make the closure type of a captureless lambda a structural type
[...]
Proposed resolution (approved by CWG 2024-02-02):
Change in 7.5.5.2 [expr.prim.lambda.closure] paragraph 3 as follows:
The closure type is not an aggregate type (9.4.2 [dcl.init.aggr]) and not ; it is a structural type (13.2 [temp.param]) if and only if the lambda has no lambda-capture. An implementation may define the closure type differently from ...
As highlighted in @303's answer, CWG 2542, posted 2 years after this initial Q&A, described that the results of not specifying certain details of lambdas results in it being unclear whether the type of a lambda can even be a structural type or not.
All standard references below, unless explicitly noted otherwise, refers to N4861 (March 2020 post-Prague working draft/C++20 DIS).
This answer is kept to show the full details which shed light on the structural type-uncertainty prior to the resolution of CWG 2542.
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. However, prior to CWG 2542, it was underspecified as to whether it fulfilled the requirements to be a structural type.
Consequently, as per the standard prior to CWG 2542, the example snippet
template<auto v> constexpr auto identity_v = v; constexpr auto l1 = [](){}; constexpr auto l2 = identity_v<l1>;
may or may not be well-formed, depending on how an implementor chooses to implement the underlying closure 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.
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
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?
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.
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 private or protected non-static data members. Whilst the first of this holds, it is nowhere specified of that the closure type of a lambda may not have any protected or private non-static data members. Thus, it cannot be read out from the standard whether the closure type of a captureless lambda is a structural type or not, as an implementor is formally allowed to implement the closure type with some internal private non-static data members.
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 typeconst 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)