Lambda Functions

In C++, a lambda function is an anonymous function with the added spice that it may freely refer to the local variables (including argument variables) in the containing function. Lambdas are implemented just like Pascal’s nested functions, except the compiler is responsible for generating an internal name for the lambda function. There are a few different ways of implementing lambda functions (see Wikipedia on Nested Functions for more information). The more generalized concept behind this are function closures, i.e., functions defined within other functions that can also escape the context of the current functions (again refer to Wikipedia on Closure).

Closures are common in many modern high-level programming languages, and especially so in functional programming languages. However, first we take a closer look at how one could implement C++’s lambda functions.

int foo(int a)
{
  auto function = [a](int x) { return x + a; };
  return function(10);
}

Here the “problem” is that the lambda function references a local variable of the caller, namely a, even though the lambda function is a function of its own. This can be solved easily by passing the local variable in as an implicit argument to the lambda function:

define internal i32 @lambda(i32 %a, i32 %x) {
	%1 = add i32 %a, %x
	ret i32 %1
}

define i32 @foo(i32 %a) {
	%1 = call i32 @lambda(i32 %a, i32 10)
	ret i32 %1
}

Alternatively, if the lambda function uses more than a few variables, you can wrap them up in a structure which you pass in a pointer to the lambda function. You will notice that this is actually the default behavior of clang.

extern int integer_parse();

int foo(int a, int b)
{
  int c = integer_parse();
  auto function = [a, b, c](int x) { return (a + b - c) * x; };
  return function(10);
}

Becomes:

; ModuleID = 'lambda_func_1_cleaned.ll'
source_filename = "lambda_func_1_cleaned.ll"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

%lambda_args = type { i32, i32, i32 }

declare i32 @integer_parse()

define i32 @lambda(%lambda_args* %args, i32 %x) {
  %1 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 0
  %a = load i32, i32* %1
  %2 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 1
  %b = load i32, i32* %2
  %3 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 2
  %c = load i32, i32* %3
  %4 = add i32 %a, %b
  %5 = sub i32 %4, %c
  %6 = mul i32 %5, %x
  ret i32 %6
}

define i32 @foo(i32 %a, i32 %b) {
  %args = alloca %lambda_args
  %1 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 0
  store i32 %a, i32* %1
  %2 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 1
  store i32 %b, i32* %2
  %c = call i32 @integer_parse()
  %3 = getelementptr %lambda_args, %lambda_args* %args, i32 0, i32 2
  store i32 %c, i32* %3
  %4 = call i32 @lambda(%lambda_args* %args, i32 10)
  ret i32 %4
}

There are a couple of possible variations over this approach:

  • You could pass all implicit captures as explicit arguments to the function.
  • You could pass all implicit captures as explicit arguments in the structure.
  • You could pass in a pointer to the frame of the caller and let the lambda function extract the arguments and locals from the input frame.