Search code examples
c++closuresanonymous-function

How are closures actually sent for evaluation in lambda expression calls


I'm trying to understand how closures are actually sent for lambda expressions calls:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    int *A = (int *) malloc((argc - 1) * sizeof(int));

    const auto myLovelyLambda = [=]()
    {
        //              ++++ captured
        for (auto i=0;i<argc-1;i++) {
            A[i] = atoi(argv[i+1]);
        //  +           ++++ captured
        //  |           
        //  +- captured
        }
    };

    myLovelyLambda();
    
    for (int i=0;i<argc-1;i++) {
        printf("%d\n", A[i]);
    }

    return 0;
}

When I inspect the generated machine code, I see the captured entities are passed on stack:

$ clang --std=c++17 -g -O0 main.cpp -o main
$ objdump -S -D main > main.asm
$ sed -n "22,31p" main.asm
;     const auto myLovelyLambda = [=]()
100003e6c: b85f83a8     ldur    w8, [x29, #-8]
100003e70: 910043e0     add x0, sp, #16
100003e74: b90013e8     str w8, [sp, #16]       // <--- captured
100003e78: f85e83a8     ldur    x8, [x29, #-24]
100003e7c: f9000fe8     str x8, [sp, #24]       // <--- captured
100003e80: f85f03a8     ldur    x8, [x29, #-16]
100003e84: f90013e8     str x8, [sp, #32].      // <--- captured
;     myLovelyLambda();
100003e88: 9400001c     bl  0x100003ef8 <__ZZ4mainENK3$_0clEv>

Do I have any control of how the compiler manages this closure move?


Solution

  • You have to conceptually separate the initialization of the object of closure type, and the function call. A lambda expression has a corresponding closure type. In your case, myLovelyLambda would translate into something like this:

    // note: this is not a completely accurate representation, it's just for exposition
    class __lambda {
      private:
        int argc;
        char** argv;
        int* A;
      public:
        void operator()() const noexcept {
            for (auto i=0;i<argc-1;i++) {
                A[i] = atoi(argv[i+1]);
            }
        };
    };
    

    Note that the order of argc, argv, and A in the closure type is unspecified according to [expr.prim.lambda] p10:

    For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified.

    Initialization and calling is then transformed like this:

    // const auto myLovelyLambda = [=]() { ... };
       const auto myLovelyLambda = __lambda{argc, argv, A};
    
       myLovelyLambda();   
    

    Do I have any control of how the compiler manages this closure move?

    Not really. You can't have volatile captures in a lambda, so the compiler is free to transform and reorder the initialization of the captures in the object significantly. It can also optimize away the lambda completely through inlining, so that the assembly is indistinguishable form having your for loop directly main.

    It is also unspecified in what order lambda captures are initialized within the closure object, and in what order they are destroyed. See C++11: In what order are lambda captures destructed?. In the end, you can just let the compiler figure it out. In your example, you don't need tight control over captures order.