Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does a compiler treat lambdas differently than regular functions?

I wonder how a compiler treats lambda functions as opposed to regular functions. Even excluding the capture-list, as I think it's called, it seems to behave slightly differently.

For example, as used in my last post, Trying to pass a constexpr lambda and use it to explicitly specify returning type, I used a constexpr lambda and passed it as a regular function parameter. I quote a part of the answer.

Parameters to constexpr functions are not themselves constexpr objects - so you cannot use them in constant expressions.

template <typename Lambda_T>
constexpr static auto foo(Lambda_T l) {
    return std::array<event, (l())>{};
} 
// Compiles with GCC (C++17), though ill-formed (according to the answer of my last post)

Though, what catches my eye is that this does compile passing a constexpr lambda, but does not compile passing a constexpr regular (global) function. What causes this difference? And are there other differences of behaviour of the compiler between regular functions and lambda-functions?

Edit: Example of Lambda-implementation

foo([](){ return 4; }); // C++17 Lambda's are implicitly constexpr

The lambda is basically a wrapper for the value in this case.

Edit: Global function

Whenever a global function is passed, the compiler will complain - as opposed to using a lambda - that this function, regardless of whether it is defined constexpr or not, cannot be used in a constant condition:

prog.cc:8:20: error: 'l' is not a constant expression
like image 262
Lourens Dijkstra Avatar asked Nov 07 '22 21:11

Lourens Dijkstra


1 Answers

Removing some extraneous stuff from your snippet, we get

template<typename T>
constexpr void foo(T t)
{
    constexpr int i = t();
}

constexpr int f() { return 42; }
auto l = []{ return 42; }

Remarks:

  1. You are attempting to use t() as a constexpr within foo. foo could in fact be a normal function and still behave the same.

  2. A lambda isn't a function. It is an anonymous struct with an operator().

    struct L { constexpr int operator()() const { return 42; } };
    

    T is deduced as a type equivalent to L in the call foo(l).

  3. T is deduced as int(*)() in the call foo(f).

  4. In both cases, t isn't a constexpr within foo.


From [expr.const]

An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract machine, would evaluate one of the following expressions: [...]

  • an lvalue-to-rvalue conversion unless [...]

    • a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of such an object, or

    • a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e;

Calling through a int(*)() requires a lvalue-to-rvalue conversion. t isn't an object defined with constexpr, neither did it begin its lifetime within the evaluation of t(). Therefore t() isn't a constexpr in foo(f).

Calling operator() doesn't require a lvalue-to-rvalue conversion. Since there is no member access, it is simply a constexpr function call. Therefore t() is a constexpr in foo(l).

There is one more step from core constant expressions to constant expressions, but isn't important for the discussion here.

like image 51
Passer By Avatar answered Nov 15 '22 06:11

Passer By