Consider this snippet of code that we introduced in lesson 9.25 -- Introduction to standard library algorithms:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> static bool containsNut(std::string_view str) // static means internal linkage in this context { // std::string_view::find returns std::string_view::npos, which is a very large number, // if it doesn't find the substring. // Otherwise it returns the index where the substring occurs in str. return (str.find("nut") != std::string_view::npos); } int main() { constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" }; // std::find_if takes a pointer to a function const auto found{ std::find_if(arr.begin(), arr.end(), containsNut) }; if (found == arr.end()) { std::cout << "No nuts\n"; } else { std::cout << "Found " << *found << '\n'; } return 0; } |
This code searches through an array of strings looking for the first element that contains the substring “nut”. Thus, it produces the result:
Found walnut
And while it works, it could be improved.
The root of the issue here is that std::find_if
requires that we pass it a function pointer. Because of that, we are forced to define a function that’s only going to be used once, that must be given a name, and that must be put in the global scope (because functions can’t be nested!). The function is also so short, it’s almost easier to discern what it does from the one line of code than from the name and comments.
Lambdas to the rescue
A lambda expression (also called a lambda or closure) allows us to define an anonymous function inside another function. The nesting is important, as it allows us both to avoid namespace naming pollution, and to define the function as close to where it is used as possible (providing additional context).
The syntax for lambdas is one of the weirder things in C++, and takes a bit of getting used to. Lambdas take the form:
[ captureClause ] ( parameters ) -> returnType { statements; }
The capture clause
and parameters
can both be empty if they are not needed.
The return type
is optional, and if omitted, auto
will be assumed (thus using type inference used to determine the return type). While we previously noted that type inference for function return types should be avoided, in this context, it’s fine to use (because these functions are typically so trivial).
Also note that lambdas have no name, so we don’t need to provide one.
As an aside...
This means a trivial lambda definition looks like this:
1 2 3 4 5 6 7 8 |
#include <iostream> int main() { []() {}; // defines a lambda with no captures, no parameters, and no return type return 0; } |
Let’s rewrite the above example using a lambda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> int main() { constexpr std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" }; // Define the function right where we use it. const auto found{ std::find_if(arr.begin(), arr.end(), [](std::string_view str) // here's our lambda, no capture clause { return (str.find("nut") != std::string_view::npos); }) }; if (found == arr.end()) { std::cout << "No nuts\n"; } else { std::cout << "Found " << *found << '\n'; } return 0; } |
This works just like the function pointer case, and produces an identical result:
Found walnut
Note how similar our lambda is to our containsNut
function. They both have identical parameters and function bodies. The lambda has no capture clause (we’ll explain what a capture clause is in the next lesson) because it doesn’t need one. And we’ve omitted the trailing return type in the lambda (for conciseness), but since operator!=
returns a bool
, our lambda will return a bool
too.
Type of a lambda
In the above example, we defined a lambda right where it was needed. This use of a lambda is sometimes called a function literal.
However, writing a lambda in the same line as it’s used can sometimes make code harder to read. Much like we can initialize a variable with a literal value (or a function pointer) for use later, we can also initialize a lambda variable with a lambda definition and then use it later. A named lambda along with a good function name can make code easier to read.
For example, in the following snippet, we’re using std::all_of
to check if all elements of an array are even:
1 2 |
// Bad: We have to read the lambda to understand what's happening. return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); }); |
We can improve the readability of this as follows:
1 2 3 4 5 6 7 8 9 |
// Good: Instead, we can store the lambda in a named variable and pass it to the function. auto isEven{ [](int i) { return ((i % 2) == 0); } }; return std::all_of(array.begin(), array.end(), isEven); |
Note how well the last line reads: “return whether all of the elements in the array are even”
But what is the type of lambda isEven
?
As it turns out, lambdas don’t have a type that we can explicitly use. When we write a lambda, the compiler generates a unique type just for the lambda that is not exposed to us.
For advanced readers
In actuality, lambdas aren’t functions (which is part of how they avoid the limitation of C++ not supporting nested functions). They’re a special kind of object called a functor. Functors are objects that contain an overloaded operator()
that make them callable like a function.
Although we don’t know the type of a lambda, there are several ways of storing a lambda for use post-definition. If the lambda has an empty capture clause, we can use a regular function pointer. In the next lesson, we introduce lambda captures, a function pointer won’t work anymore at that point. However, std::function
can be used for lambdas even if they are capturing something.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <functional> int main() { // A regular function pointer. Only works with an empty capture clause. double (*addNumbers1)(double, double){ [](double a, double b) { return (a + b); } }; addNumbers1(1, 2); // Using std::function. The lambda could have a non-empty capture clause (Next lesson). std::function addNumbers2{ // note: pre-C++17, use std::function<double(double, double)> instead [](double a, double b) { return (a + b); } }; addNumbers2(3, 4); // Using auto. Stores the lambda with its real type. auto addNumbers3{ [](double a, double b) { return (a + b); } }; addNumbers3(5, 6); return 0; } |
The only way of using the lambda’s actual type is by means of auto
. auto
also has the benefit of having no overhead compared to std::function
.
Unfortunately, we can’t always use auto
. In cases where the actual lambda is unknown (e.g. because we’re passing a lambda to a function as a parameter and the caller determines what lambda will be passed in), we can’t use auto
. In such cases, std::function
should be used.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <functional> #include <iostream> // We don't know what fn will be. std::function works with regular functions and lambdas. void repeat(int repetitions, const std::function<void(int)>& fn) { for (int i{ 0 }; i < repetitions; ++i) { fn(i); } } int main() { repeat(3, [](int i) { std::cout << i << '\n'; }); return 0; } |
Output
0 1 2
Rule
Useauto
when initializing variables with lambdas, and std::function
if you can’t initialize the variable with the lambda.
Generic lambdas
For the most part, lambda parameters work by the same rules as regular function parameters.
One notable exception is that since C++14 we’re allowed to use auto
for parameters (note: in C++20, regular functions will be able to use auto
for parameters too). When a lambda has one or more auto
parameter, the compiler will infer what parameter types are needed from the calls to the lambda.
Because lambdas with one or more auto
parameter can potentially work with a wide variety of types, they are called generic lambdas.
For advanced readers
When used in the context of a lambda, auto
is just a shorthand for a template parameter.
Let’s take a look at a generic lambda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> int main() { constexpr std::array months{ // pre-C++17 use std::array<const char*, 12> "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; // Search for two consecutive months that start with the same letter. const auto sameLetter{ std::adjacent_find(months.begin(), months.end(), [](const auto& a, const auto& b) { return (a[0] == b[0]); }) }; // Make sure that two months were found. if (sameLetter != months.end()) { // std::next returns the next iterator after sameLetter std::cout << *sameLetter << " and " << *std::next(sameLetter) << " start with the same letter\n"; } return 0; } |
Output:
June and July start with the same letter
In the above example, we use auto
parameters to capture our strings by const
reference. Because all string types allow access to their individual characters via operator[]
, we don’t need to care whether the user is passing in a std::string
, C-style string, or something else. This allows us to write a lambda that could accept any of these, meaning if we change the type of months
later, we won’t have to rewrite the lambda.
However, auto
isn’t always the best choice. Consider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> int main() { constexpr std::array months{ // pre-C++17 use std::array<const char*, 12> "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; // Count how many months consist of 5 letters const auto fiveLetterMonths{ std::count_if(months.begin(), months.end(), [](std::string_view str) { return (str.length() == 5); }) }; std::cout << "There are " << fiveLetterMonths << " months with 5 letters\n"; return 0; } |
Output:
There are 2 months with 5 letters
In this example, using auto
would infer a type of const char*
. C-style strings aren’t easy to work with (apart from using operator[]
). In this case, we prefer to explicitly define the parameter as a std::string_view
, which allows us to work with the underlying data much more easily (e.g. we can ask the string view for its length, even if the user passed in a C-style array).
Generic lambdas and static variables
One thing to be aware of is that a unique lambda will be generated for each different type that auto
resolves to. The following example shows how one generic lambda turns into two distinct lambdas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> int main() { // Print a value and count how many times @print has been called. auto print{ [](auto value) { static int callCount{ 0 }; std::cout << callCount++ << ": " << value << '\n'; } }; print("hello"); // 0: hello print("world"); // 1: world print(1); // 0: 1 print(2); // 1: 2 print("ding dong"); // 2: ding dong return 0; } |
Output
0: hello 1: world 0: 1 1: 2 2: ding dong
In the above example, we define a lambda and then call it with two different parameters (a string literal parameter, and an integer parameter). This generates two different versions of the lambda (one with a string literal parameter, and one with an integer parameter).
Most of the time, this is inconsequential. However, note that if the generic lambda uses static duration variables, those variables are not shared between the generated lambdas.
We can see this in the example above, where each type (string literals and integers) has its own unique count! Although we only wrote the lambda once, two lambdas were generated -- and each has its own version of callCount
. To have a shared counter between the two generated lambdas, we’d have to define a variable outside of the lambda. For now, this means defining the variable even outside of the function the lambda is defined in. In the above example, this means adding a global variable. We’ll be able to avoid the global variable after talking about lambda captures in the next lesson.
Return type deduction and trailing return types
If return type deduction is used, a lambda’s return type is deduced from the return
-statements inside the lambda. If return type inference is used, all return statements in the lambda must return the same type (otherwise the compiler won’t know which one to prefer).
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { auto divide{ [](int x, int y, bool bInteger) { // note: no specified return type if (bInteger) return x / y; else return static_cast<double>(x) / y; // ERROR: return type doesn't match previous return type } }; std::cout << divide(3, 2, true) << '\n'; std::cout << divide(3, 2, false) << '\n'; return 0; } |
This produces a compile error because the return type of the first return statement (int) doesn’t match the return type of the second return statement (double).
In the case where we’re returning different types, we have two options:
1) Do explicit casts to make all the return types match, or
2) explicitly specify a return type for the lambda, and let the compiler do implicit conversions.
The second case is usually the better choice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int main() { // note: explicitly specifying this returns a double auto divide{ [](int x, int y, bool bInteger) -> double { if (bInteger) return x / y; // will do an implicit conversion to double else return static_cast<double>(x) / y; } }; std::cout << divide(3, 2, true) << '\n'; std::cout << divide(3, 2, false) << '\n'; return 0; } |
That way, if you ever decide to change the return type, you (usually) only need to change the lambda’s return type, and not touch the lambda body.
Standard library function objects
For common operations (e.g. addition, negation, or comparison) you don’t need to write your own lambdas, because the standard library comes with many basic callable objects that can be used instead. These are defined in the <functional> header.
In the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <algorithm> #include <array> #include <iostream> bool greater(int a, int b) { // Order @a before @b if @a is greater than @b. return (a > b); } int main() { std::array arr{ 13, 90, 99, 5, 40, 80 }; // Pass greater to std::sort std::sort(arr.begin(), arr.end(), greater); for (int i : arr) { std::cout << i << ' '; } std::cout << '\n'; return 0; } |
Output
99 90 80 40 13 5
Instead of converting our greater
function to a lambda (which would obscure its meaning a bit), we can instead use std::greater
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <algorithm> #include <array> #include <iostream> #include <functional> // for std::greater int main() { std::array arr{ 13, 90, 99, 5, 40, 80 }; // Pass std::greater to std::sort std::sort(arr.begin(), arr.end(), std::greater{}); // note: need curly braces to instantiate object for (int i : arr) { std::cout << i << ' '; } std::cout << '\n'; return 0; } |
Output
99 90 80 40 13 5
Conclusion
Lambdas and the algorithm library may seem unnecessarily complicated when compared to a solution that uses a loop. However, this combination can allow some very powerful operations in just a few lines of code, and can be more readable than writing your own loops. On top of that, the algorithm library features powerful and easy-to-use parallelism, which you won’t get with loops. Upgrading source code that uses library functions is easier than upgrading code that uses loops.
Lambdas are great, but they don’t replace regular functions for all cases. Prefer regular functions for non-trivial and reusable cases.
Quiz time
Question #1
Create a
struct Student
that stores the name and points of a student. Create an array of students and use std::max_element
to find the student with the most points, then print that student’s name. std::max_element
takes the begin
and end
of a list, and a function that takes 2 parameters and returns true
if the first argument is less than the second.
Given the following array
1 2 3 4 5 6 7 8 9 10 |
std::array<Student, 8> arr{ { { "Albert", 3 }, { "Ben", 5 }, { "Christine", 2 }, { "Dan", 8 }, // Dan has the most points (8). { "Enchilada", 4 }, { "Francis", 1 }, { "Greg", 3 }, { "Hagrid", 5 } } }; |
your program should print
Dan is the best student
Question #2
Use std::sort
and a lambda in the following code to sort the seasons by ascending average temperature.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <algorithm> #include <array> #include <iostream> #include <string_view> struct Season { std::string_view name{}; double averageTemperature{}; }; int main() { std::array<Season, 4> seasons{ { { "Spring", 285.0 }, { "Summer", 296.0 }, { "Fall", 288.0 }, { "Winter", 263.0 } } }; /* * Use std::sort here */ for (const auto& season : seasons) { std::cout << season.name << '\n'; } return 0; } |
The program should print
Winter Spring Fall Summer
![]() |
![]() |
![]() |
Ok, this lambda syntax initially tickled my brain, but I think I got it now!
Main benefit of Lambda = no need to write a comparator function for <algorithm> functions like std::sort, std::max_element, etc...
For anyone confused, please check out my notes below. I think this is a good way to think about this. :)
Copy/Pasting the solution for Question 1 produces this error message in Virtual Studio 2019:
Severity Code Description Project File Line Suppression State
Error MSB3491 Could not write lines to file "Debug\10.15 — .f1626984.tlog\10.15 — Introduction to lambdas (anonymous functions) Question 1.lastbuildstate". Path: Debug\10.15 — .f1626984.tlog\10.15 — Introduction to lambdas (anonymous functions) Question 1.lastbuildstate exceeds the OS max path limit. The fully qualified file name must be less than 260 characters. 10.15 — Introduction to lambdas (anonymous functions) Question 1 C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Microsoft\VC\v160\Microsoft.CppBuild.targets 354
Ah nvm! The title of my project was just too long and maybe the directory it was located in was too many layers deep.
Hi, is there any reason we're using std::string_view rather than std::string in Quiz #2?
We know that there are only 4 seasons, there will never be more or less, and we know their names at compile time.
If you can't provide the season names as compile-time strings, you need to use `std::string` to make the `Season` own the string.
Hi, can you tell me what's the error in my code:
I get this error message:
||=== Build: Debug in LearnCPP715-1 (compiler: GNU GCC Compiler) ===|
C:\CBProjects\LearnCPP715-1\main.cpp||In function 'int main()':|
C:\CBProjects\LearnCPP715-1\main.cpp|28|error: could not convert 'std::max_element<Student*, main()::<lambda(Student, Student)> >(std::begin<std::array<Student, 8> >(students), std::end<std::array<Student, 8> >(students), (<lambda closure object>main()::<lambda(Student, Student)>{}, main()::<lambda(Student, Student)>()))' from 'Student*' to 'std::__cxx11::string' {aka 'std::__cxx11::basic_string<char>'}|
C:\CBProjects\LearnCPP715-1\main.cpp|34|error: missing initializer for member 'Student::points' [-Werror=missing-field-initializers]|
||=== Build failed: 2 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|
`std::max_element` returns an iterator. You can get the element an iterator is pointing to by using the *asterisk operator
How is this:
better than this:
?
2. I do not fully understand lambdas. I just understand it as shorthand. I have an idea of the syntax but what am I doing is following the syntax for Quiz 2.
Quiz 2 is easier to do because the answer is similar to Quiz 1.
I'm still stuck with when references should be used.
3. Here are my assumptions.
a. I assume we use "auto" because users won't have to know what type was defined in the array-structs. The type in the struct can be hidden E.g. ??? points. Auto just matches the ???.
b. We use "&" (references) because arrays decay to pointers
c. We use "const" to ensure that we don't accidentally change the value in the array-struct. It isn't actually necessary to have "const" to solve Quiz 2.
d. Lambdas are just shorthand so you don't have to write a boolean function with input parameters, return function.
3.
a. The developer knows the type is `Season`, because that's the type of the elements in `seasons`. But because the developer already knows what type the elements have, there's no need to repeat the type. `auto` makes the code less repetitive.
b. `std::array` doesn't decay. `ref1` and `ref2` are not arrays. We use references because copying non-fundamental types is slow. Without using references, `std::sort` would copy 2 elements every time it calls the comparison function.
c. In general, we use `const` to ensure we don't accidentally change the value of the object. But it is required for `std::sort` and many other functions. Non-const references can only bind to lvalues, ie. not to temporaries.
`std::sort` requires the comparison function to be usable with any value category, including temporaries. I don't see why `std::sort` would do that, but it wants to be able to do so, so we have to comply. You're compiler probably accepts the code with non-const references, that doesn't mean it's correct, the code could fail to compile with a different compiler.
d. Yes
Lambdas are pretty hard to wrap one's head around. I can't do lambdas without hints or solution. Too many errors occur.
None of your ERROR comments are related to lambdas, but iterators.
1. `std::max_element` returns an iterator of `arr`. The element an iterator is pointing to can be accessed with `best->name` or `*best`. `arr`'s elements are `Student`, so `*best` returns a `Student` reference.
2. `arr` is a `std::array`, you need to select an element of the array first.
3. `std::max_element` passes the elements of the container to the comparison function. The elements of the container are `Student`s, not `int`s.
4. `best` is an iterator.
I think I'm going to be a little confused with lambdas.
This is what I know.
1. Lambdas are functors.
2. Lambdas are not just function pointers. Function pointers are a subset of functors.
3. Lambdas are NOT iterators?
I'm not familiar with the term "functor". I'll treat it as "anything that's callable", if I'm wrong please correct me.
1. Yes
2. Lambdas are not function pointers. Please ask again after lesson 9.9, that's when I'll be able to tell you more. Without operator overloading it's hard to explain how lambdas work. Function pointers can point at some functors. `std::function` can "point at" all functors.
3. Lambdas have nothing to do with iterators. An iterator is something that knows how to walk through a container. A lambda is a function-like thing. Iterators and lambdas often occur together when used for standard algorithms.
Ok, got it.
1. Lambdas are functors, whatever they are.
2. Lambdas are NOT function pointers
3. Lambdas are NOT iterators
You could add the above to a quiz on lambdas.
Paragraph 1:Lamdas to the rescue:
It was written "naming pollution". Shouldn't it be naming collision?
A pollution can lead to a collision.
Pollution means you're adding unnecessary names, and if you add enough unnecessary names, you'll likely run into a collision.
Oh got it ✌️
I'm using Visual Studio 2017 and this doesn't work for me:
But this does:
Class template argument deduction is supported from VS2017 15.7 onwards.
Under "Generic lambdas and static variables" section, you have stated: "To have a shared counter between the two generated lambdas, we’d have to define a variable outside of the lambda. For now, this means defining the variable even outside of the function the lambda is defined in." Why so? Wouldn't it suffice to define the shared counter at the starting of main() since the lambda is being used only within main()?
Edit: Just read the next lesson. I get it now.
Hi. In terms of performance, should we use functions or lamdas why?
You're comparing cars to trains. If you live next to a train station, take the train. If you live in a desert, take the car.
Don't use what you think will be generally faster. Use what fits the situation best.
Either one can run as fast as the other. If you're developing a performance-critical application, run benchmarks.
my solution for the question #2
just why did you use const?
The more `const` the better. It prevents you from accidentally modifying something, and also makes your code easier to understand because the reader doesn't have to worry about variables changing their values.
Hi @nascardriver!
I have 3 questions for quiz 1.
1) What does the "->" do here? How does it work? Sorry, but I seem to have missed this.
2) I tried using a for loop prior to watching the hit (to print the correct name) but I failed. So is there another way to replace that -> in this case?
3) dumb question. What's the purpose of & in lambda parameters?
Thanks!
EDIT:
1>Member selection. But, is "best" a pointer?
2>nevermind I guess
3>Oh, "auto" is "Student"! I now see why you used a reference there.
I can't understand the "->" member selection here.
So we are accessing struct "Student" through a variable "best" that isn't of type Student? I can't get it...
EDIT:
I tried replacing "auto best" with "Student *best" and it seems to work just the same. Is this the reason as to why we can access the Student's member through "best"? Because in reality that auto is of type Student? Sorry for bothering.
Hii Tony
1) `best` is an iterator. Iterators can be accessed the same way as pointers. See the lesson about iterators
2) I don't understand. Can you show the code that you tried and tell me what you expected it to do?
3) That's a reference, like in normal functions. `a` and `b` are `const Student&`. Without the reference, we'd copy the students many times, which is unnecessary and expensive. See the lesson about passing arguments by reference.
Now I see you already answered your questions, nevermind I guess
`best` is _not_ a pointer. It might be (convertible to) a pointer with your compiler, but that's not always the case. `best` is a `std::array<Student, 8>::iterator` (That's a type alias and might refer to `Student*` but could but an entirely different type). Most often, `auto` is used for iterators, because their type doesn't matter. It only matter how we can use them, and that's always the same.
my solution:
The point of the quiz was to get to use a lambda
great
thanks so much @nascardriver, i understand it better now.
Hi,
I'm a bit confused with the output result for "Generic lambdas and static variables"
i understand that the first and second call to function print() output "0: hello and
1: world" however, the third and fourth call also take same process which prints "0: 1 and 1: 2". What got me confused is the fifth call to function print(), i expect the output to be "0: ding dong" but it outputs "2: ding dong" instead. Could you please, make this more clearer why it was like that? Thank you.
The compiler generated 2 versions of `print`. One with a `const char*` parameter, and one with an `int` parameter. Each of those versions has a `callCount`. `callCount` never gets reset.
The first 2 calls call the `const char*`, increasing its `callCount` to 2.
Then the `int` version gets called and its `callCount` is also incremented to 2.
Now the `const char*` version (Its `callCount` is still 2) is called again and its `callCount` is incremented to 3.
"The syntax for lambdas is one of the weirder things in C++, and takes a bit of getting used to. Lambdas take the form:"
Perhaps you meant "weirdest" there! :D
Actually he meant "weirdestestest" thing... Lol.. I can't understand anything everything is just wired to understand the syntax.