In Java, we have our take on functional programming with lambdas. Obviously, as functional programmers love to tell us - they're just a mere simplifications of the 'real' functional languages. However, introduced in Java 8, they've had really changed how we do things in Java now. As for C/C++, functions were there from the very beginning (obviously!), but concepts of lambdas emerged just in the latest C++20 standard.
In this post I will try to take more broad approach to the concepts of functions and lambdas in C++. I've thought about writing about C function pointers, but I've decided to just refresh my memory with Richard Reese Understanding and using C pointers' book, but do not write about it here. Let's keep it pure C++, and concentrate on more abstract solutions. Like the one just coming in our way - functions as objects.
Function as an object
The concept of functions being first class citizens in the programming languages has a long tradition. In order to achieve that in C++, we need a way to pass functions to the methods as params. A universal container for callable objects is std::function class - wrapper around function pointer. It's a piece of standard library, residing in the header. I'm starting with it, as it is more generic than lambdas.
In general, it's possible to create an empty std::function object, but that won't be much of use - trying to call such object will result in std::bad_function_call exception being thrown. What we need, is object that actually holds a callable 'thing'. There are two ways to assign a callable object to the std::function instance - either as a constructor param, or just by assigning it to the pointer. Here's an example:
#include <iostream>
#include <functional>
void testFunction()
{
std::cout << "Test function";
}
int main()
{
std::function<void()> testFunctionHandler { testFunction };
std::function<void()> testFunctionHandler2 = testFunction;
testFunctionHandler();
testFunctionHandler2();
}
It does not look scary - maybe syntax is a little weird, but nothing we cannot handle. std::function can serve as w convenient abstraction, representing all the types of callable things. However, here's the more modern approach to functional programming in C++ (and more close to Java equivalent) - lambdas.
Lambdas in C++
The same as in Java, it's a popular use case, to just pass some behaviour represented by a function. We don't need to (or don't want to), create a separate function or an object for that. What we want, is to pass some behaviour to the method and let it do its job. That's what lambdas are for - simple (ok, got me) concepts of passable behaviour. Let's start with the most simple example of a function - that takes nothing and returns nothing (besides side-effect):
#include <iostream>
void printSomeMsg() {
std::cout << "Some message" << std::endl;
}
int main() {
printSomeMsg();
}
As simple as it is - it just prints the hardcoded message. However, let's assume that we don't want to write such a trivial function in our code. We just need its functionality in the specific places. That's where the lambda comes in. Let's take a look at this:
#include <functional>
#include <iostream>
int main() {
auto ourLambdaAsAuto = []() { std::cout << "Some message" << std::endl; };
std::function<void()> ourLambdaAsFunction = []() { std::cout << "Some message 2" << std::endl; };
ourLambdaAsAuto();
ourLambdaAsFunction();
}
Again, the syntax may look scary at the beginning, but that's not that big of a deal. Let's go through it step by step:
- [] - it is empty now, but that does not mean it's not being used. These square brackets are responsible for specifying captures. In simple words - they fetch needed data/variables from the calling context. We will discuss this in detail later, as it is quite important.
- () - rather familiar feature. In the simple parenthesis we just specify arguments to the lambda. We don't have any here, that's why it's empty.
- {} - in the curly braces we provide the body of our lambda. In short - the job to be done.
Wasn't that bad, right? Of course, our simple example did not cover everything. Generic expression for lambda creation looks like this:
[captures] (parameters) modifiers -> return-type { body }
From the above expression we know already captures, parameters and body. The remaining two are quite simple:
- modifiers - we can specify different modifiers, but depending on the standard they may vary, and their behaviour too. The best way to learn about them is to visit official C++ ref docs. The most popular are const and constexpr.
- return type - by default the compiler will deduce the return type of the lambda. However, especially when we're using template lambdas, it's advisable to help the compiler with small hint. Like in this example: [](auto x, double y) -> decltype(x+y) { return x + y; }
More about lambdas params
Parameters to lambdas are not that different from the regular ones. That means, we can both assign to them default values, and use auto type. Slightly modified example taken from 'C++ Crash Course' shows these two in combination.
#include <iostream>
int main() {
auto increment = [](auto x, int y = 1) { return x + y; };
std::cout << increment(10) << std::endl;
std::cout << increment(10, 5) << std::endl;
}
Lambda captures
Remember these square brackets that up until now we've always left empty? Now the time has come to tell something more about them. Captures are quite important for lambdas, as they allow them to have context passed. Let's start with simple example.
#include <iostream>
class MySimpleClassPiece {
public:
int x;
std::string someStringValue;
};
class MySimpleClass {
public:
MySimpleClassPiece piece;
};
int main() {
MySimpleClass mySimpleClass { MySimpleClassPiece {1, "someStringValue"} };
std::cout << mySimpleClass.piece.someStringValue << std::endl;
std::cout << mySimpleClass.piece.x << std::endl;
}
Everything here is public, for the sake of simplicity. Printing to the standard output is done directly in the main function, and that is not what we want in the long run. Let's assume, that based on some logic, we want to either print the object state to the console, or modify the values in the object. As this is purely behaviour-driven thing, we're going to use lambdas for this. Let's start with the printing piece first.
// Everything besides main() is the same as above
int main() {
MySimpleClass mySimpleClass { MySimpleClassPiece {1, "someStringValue"} };
auto printingLambda = [mySimpleClass]() {
std::cout << mySimpleClass.piece.someStringValue << std::endl;
std::cout << mySimpleClass.piece.x << std::endl;
};
printingLambda();
}
Running above code snippet produces the same output as before. What happened here? In general, in the capture section of a lambda, we can put whatever parameters we want, and what is more, we can select variables from the calling context, to pass into the lambdas' body (as presented above). By default, all the variables are passed by value!
Of course, we're not limited to just passing the value - we can change its name. It is done like this:
int main() {
MySimpleClass mySimpleClass { MySimpleClassPiece {1, "someStringvalue"} };
auto printingLambda = [externalVariable=mySimpleClass]() {
std::cout << externalVariable.piece.someStringValue << std::endl;
std::cout << externalVariable.piece.x << std::endl;
};
printingLambda();
}
Using this technique can improve the readability of the code, especially when the lambda is contained within the same compilation unit. But that's not over! When it comes to parameters, we can also provide new ones, completely unrelated to the context variables.
int main() {
MySimpleClass mySimpleClass { MySimpleClassPiece {1, "someStringvalue"} };
int xx = 5;
int zz = 10;
auto printingLambda = [externalVariable=mySimpleClass, y = 1, z = 5, result = xx + zz]() {
std::cout << externalVariable.piece.someStringValue << std::endl;
std::cout << externalVariable.piece.x << std::endl;
std::cout << y + z << std::endl;
std::cout << result << std::endl;
};
printingLambda();
}
This piece of code will actually print:
someStringvalue 1 6 15
As you can see this feature is quite powerful, and enables lambda to get as much data as needed, to perform a specific operation. However, up until now we're operating with named parameters - we're specifying all of them in the captures section. That gives us a lot of flexibility, but sometimes we just want to pass everything to the lambda at once. We don't want to provide variables one by one, or (in the future), have to add additional params when new variables appear.
The way to achieve that, is to use 'wildcard-style' in the square brackets. By putting there '=' sign, we pass to the lambda all the variables that enclosing context contains, and lambda wants to use. As simple as that:
int main() {
MySimpleClass mySimpleClass { MySimpleClassPiece {1, "someStringvalue"} };
int xx = 5;
int zz = 10;
int result = xx + zz;
auto printingLambda = [=]() {
// Pay attention that we have to use the name of the original variable here!
std::cout << mySimpleClass.piece.someStringValue << std::endl;
std::cout << mySimpleClass.piece.x << std::endl;
std::cout << result << std::endl;
};
printingLambda();
}
It makes the code easier to maintain, but as I've said - it may influence the readability. Therefore, it's always situation based, whether we use default capture or named one. Ok, so far we've been dealing with parameters' values, as by default the parameters are passed by values. What if the lambda wanted to actually modify them? Trying to do that in the above example will fail.
auto printingLambda = [=]() {
std::cout << mySimpleClass.piece.someStringValue <<; std::endl;
std::cout << mySimpleClass.piece.x << std::endl;
std::cout << result << std::endl;
result = 14; // Results in compilation error with: assignment of read-only variable 'result'
};
If we want to make modifications to the variables, we could fall in a nasty trap here. I've mentioned in the list above, that one part of lambda expression can belong to modifiers. One of them is actually mutable. Sounds about right! Let's take a look at the following code, and try to predict how it behaves.
int main() {
int toAdd = 0;
auto printingLambda = [=]() mutable { // mutable added
toAdd += 5;
std::cout << toAdd << std::endl;
};
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
}
If you've expected ever-increasing output I have bad news for you. It actually looks like this:
5 In main: 0 10 In main: 0 15 In main: 0
Weird, isn't it? The problem here is, that mutable does not allow modifying params passed by values - quite reasonable I would say. What it does instead, is that it creates a new variable named exactly as the one used (in this example it is toAdd), and then keeps it in memory, as long as lambda is in use. That explains how we got this specific output.
All right, but what if I want to modify actual variables from the outside world. Well, that's not a problem - just pass them as references. We can do that by replacing '=' sign with '&' one.
int main() {
int toAdd = 0;
auto printingLambda = [&]() {
toAdd += 5;
std::cout << toAdd << std::endl;
};
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
printingLambda();
std::cout << "In main: " << toAdd << std::endl;
}
With that change, our output looks like we wanted it for the first time:
5 In main: 5 10 In main: 10 15 In main: 15
What must be said here, is that we're not limited to either named captures and default ones! We can mix them for every lambda! Here's an example:
int main() {
int toAdd = 0;
int x = 5;
auto printingLambda = [=, &toAdd]() {
toAdd += 5;
std::cout << toAdd << std::endl; // toAdd passing/changes will work as in the previous example
std::cout << x << std::endl;
// x += 10; This line will cause compiler to fail
};
// printing skipped
}
To finish discussing captures I have to mention, that it's also possible to pass to the lambda (either by value or by reference), an actual instance of the wrapping class. However, the topic is not trivial - I recommend reading an excellent article on Nextptr.com. What is more - linked presentation in the sources section also provides valuable info about it. That's it for today.
SOURCES:
- Free ebook about lambdas - whole book dedicated to lambdas in C++. Just provide email address and it's yours.
- Introduction to lambdas - BackToBasis series on CPPConf
- 'Understanding and using C Pointers' by Richard Reese, chapter about function pointers
- 'C++ Crash Course' by Josh Lospinoso
- Nexptr.com article about the evolution of this capturing
Top comments (0)