Search code examples
c++lambdalanguage-lawyerconst-correctness

Why is mutable lambda converted to function pointer instead of calling operator()?


From cppinsights we see how in the following code the line lambda(); is interpreted by the language:

const auto lambda = [] () mutable {};

void foo()
{
    lambda();
}

One would naively think, that this line calls the lambda's (non-const) operator(), which in turn doesn't compile. But, instead of this, the compiler converts the non-capturing lambda to a function pointer, calls the function pointer, and accepts the code.

What's the purpose of this conversion? To me, it would be more logical, if this would be rejected. There is no sign, that the programmer intended this conversion. The language does this on its own.

Note, that the conversion happens only in the above case, where calling lambda.operator()() would discard qualifiers. It does not happen (i.e., operator() is called directly), if lambda is not const, or operator() is not marked mutable.


Solution

  • Let Lambda be the type of the lambda closure. Thus decltype(lambda) is const Lambda.

    The expression lambda() can be interpreted in several ways. It can be – in this case –

    • a call to Lambda::operator(), or
    • a call to a function pointer resulting from converting lambda. Since the capture list of the lambda expression is empty, a conversion function to void (*)() exists (see the rule). We can see it in cppinsights too.

    This is detailed in overload resolution:

    If E in a function call expression E(args) has class type cv T, then

    • The function-call operators of T are obtained by ordinary lookup of the name operator() in the context of the expression (E).operator(), and every declaration found is added to the set of candidate functions.

    • For each non-explicit user-defined conversion function in T or in a base of T (unless hidden), whose cv-qualifiers are the same or greater than T's cv-qualifiers, and where the conversion function converts to:

      • pointer-to-function
      • reference-to-pointer-to-function
      • reference-to-function

      then a surrogate call function with a unique name whose first parameter is the result of the conversion, the remaining parameters are the parameter-list accepted by the result of the conversion, and the return type is the return type of the result of the conversion, is added to the set of candidate functions. If this surrogate function is selected by the subsequent overload resolution, then the user-defined conversion function will be called and then the result of the conversion will be called.

    In this case in E(args) E is lambda, args is empty, and cv T is const Lambda, a class type. For this expression there are two candidates in contest:

    1. void Lambda::operator()(), the non-const function-call operator of the lambda closure, and
    2. void SurrogateFunction(void(*)()), the invented surrogate function.

    Let's rewrite the first candidate in the form of a global function, to make it easier to understand. In this case its first parameter will be the object *this:

    1. void FunctionCallOperator(Lambda&),
    2. void SurrogateFunction(void(*)()).

    The lvalue expression lambda of type const Lambda is tried in these two.

    1. The 1st cannot be called. Conversion would lose qualifiers.
    2. The 2nd can be called with the user-defined conversion of Lambda.

    So, there is no hidden purpose behind what's happening. The conversion function wins the overload resolution, which is a general rule, not specific to lambdas.