Can lambdas be used as non-type template parameter? [duplicate]
Asked Answered
B

3

6

Is the following code legal?

template <auto Lambda>
struct A {};

int main () {
  auto lmb = [](int i){return i*i;};
  A<lmb> a;
  return 0;
}

I noticed that g++ compiles it fine, while clang++ returns error: a non-type template parameter cannot have type '(lambda at main.cpp:...)'.

Basal answered 11/6, 2020 at 12:7 Comment(4)
gcc also accepts constexpr auto= ... here. What about the other compilers? Only constexprs can be template parameters (incl. auto).Merta
According to this page, clang doesn't support "Class types as non-type template parameters" yet.Ventriloquy
... where GCC does, as of GCC 9.Refluent
@Sam constexpr works for non-capturing lambdas, they are explictly Literl Types since C++17, but it stops working when you capture something non-constexpr.Meeker
K
4

Can lambdas be used as non-type template parameter?

Yes, with implementations that has implemented P0732R2 - Class types in non-type template parameters but clang++ has not implemented it yet.

Source: https://en.cppreference.com/w/cpp/compiler_support


Note that the lambda needs to be at least constexpr (which it is by default):

When this specifier is not present, the function call operator will be constexpr anyway, if it happens to satisfy all constexpr function requirements.

You can however add constexpr to get an error on the lambda itself instead of when using it as a template parameter. As a side note: You can also specify it to be consteval to make it work as a non-type template parameter.

A stateful lambda can be constexpr:

constexpr auto lmb1 = [](int i) {
    static int x = 0;
    return i*i + ++x;
};

while a lambda capturing by reference, or capturing by copy and mutating (mutable), can not. Capturing by copying a constexpr is ok though.

Generic lambdas may be constexpr too:

constexpr auto gen_lmb = []<typename T>(T& val) {
   val += val;
   return val;
};

template <auto Lambda>
struct A {
    template<typename T>
    void doit(T&& arg) {
        std::cout << Lambda(arg) << '\n';
    }
};

//...

A<gen_lmb> ginst;

int v = 1000;
ginst.doit(v);
ginst.doit(std::string("foo "));
std::cout << v << '\n';
2000
foo foo
2000
Kernan answered 11/6, 2020 at 12:25 Comment(5)
With the acceptance of defect report CWG2542, lambdas are no longer considered to be structural types and can therefore not be used as NTTPs. This means that compilers could implement CWG2542 at any point and reject C++20 (or newer) code that tries to use lambdas as NTTPs.Lynxeyed
@Lynxeyed Thanks! That's interesting. I've read the link you shared and different standards but I can't figure out if this DR is supposed to be applied retroactively or not. The new wording is in C++26 standard so from there on I guess it's clear but what about 20 and 23? If you have any additional info I'd gladly get some links to read so that I can make a proper update of the answer.Kernan
I'm not too sure about that either. However, it is clear that the DR is accepted and the proposed solution is approved. This also seems to be reflected by the corresponding cppreferences entry on closures: "The lambda expression is a prvalue expression of unique unnamed non-union non-aggregate non-structural class type ..." Which is stated as the core text of that page without having any specific C++ version stamped on it. ...Lynxeyed
... In any case, considering lambdas to be of structural types has always been a bit of a stretch to some degree, as this was never clearly stated in the standard to begin with (hence the defect report, of course). So, to the very least, the use of lambdas as structural types should be considered deprecated and be avoided for any C++ version.Lynxeyed
@Lynxeyed Yes, it's probably wise to avoid them even in C++20 and 23 now with this accepted DR. I think I'll need to do some more digging before formulating an update to the answer. The wording changes regarding this hits "all over" the place in the standards be so it's easy to get it wrong :)Kernan
S
1

[temp.arg.nontype]/1:

If the type T of a template-parameter contains a placeholder type ([dcl.spec.auto]) or a placeholder for a deduced class type ([dcl.type.class.deduct]), the type of the parameter is the type deduced for the variable x in the invented declaration

T x = template-argument ;

If a deduced parameter type is not permitted for a template-parameter declaration ([temp.param]), the program is ill-formed.

So, the rules are set by [temp.param]/6:

A non-type template-parameter shall have one of the following (possibly cv-qualified) types: ...

(6.1) a structural type ...

The rules for structural type are: --my emphasis--

