Lambdas

Cpp

#include <algorithm>
#include <functional>
#include <vector>

int main() {
    std::function<int(int)> addOne = [](int x) -> int {
        return x + 1;
   };

   addOne(5);
   // returns 6

   // use lambda as value, do in-place modification
   std::vector<int> v = {1, 2, 3, 4, 5};
   std::transform(v.begin(), v.end(), v.begin(),
       addOne);
   // new value of 'v' is [2, 3, 4, 5, 6]
}

Go

package main

func main() {
	addOne := func(x int) int {
		return x + 1
	}

	addOne(5)
	// returns 6

	// use lambda as a value
	v := []int{1, 2, 3, 4, 5}
	apply([]int{1, 2, 3, 4, 5}, addOne)
	// new value of 'v' is [2, 3, 4, 5, 6]
}

What This Code Does

The above code creates a simple lambda that adds 1 to the value provided. It is called directly and also passed as a value into functions that operate over lists of data.

What's the Same

The Go and the C++ version define a lambda that is both callable directly and can be passed as a value without any special treatment. Both versions also define "pure" lambdas that do not capture any local state or perform any kind of mutating state-changes (i.e. calling the code, alone, does not affect our program). Since both languages are statically typed, they also both defined input and output types on the lambda.

What's Different

The differences lie mainly in syntax. Whereas Go defines functions with the func keyword, C++ uses some syntax that differs from a regular function. The C++ lambda consists of 4 parts; the capture, the parameters, the return type, and the body.

auto lambda = [](int param1, int param2) -> int {
    return param1 + param2;
}

The [] part is a list of variables we'd like to "capture" in our lambda (more on this in the next section). The parameters are everything between ( and ) and what comes after the ->, but before the {, is the return type. Lastly, what's in the curly-braces is the function body.

Another difference is how we ascribe a type to the lambda. In Go, the type of the lambda looks very much like the definition (sans the variable names and function body). In C++ we use the following form.

std::function<ReturnType(Arg1Type, Arg2Type, Arg3Type)>

It's worth noting that there is one small "cheat" on the Go side of things which is the use of an apply function. Since Go doesn't have many facilities for transforming arrays/slices, I've imagined a method that applies a lambda to an array in order to provide an equivalent for the C++ code. To make this compile, you can use

func apply(arr []int, f func(int) int) {
    for i, a := range arr {
        arr[i] = f(a)
    }
}

Capturing Local State

Cpp

#include <functional>

template<typename T>
std::function<T(T)> addX(T x) {
    return [x](T n) -> T {
        return n + x;
    };
}

int main() {
    auto addFive = addX<int>(5);

    addFive(10);
    // returns 15
}

Go

package main

func addX(x int) func(int) int {
	return func(n int) int {
		return n + x
	}
}

func main() {
	addFive := addX(5)

	addFive(10)
	// returns 15
}

What This Code Does

addX is a function that returns another function. The function it returns adds x to whatever value is provided. x is initially given to the addX function but is captured by the lambda that is returned.

What's Different

The first notable difference is the use of template <typename T> written before the addX function. C++ must make use of "generics" in order to account for different numeric types, such as int, float, double, etc. This is done with what is called C++ templates. Templates are expanded at compile-time into concrete implementations. So, to be fair, this is less generic programming and more akin to sophisticated macros.

// When used in code, a version is compiled to match the usage.
template<typename T>
sum(T a, T b) {
    return a + b;
}

void main() {
    sum<int>(1, 2);
    sum<double>(1.0, 2.0);
    // this causes 2 specialized versions to be compiled
}

In Go, the types for numeric types must also be specified with options availabel such as uint8, int16, float64, and complex128 to name some of them. However, Go lacks the ability to specify generic lambdas that work for all numeric types. As such, multiple versions would have to be created if they were needed. And they would have to have different names in order to avoid conflicting.

addXUInt8 := func(x uint8) func(uint8) uint8 { /* implementation */ }
addXUint16 := func(x uint16) func(uint16) uint16 { /* implementation */ }
addXUint32 := func(x uint32) func(uint32) uint32 { /* implementation */ }
// ... repeat for other numeric types

