A Crash Course in C++: Functional Programming

Damn, it feels good to be a Lambda

James Collerton
7 min read1 day ago
Functions, functions, functions

Audience

This article is aimed at those looking to understand functional programming in C++. We’ll mainly focus on Lambda functions, and lightly cover some other common patterns. We won’t dive deep into topics, we’ll more just dabble toes.

To get the most from the article you will need a solid understanding of at least one other language. It will help if it’s compiled, but it’s not completely necessary as long as you understand the concepts.

You’ll also need to already know the basics of functional programming. We won’t discuss the definition of the concepts we cover (although we’ll offer a light subject area refresher), we’ll instead focus on C++ implementation details.

Additionally it would be good to read my previous articles on C++, compiled in the list here.

Argument

What is functional programming?

In functional programming we create programs by applying and composing functions. Contrast this to the imperative style, where we issue a set of sequential instructions to dictate the flow.

A lot of the time when people refer to functional programming, they mean its pure incarnation. In this particular flavour, functions must have no side-effects.

This means that when you call a function, it shouldn’t mutate anything. It can compute and return an answer, but not change the state of any objects available outside its scope.

If you abide by this, you should obtain referential transparency. This means that given the same inputs, a function should always return the same output. For example, if I call the function sum(3,3), it should always return 6 (and do nothing else).

We can begin to see why this style is so popular. It’s easy to test (we only need to check the result), it optimises quickly (e.g. we can replace all calls to sum(3,3) with 6), it’s parallelisable (threads won’t unexpectedly interfere with each other through side-effects), and simple to reason about.

What is a Lambda function?

Lambda (a.k.a. anonymous) functions are a quick, convenient way of defining a function, and potentially assigning it to a variable.

An example in pseudo-code:

addOne = (x) -> x + 1;   // We assign a function to 'addOne'
addOne(5); // This is 6
addOne(addOne(5)); // This is 7

We can begin combining these to create complex, powerful new functions. If this seems a bit vague, let’s look at some C++ examples.

Our first C++ Lambda

Here we go!

auto sumTwoIntegers = [](int a, int b) {
return a + b;
};
sumTwoIntegers(1, 2) // This returns 3

This takes in two integers and sums them together. However, there’s a bit of excess syntax hanging around. Let’s build on this example to explain the component parts.

[capture clause] (parameter list) mutable throw () -> int {
body
}

The shape of a Lambda function is above. Breaking it down:

  • Capture clause (mandatory): This allows you to access variables in the surrounding scope. This can be done by reference (prefixing them with &) or by value (with no prefix).
  • Parameter list (mandatory): Equivalent to the parameters you give to a normal function.
  • Mutable (optional): This lets us modify variables captured by value. If this seems confusing don’t worry, we’ll expand on it later.
  • Exception specification (optional): Indicates which exceptions can be thrown by the Lambda (e.g. noexcept or throw).
  • Return type (optional): Usually the return type is deduced, but we can use trailing return types in cases where it can’t be (expanded on shortly).
  • Body (mandatory): What the function does!

The best way of learning is by example, so let’s dig in with a few!

Capture Clause

We can access surrounding variables in several ways, which we summarise in the snippet below.

A cool feature of capture clauses is the ability to declare and initialise a variable within them. We’ll demonstrate in the following section.

Mutable

In the example above we capture by value. This means the variable stays constant, we can’t update it in the Lambda body.

auto x = 10;

auto addTwenty = [x](int a) {
x = 20; // Error!
return x + a;
};

Sometimes, however, we don’t want this behaviour. Let’s imagine a function for incrementing a number, but each time we call it we’d like to add a higher value. The first call adds 1, the second 2, the third 3.

auto increasingIncrement = [x = 1](int a) mutable {
return x++ + a;
};

std::cout << "Increment 1: " << increasingIncrement(1) << std::endl; // 2
std::cout << "Increment 2: " << increasingIncrement(1) << std::endl; // 3
std::cout << "Increment 3: " << increasingIncrement(1) << std::endl; // 4

We still won’t alter the original value of x, and unless explicitly told to, we won’t make copies every time we mutate the Lambda.

Also, note how we declared and initialised x in the capture clause. Cool, eh?

Exception Specifications

TL;DR, you probably won’t use these.

