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.