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.