There’s a bit of a complex history here. In times gone by (i.e. post C++11 this was deprecated, post C++17 it was illegal), you could indicate which exceptions your function would throw.

void badFunction() throw(int) ...

In the above you’re saying the function can throw an exception of type int. However, in modern C++ there is only one specification, noexcept. This tells the compiler the underlying function doesn’t throw anything, which can be used for optimisation.

Return Types

Generally the return type will be deduced. There are, however, occasions when it is useful to be more explicit.

In the below example the return type cannot be deduced from the initialisation, we need to clue in the compiler as to what it should return.

struct SimpleStruct {
std::string a;
std::string b;
};

auto create_simple_struct = [](std::string a, std::string b) -> SimpleStruct {
// What is this? We need to say!
return { a, b };
};

Another example of requiring the return type is when we are programming to an abstract class and don’t want to return an instance of the implementation.

// This is a base class the other two will extend
class BaseClass {
public:
BaseClass(std::string value): _value(value) {}
private:
std::string _value;
};

// First extension of the base class
class ConcreteClassA: public BaseClass {
public:
ConcreteClassA(std::string value, std::string a): BaseClass(value), _a(a) {}
private:
std::string _a;
};

// Second extension of the base class
class ConcreteClassB: public BaseClass {
public:
ConcreteClassB(std::string value, std::string b): BaseClass(value), _b(b) {}
private:
std::string _b;
};

// In this function we need to explicitly tell the compiler
// we want to return something recognised as the 'BaseClass'
// otherwise the deduction gets confused between the A and B
// classes.
auto create_base_class = [](std::string value, std::string other, bool isAClass) -> BaseClass {
if(isAClass) {
return ConcreteClassA(value, other);
}
return ConcreteClassB(value, other);
};

create_base_class("Value", "A", true);
create_base_class("Value", "B", false);

These are only a couple of use cases, but it’s important to recognise it as an option as you go along.

Combining functions

Revisiting our definition of functional programming ‘we create programs by applying and composing functions’. It is useful to define a higher-order function, which either takes a function as an argument, or returns one.

We can see how we begin building programs. If function B takes function C as an argument, then function A takes function B as an argument we can begin composing!

So how do we start passing things around? Let’s look at pointers, then std::function.

// Here we declare a function that takes in another
// function (f, which takes two integers and returns
// another) then applies it.
int apply(int a, int b, int (*f)(int, int)) {
return f(a, b);
}

// Here we define a Lambda function as the final argument
// to apply, and carry it out.
void higherOrderFunctionsExamples() {
auto output = apply(1, 2, [](int a, int b) { return a + b; });
std::cout << "Output of function: " << output << std::endl;
}

In practice I haven’t seen this used much. A more flexible replacement (and in my opinion, the most common way of doing things) is std::function. Glossing over the details (a very useful table of differences for the separate methods can be found here), they can capture more context and store more types of callable object (functors, Lambdas etc.).

// We replace the function pointer with std::function
int apply(int a, int b, std::function<int(int, int)> f) {
return f(a, b);
}

// Here we define a Lambda function as the final argument
// to apply, and carry it out.
void higherOrderFunctionsExamples() {
auto output = apply(1, 2, [](int a, int b) { return a + b; });
std::cout << "Output of function: " << output << std::endl;
}

Easy!

The final thing we’ll cover are some of the common C++ libraries that cater to functional programming.

We can iterate through elements, applying a function and storing the result elsewhere using std::transform .

// We declare an initial array with values, then a secondary array
// to store the result into.
int arr[] = {1, 2, 3, 4};
int arr_2[4];

// Now we call the transform function, which goes along the initial array,
// carries out the function (doubling) and stores it in the second array.
std::transform(std::begin(arr), std::end(arr), arr_2, [](int a) { return 2 * a; });

There are others that work in a comparable fashion

  • copy_if: Copies all elements in a range to a new location for which a predicate function returns true
  • find_if: Finds first element in a range that satisfies a predicate)
  • sort: You can pass a custom comparator to define the sorting order of a range.

The list goes on!

Conclusion

Functional programming is a rich topic, and we’ve barely scratched the surface in this article. However, understanding Lambdas, higher order functions, and some of the initial building blocks C++ provides should set you in the right direction!

--

--

James Collerton
James Collerton

Written by James Collerton

Senior Software Engineer at Spotify, Ex-Principal Engineer at the BBC

No responses yet