The second notable difference is that the capture portion of our lambda ([]) contains the variable x. This value is copied into the lambda. Since C++ is not a garbage-collected language, we have to be specific about which variables we want to capture and how we'd like to capture them. For things we may not want to copy, we can pass by reference.

int main() {
    auto bigString = "I'm a really long string, don't copy me ...";
    auto printFn = [&bigString]() -> void {
        std::cout << bigString;
    };
    printFn();
}

Note the & before the variable name in [&bigString]. This copies the reference to the variable instead of copying the value into a new variable. The caveat with this is that a reference is just a pointer to a location in memory. If the variable is cleaned up before the lambda is called, bad things could happen.

Digging Deeper

Everything in C++ must have some sort of type and representation in memory, and the same is just as true for lambdas as it is any other type. Take the following:

int main() {
    auto age = 65;
    std::string name = "Sir Robert Christianson Manyard Sr";

    auto printPerson = [age, &name](bool includeAge) -> void {
        std::cout << name;
        if (includeAge) {
            std::cout << ", age: " << age;
        }
        std::cout << "\n";
    }
}

The specification for C++ says that a lambda is an object of an anonymous type, created on the stack. You can imagine the equivalent for this would look like:

struct LambdaPrintPerson {
    int age;
    std::string *name;

    void printPerson(bool includeAge) {
        std::cout << name;
        if (includeAge) {
            std::cout << ", age: " << age;
        }
        std::cout << "\n";
    }
};
int main() {
    auto age = 65;
    std::string name = "Sir Robert Christianson Manyard Sr";

    auto printPerson = LambdaPrintPerson { age, &name };
}

The reason you can imagine a lambda to look like this is because the exact layout (padding, alignment, etc) is compiler dependent according to the specification. However, it should make more sense now how values are "captured" by the lambda and the difference between capturing a reference (no copy) and capturing a value (copy).

Note that it is possible to allocate a lambda on the heap, even though the default is to allocate on the stack. Since a lambda is just an object, it can be heap allocated with the new keyword, such as:

auto printPerson = new auto ([age, &name](bool includeAge) -> void {
    // function body
});

Note the only additional bit of syntax is that we must wrap the lambda in parens ().

Consuming

Cpp

#include <cstdlib>
#include <functional>
#include <iostream>
#include <string>

void sendEmail(std::string to, std::string from, std::string subject,
               std::string body,
               std::function<void(std::string)> success_cb,
               std::function<void(std::string)> failure_cb) {
    if (std::rand() > 0.5) {
        success_cb(to);
    } else {
        failure_cb(to);
    }
}

int main() {
    sendEmail("you@your_domain.com", "me@my_domain.com",
        "Very Important Email",
        "TODO: remember to write email body. :-D",
        [](std::string to) -> void {
            std::cout << "Successful email sent to: " << to << "\n";
        },
        [](std::string to) -> void {
            std::cout << "OH NO! Very important email not sent to "
                      << to
                      << "\n";
        });
}

Go

package main

import (
	"fmt"
	"math/rand"
)

func sendEmail(to string, from string, subject string,
               message string,
               successCb func(string),
               failureCb func(string)) {
	if rand.Float32() >= 0.5 {
		successCb(to)
	} else {
		failureCb(to)
	}
}

func main() {
	sendEmail("you@your_domain.com", "me@my_domain.com",
		"Very Important Email",
		"TODO: remember to write email body. :-D",
		func(to string) {
			fmt.Printf("Successful email sent to: %s\n", to)
		},
		func(to string) {
			fmt.Printf(
				"OH NO! Very important email not sent to %s\n",
				to)
		})

}

What This Code Does

sendEmail is a user-defined function that sends and email (or in our example, pretends to) and then calls one of two callbacks provided by the user. This shows how to receive (consume) lambdas in your own code.

What's Different

Aside from the syntactical differences in how we define and type lambdas, the two examples are semantically equivalent.

Fork me on GitHub