C++ Lambda Expressions

In C++, typically functions are defined and called by name. They are associated with an object, or not, but regardless they are named: print_tables(), object.get_items(). With lambda functions or lambda expressions, you now have the option for anonymous functions … that is, a function with no name. Once again, you should consider a lambda essentially shorthand for defining and using a function object. The main components of lambdas are described below.

Capture list - Specifies what names from the definition environment can be used in the lambda expression body and if they are captured by value or reference. Not optional, but possibly empty.

Parameter list - Specifies which arguments the lambda expression requires. Optional.

Optional mutable specifier, says that the lambda expression's body can change lambda state.

Optional return type declaration.

Lambda body.

In the example below, there is a simple Hello World lambda function.

#include <iostream>

using namespace std;

int main()
{   
    [] () { cout << "Hi from lambda!" << endl; } ();

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_11$ ./lambdas 
Hi from lambda!

Breaking down this slightly strange syntax, you’ll be able to pick apart the lambda pieces:

[]             ()             { cout << "Hi from lambda!" << endl; }    ();
capture list   param list       lambda body                             call the lambda

So, at this point we understand this is just a simple print function call. But how about in memory? Does this look like a normal function call at the assembly level? To examine that, I’ll create a named function doing the same as the lambda expression and call both:

#include <iostream>

using namespace std;

void print_hello()
{   
    cout << "Hi from named function!" << endl;
}

int main()
{   
    [] () { cout << "Hi from lambda!" << endl; } ();

    print_hello();

    return 0;
}

Output:

user@ubuntu:~/cpp/part_2/chapter_11$ ./lambdas 
Hi from lambda!
Hi from named function!

Breaking right before the lambda call, we see something interesting:

   0x40090d <main+8>     mov    rax, qword ptr fs:[0x28]
   0x400916 <main+17>    mov    qword ptr [rbp - 8], rax
   0x40091a <main+21>    xor    eax, eax
   0x40091c <main+23>    lea    rax, [rbp - 9]
   0x400920 <main+27>    mov    rdi, rax
 ► 0x400923 <main+30>    call   0x4008da
 
   0x400928 <main+35>    call   print_hello() <0x4008b6>
 
   0x40092d <main+40>    mov    eax, 0
   0x400932 <main+45>    mov    rdx, qword ptr [rbp - 8]
   0x400936 <main+49>    xor    rdx, qword ptr fs:[0x28]
   0x40093f <main+58>    je     main+65 <0x400946>
────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────
In file: /home/user/cpp/part_2/chapter_11/lambdas.cpp
    9 }
   10 
   11 
   12 int main()
   13 {
 ► 14     [] () { cout << "Hi from lambda!" << endl; } ();
   15 
   16     print_hello();
   17 
   18     return 0;
   19 }

The name is unknown in the debugger, as expected, but everything else about it looks like a normal function call. 0x4008da resides in the .text section just like print_hello().

[13]     0x004007c0->0x00400a12 at 0x000007c0: .text ALLOC LOAD READONLY CODE HAS_CONTENTS

Stepping into the lambda, we see the following standard-looking disassembly:

► 0x4008da    push   rbp
  0x4008db    mov    rbp, rsp
  0x4008de    sub    rsp, 0x10
  0x4008e2    mov    qword ptr [rbp - 8], rdi
  0x4008e6    mov    esi, 0x400a25
  0x4008eb    mov    edi, std::cout@@GLIBCXX_3.4 <0x601080>
  0x4008f0    call   0x400770

  0x4008f5    mov    esi, 0x4007a0
  0x4008fa    mov    rdi, rax
  0x4008fd    call   0x400790

  0x400902    nop    
  0x400903    leave  
  0x400904    ret

The lambda looks almost identical to the print_hello() disassembly:

► 0x4008b6 <print_hello()>       push   rbp
  0x4008b7 <print_hello()+1>     mov    rbp, rsp
  0x4008ba <print_hello()+4>     mov    esi, 0x400a25
  0x4008bf <print_hello()+9>     mov    edi, std::cout@@GLIBCXX_3.4 <0x601080>
  0x4008c4 <print_hello()+14>    call   0x400770

  0x4008c9 <print_hello()+19>    mov    esi, 0x4007a0
  0x4008ce <print_hello()+24>    mov    rdi, rax
  0x4008d1 <print_hello()+27>    call   0x400790

  0x4008d6 <print_hello()+32>    nop    
  0x4008d7 <print_hello()+33>    pop    rbp
  0x4008d8 <print_hello()+34>    ret

There are small differences. You’ll notice in the lambda, register RDI is preserved at the location stored at RBP-8. RDI is soon after loaded with the address of cout. Another notable difference is the lambda uses 64-bit registers, while print_hello() is using 32-bit registers. Perhaps this is the GNU compiler optimizing named functions, but not doing the same for anonymous expressions. Lastly, there are differences in the function epilogue. The lambda utilizes the LEAVE, RET instruction sequence while the named function only calls RET. In other words, the lambda does stack cleanup and restores stack registers via LEAVE, while print_hello() does it manually with POP RBP. More details on the LEAVE instruction:

The LEAVE instruction copies the frame pointer (in the EBP register) into the stack pointer register (ESP), which releases the stack space allocated to the stack frame. The old frame pointer is then popped from the stack into the EBP register, restoring the calling procedure's stack frame.

 

Leave a Reply