(7.1) a scalar type, or

(7.2) an lvalue reference type, or

(7.3) a literal class type with the following properties:

(7.3.1) all base classes and non-static data members are public and non-mutable and

(7.3.2) the types of all bases classes and non-static data members are structural types or (possibly multi-dimensional) array thereof.

Since the lambda has no base class, the only requirement is that it has to be a literal class type ([basic.types]) which includes:

(10.5.2) ... a closure type ([expr.prim.lambda.closure]) ...

The data members of a structural type shall also be structural type, this applies to the lambda's capture in this case, as long as all its members are public and non-mutable.


@Nicol Bolas commented below that a lambda with captures, even if constexpr literal type captures, is not mandated by the standard to manage the captures as public fields.


The bottom line is that in C++20 a constexpr lambda expression without a capture shall be legal as a template non-type argument (based on [basic.types]/10.5.2 mentioned above).

See also an answer by @Barry to a similar question.


Below code compiles with gcc, but as I understand from the comment by Nicol Bolas, not all cases are guaranteed by the spec (or even worse, all cases are not guaranteed by the spec?).


Suppose we have:

template <auto T> struct A {};

struct B {};

struct C {
    ~C(){}
};

Literal type lambdas, that shall be legal as template arguments:

// compiles in gcc and should be ok by the spec as of [basic.types]/10.5.2
A<[](){}> a; // compiler deduces the anonymous lambda to be constexpr

auto lmb1 = [](){};
// same as above
A<lmb1> a1;

// compiler deduces lmb1 above to be constexpr
// same as it will deduce the following:
B b {};
A<b> here_i_am;

Lambdas, that are compiled by gcc as template arguments, but as Nicol Bolas argues in the comment - the spec doesn't guarantee them to be literal types:

const int i = 0;
constexpr auto lmb2 = [i](){};
// compiles in gcc but is not guaranteed by the spec 
A<lmb2> a2;

constexpr auto lmb3 = [b](){}; // B is literal
// compiles in gcc but is not guaranteed by the spec 
A<lmb3> a3;

Non-literal type lambdas, not legal as template arguments:

const int j = 0;
// below doesn't compile: <lambda()>{j} is not a constant expression
constexpr auto lmb4 = [&j](){}; // local reference - not constexpr
A<lmb4> a4;

C c;
// below doesn't compile: <lambda()>'{c} does not have 'constexpr' destructor
constexpr auto lmb5 = [c](){}; // C is not literal
A<lmb5> a5;
Sipe answered 11/6, 2020 at 13:2 Comment(3)
Your analysis takes you to the correct and most interesting passage, whether a given lambda is a literal class type, as of [basic.types]/10.5, but I think the answer skips the most important parts (given the OP's question), namely covering when a lambda not explicitly marked as constexpr is still of a literal class type? E.g. when is the implicitly generated destructor of a lambda ([expr.prim.lambda.closure]/14) constexpr?Refluent
"or with a constexpr literal type captures" No, actually. Strong structural equality, as you quoted, requires that the members be public. Nothing in the standard requires that a lambda's captures are public. So a capturing lambda is not required to be used as an NTTP. Also, there's nothing in the standard that requires that a non-capturing lambda has no members. It can still be a literal type, but without an explicit statement that a non-capturing lambda must have strong structural equality, you cannot rely on it.Piste
@NicolBolas I just found out that this question is already discussed elsewhere, with an answer by @Barry, a partial quote: "captureless lambdas count as structural types ([temp.param]/7) by way of simply not having any data members at all". My humble view on that is the same as Barry.Sipe
M
0

First, I think your lambda should be constexpr to be used as a non-type template parameter. I find it a bit weird, that it works.

But then it should work in this case. The Standard tells us that a non-type template parameter can be a literal class type (that's a bit iffy since closures are literal but not really class types, I think they are explicitly included here) with the additional requirements that

  • All base clases and non-static members must be non-mutable and public
  • Their types must be structural or arrays thereof

So we don't have problems in this easy example. But if you capture anything, your lambda has a non-public member variable and should be out. If that is not so sharp for closures, it definitely stops working if you capture something non-constexpr.

Meeker answered 11/6, 2020 at 12:27 Comment(1)
Lambdas are constexpr by default since c++17Aikido

© 2022 - 2024 — McMap. All rights reserved.