How C++ Lambda Function Is Compiled

Lambda function is a convenient way of defining an anonymous function in the place where it is invoked or assigned as an argurment or variable. It is largely used to pass a short stateless function to an algorithm (comparison to sort algorithm), and as a callback from asynchronous methods. I wonder how lambda functions are compiled. Are they functions with a random name? Are they inline functions expressing simple logics?

Lets start with a minimalistic case.

int main(int argc, char *argv[])
{
    return []() {
        return 6;
    } ();
}

It calls a lambda function which does nothing else than returning an integer 6. The corresponding assembly shows that these codes generate 2 functions: the main and the lambda function.

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14    sdk_version 10, 14
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    subq    $32, %rsp
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    leaq    -24(%rbp), %rdi
    callq   __ZZ4mainENK3$_0clEv
    addq    $32, %rsp
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function
    .p2align    4, 0x90         ## -- Begin function _ZZ4mainENK3$_0clEv  (lambda function)
__ZZ4mainENK3$_0clEv:                   ## @"_ZZ4mainENK3$_0clEv"
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, -8(%rbp)
    movl    $6, %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

The "anonymous" function does have a name, which is _ZZ4mainENK3$_0clEv. Let's demangle it to see what does it mean.

➜  lambda_cpp nm lambda.out | c++filt
0000000100000fa0 unsigned short main::$_0::operator()() const
0000000100000000 T __mh_execute_header
0000000100000f70 T _main
                 U dyld_stub_binder

It is defined as a const function under the namespace of "main", returing an unsigned short, which is obviously deduced from the constant "6". The name "$_0::operator()()" seems like a wrapper class "$0" with a function "operator()()". The name $_0 may suggest a auto-increasing key for each lambda function in the same scope. Lets try define multiple lambda function and see how they are named.

int main(int argc, char *argv[])
{
    auto funcA = []() {
        return 1;
    };

    auto funcB = []() {
        return 2;
    };

    auto funcC = []() {
        return 3;
    };

    return funcA() + funcB() + funcC();
}

This time we got main::$_[0-2]::operator()() functions. That wrapper class is in fact a number. The naming convention is under the namespace where it is defined, a wrapper class with an increasing number to disinguish one from another, then the function itself "operator ()" with its arguments' list.

0000000100000f80 unsigned short main::$_0::operator()() const
0000000100000f90 unsigned short main::$_1::operator()() const
0000000100000fa0 unsigned short main::$_2::operator()() const

Now let's try adding parameters into the lambda function, starting with the capture clause "[]". We put a local variable into the capture clause so that it can be used inside the lambda function. In this example we use the copy capture clause, which is the default behavior.

int main(int argc, char *argv[])
{
    int var = 7;
    auto funcA = [=]() {
        return var;
    };
    return funcA();
}

From the output code we can see in this case it is exactly the same sequence as a regular function call: copy local variable into stack, move the stack upward and then call the function.

    movl    $7, -20(%rbp)
    movl    -20(%rbp), %edi
    movl    %edi, -24(%rbp)
    leaq    -24(%rbp), %rdi
    callq   __ZZ4mainENK3$_0clEv

Meanwhile in lambda function has just a normal function body, getting the argument from stack and copy it into the return register.

    .p2align    4, 0x90         ## -- Begin function _ZZ4mainENK3$_0clEv
__ZZ4mainENK3$_0clEv:                   ## @"_ZZ4mainENK3$_0clEv"
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rdi
    movl    (%rdi), %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

By changing the capture clause to reference mode, we get almost the same code but this time it is the address of variable pushed into the stack.

    auto funcA = [=]() {
        return var;
    };
    ...
    movl    $7, -20(%rbp)
    leaq    -20(%rbp), %rsi     ## address of "var" into %rsi
    movq    %rsi, -32(%rbp)
    leaq    -32(%rbp), %rdi
    callq   __ZZ4mainENK3$_0clEv
    ...

    .p2align    4, 0x90         ## -- Begin function _ZZ4mainENK3$_0clEv
__ZZ4mainENK3$_0clEv:                   ## @"_ZZ4mainENK3$_0clEv"
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rdi
    movq    (%rdi), %rdi
    movl    (%rdi), %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

Now let's try constexpr on lambda expression. The constexpr objects will be evaluated during compilation time and reduced to a constant in the final code. Let's see.

int main(int argc, char *argv[])
{
    auto funcA = [&](int a) constexpr
    {
        return a + 3;
    };
    constexpr int result = funcA(7);
    return result;
}

The lambda function is decalred as constexpr, thus during compilation time, the result of funcA(7) can be calculated as 10. The code is then equivalent to:

int main(int argc, char *argv[])
{
    constexpr int result = 10;
    return result;
}

That leads to an integer 10 in stack movl $10, -28(%rbp) and a direct return of 10 movl $10, %eax:

    .section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 14    sdk_version 10, 14
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    movl    $10, -28(%rbp)
    movl    $10, %eax
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols