Introduction
I’ve got my hands on the C++ lambda story by Bartłomiej Filipek and wanted to try out a few of the examples.
Lambdas in C++11
An example of the most minimal lambda expression is:
[] section is called the lambda introducer and the empty {} part is for the function body.
By capturing a variable, you create a member copy of that variable in the closure type. Then, inside the lambda body, you can access it.
The arguments are passed into a lambda function like any regular function. The return type is not needed as the compiler will automatically deduce it.
auto norm = [](std::vector<double> v) -> double{
double result{0};
for(auto i{0uz}; i < v.size(); ++i)
{
result += v[i] * v[i];
}
return sqrt(result);
}In the above example, we explicitly set a return type. The trailing return type is also available for regular function declaration since C++11.
auto f = [x](double a, double b) mutable{
++x;
return a < b;
};
auto g = [](float param) noexcept{
rrtutn param * param;
};
auto h = [](int a, int b) mutable noexcept{
++x;
return a < b;
};Before the body of the lambda, you can use other specifiers. In the code, we used mutable (so that we can change the captured variable) and also noexcept. The third lambda uses mutable and noexcept and they have to appear in that order (you cannot write noexcept mutable as the compiler will reject it).
While the () part is optional, if you want to apply mutable or noexcept then () needs to be in the expression.
Core Definitions
The core definition from the C++ standard from expr.prim.lambda#2 says:
The evaluation of a lambda-expression results in a prvalue temporary. This temporary is called the closure object.
From the above definition, we can understand that the compiler generates a unique wrapper class (closure type) from a lambda expression.
Consider the following code snip:
#include <cmath>
auto cum_normal_cdf = [](double x){
return 0.5 * (1.0 - std::erf(-x/std::sqrt(2.0)))
}CppInsights reveals that the C++ compiler creates the following class, where the function call operator () is overloaded.
Constructors and copying
In the specification of the feature at expr.prim.lambda, we can also read the following:
The closure-type associated with a lambda expression has a deleted default constructor and a deleted copy assignment operator.
That’s why you cannot write:
<source>: In function 'int main()':
<source>:9:19: error: use of deleted function 'main()::<lambda()>::<lambda>()'
9 | decltype(foo) fooCopy;
| ^~~~~~~
<source>:5:23: note: a lambda closure type has a deleted default constructor
5 | auto foo = [&x, &y] {
| ^
<source>:9:19: note: use '-fdiagnostics-all-candidates' to display considered candidates
9 | decltype(foo) fooCopy;
| However, we can copy lambdas:
#include <cmath>
#include <print>
#include <type_traits>
using Point = std::pair<double, double>;
int main() {
auto distance = [](Point x, Point y) noexcept {
return sqrt(pow((x.first - y.first), 2) +
pow((x.second - y.second), 2));
};
Point p{3.0, 4.0};
Point origin{0.0, 0.0};
std::print("Distance to (3,4) from the origin = {}", distance(p, origin));
auto distance_copy = distance;
static_assert(std::is_same_v<decltype(distance), decltype(distance_copy)>);
return 0;
}Captures
The [] does not only introduce the lambda but also holds the list of captured variables. It’s called the capture clause. By capturing a variable from outside the scope of the lambda, you create a non-static data member in the closure type. Then, inside the lambda body, you can access it.
The syntax for captures in C++11 are:
| Syntax | Description |
|---|---|
[&] |
Capture by reference all automatic storage duration variables declared in the reaching scope. |
[=] |
Capture by value (create a copy) all automatic storage duration variables declared in the reaching scope. |
[x, &y] |
Capture x by value, capture y by reference |
[args...] |
Capture a template argument pack all by value |
[&args...] |
Capture a template argument pack all by reference |
[this] |
Captures the this pointer inside the member function |
Note that for [=] and [&] cases, the compiler generates data members for all used variables inside the lambda body. This is a convenient syntax where you don’t want to explicitly mention which variables you capture.
The mutable keyword
By default the operator() of the closure type is marked as const and you cannot modify the captured variables inside the body of the lambda. If you want to change this behavior, you need to add the mutable keyword after the parameter list. This syntax removes the const from the call operator declaration in the closure type.
If you have a simple lambda expression with a mutable:
It will be expanded into the following function object:
The call operator can change the value of the member-fields(capture variables).
#include <iostream>
int main() {
const auto print = [](const char* str, int x, int y) {
std::cout << str << ": " << x << " " << y << "\n";
};
int x{1}, y{1};
print("in main", x, y);
auto foo = [x, y, &print]() mutable {
++x;
++y;
print("in foo", x, y);
};
foo();
print("in main()", x, y);
return 0;
}Output:
In the above example, we can change the values of x and y. Since those are only the copies of x and y from the enclosing scope, we don’t see their new values after foo is invoked.
On the other hand, if you capture by reference, you don’t need to apply the mutable keyword to modify the value. This is because the captured data members are references which means that you cannot rebound them to a new object anyway, but you can change the referenced values.
#include <iostream>
int main() {
int x{1};
std::cout << x << "\n";
const auto foo = [&x]() noexcept { ++x; };
foo();
std::cout << x << "\n";
return 0;
}Output:
In the above example, the lambda is not specified with mutable but it can change the referenced value.
One important thing is that when you apply mutable, then you canot mark your resulting closure object with const as it prevents you from invoking the lambda!
The last line won’t compile as we cannot call a non-const member function on a const object.
Capturing Global Variables
If you have a global variable and you use [=] in your lambda, you might think that your global object is also captured by value. But, it’s not.
#include <iostream>
int global{10};
int main() {
std::cout << global << "\n";
auto foo = [=]() mutable noexcept { ++global; };
foo();
std::cout << global << "\n";
const auto increaseGlobal = []() noexcept { ++global; };
increaseGlobal();
std::cout << global << "\n";
const auto moreIncreaseGlobal = [global]() noexcept { ++global; };
moreIncreaseGlobal();
std::cout << global << "\n";
}Output:
In the above example, we have defined a static variable global and then used it with several lambdas defined in the main() function. If you run the code, then no matter the way you captgure, it will always point to the global object, and no local copies will be created.
Line 13 of the code causes the compiler to generate the following warning:
source>: In function 'int main()':
<source>:13:38: warning: capture of variable 'global' with non-automatic storage duration
13 | const auto moreIncreaseGlobal = [global]() noexcept { ++global; };It’s because only variables with automatic storage duration can be captured.
If you use [=] explicitly, the compiler won’t help you and it generates an error.
Capturing static variables
Similar to capturing global variables, you’ll get the same issues with static objects:
#include <iostream>
void bar() {
static int static_int{10};
std::cout << static_int << "\n";
auto foo = [=]() mutable noexcept { ++static_int; };
foo();
std::cout << static_int << "\n";
const auto increase = []() noexcept { ++static_int; };
increase();
std::cout << static_int << "\n";
const auto moreIncrease = [static_int]() noexcept { ++static_int; };
moreIncrease();
std::cout << static_int << "\n";
}
int main() {
bar();
return 0;
}Caturing a class member and the this pointer
Things get a bit more complicated where you’re in a class member function and you want to capture a data member. Since all non-static data members are related to the this pointer, it also has to be stored somewhere.
#include <iostream>
struct Baz{
void foo(){
const auto lam = [s](){ std::cout << s; };
lam();
}
std::string s;
};
int main()
{
Baz b;
b.foo();
}The code tries to capture s which is a data-member. But the compiler will emit the following message:
<source>: In member function 'void Baz::foo()':
<source>:5:27: error: capture of non-variable 'Baz::s'
5 | const auto lam = [s]() { std::cout << s; };
| ^
<source>:9:17: note: 'std::string Baz::s' declared here
9 | std::string s;
| ^
<source>: In lambda function:
<source>:5:47: error: 'this' was not captured for this lambda function
5 | const auto lam = [s]() { std::cout << s; };To solve this issue, we have to capture the this pointer. Then, we have access to the data members. We can updaten the code to:
#include <iostream>
struct Baz{
void foo(){
const auto lam = [this](){ std::cout << s; };
lam();
}
std::string s;
};
int main()
{
Baz b;
b.foo();
}There are no compiler errors now.
We can also use [=] or [&] to capture this. They both have the same effect in C++11/14. Note that, we captured this by value to a pointer. That’s why we have access to the initial data-member and not its copy.
The value of the value-captured variable is the value at the time the lambda is defined - not when it is used. The value of a ref-captured variable is the value when the lambda is used - not when it is defined.
The C++ closures do not extend the lifetimes of the captured references. We must be sure that the capture variable still lives when the lambda is invoked.
Moveable-only objects
If you have an object that is moveable only(for example a unique_ptr), then we can move the object into a member of the closure type:
#include <iostream>
#include <memory>
#include <utility>
int main() {
std::unique_ptr<int> p = std::make_unique<int>(10);
std::cout << "Before definition pointer in main(): " << p.get() << "\n";
const auto bar = [ptr = std::move(p)] {
std::cout << "pointer in lambda: " << ptr.get() << "\n";
std::cout << "value in lambda: " << *ptr << "\n";
};
std::cout << "After definition pointer in main(): " << p.get() << "\n";
bar();
}Output:
Preserving const
If you capture a const variable, then the const-ness is preserved:
#include <iostream>
#include <type_traits>
int main(){
const int x{10};
auto foo = [x]() mutable{
std::cout << std::is_const<decltype(x)>::value << "\n";
x = 11;
}
foo();
}This code will not compile.
Capturing a parameter pack
We can also leverage captures with variadic templates. The compiler expands the pack into a list of non-static data members whihc might be handy, if you want to use lambda in templated code.
#include <iostream>
#include <tuple>
template <class... Args>
void captureTest(Args... args) {
const auto lambda = [args...] {
const auto tup = std::make_tuple(args...);
std::cout << "tuple size: " << std::tuple_size<decltype(tup)>::value
<< "\n";
std::cout << "tuple 1st: " << std::get<0>(tup) << "\n";
};
lambda();
}
int main() {
captureTest(1, 2, 3, 4);
captureTest("Hello World", 10.0f);
}Output:
Return Type
In most cases, you can skip the return type of the lambda and the compiler will deduce the typename for you. The compiler is able to deduce the return type as long as all of your return statements are of the same type.
IIFE - Immediately Invoked Functional Expression
In most of the examples, we’ve seen so far, notice that we defined ht lambda and then immediately called it later. However, you can also invoke a lambda immediately.
Lambdas in C++14
Default parameters for lambda functions
Let’s start with smaller updates. In C++14, you can use default parameters in a lambda function definition.
Captures with an initialiser
We recall that, in a lambda expression, we can capture variables form the outside scope. The compiler expands that capture syntax and creates corresponding non-static data members in the closure type.
In C++14, you can create new data-members and initialize them in the capture clause. Then, you can access those variables inside the lambda. It’s called capture with an initialiser or another name for this feature is generalized lambda capture.
#include <iostream>
int main(){
int x{30};
int y{12};
const auto foo = [z = x + y]{ std::cout << z << "\n"; };
x = 0;
y = 0;
foo();
}Output:
Keep in mind, that the new variable is initialized at the place where you define the lambda and not where you invoke it.
Creating variables through an initialiser is also flexible, since you can, for example, create references to variables from outside scope.
#include <iostream>
int main() {
int x{30};
const auto foo = [&z = x]() { std::cout << z << "\n"; };
foo();
x = 0;
foo();
}Output:
Limitations
Note, that while you can capture by reference with an initialiser, it’s not possible to write rvalue reference &&. So, the below code would be invalid.
One gotcha with std::function
Having a moveable-only captured variable in a lambda makes the closure object not copyable. This might be an issue if you want to store such a lambda in std::function which accepts only copyable copy objects.
Optimisation
Another idea is to use capture initialisers as a potential optimisation technique. Rather than computing some value every time, we invoke a lambda, we can compute it once in. the initialiser:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
int main()
{
using namespace std::string_literals;
const std::vector<std::string> vs = {
"apple", "orange",
"foobar", "lemon"
};
const auto prefix = "foo"s;
auto result = std::find_if(vs.begin(), vs.end(),
[&prefix](const std::string& s){
return s == prefix + "bar"s;
}
);
if(result != vs.end())
std::cout << prefix << "-something found!\n";
result = std:find_if(vs.begin(), vs.end(),
[savedString = prefix + "bar"s](const std::string& s){
return s == savedString;
}
);
if(result != vs.end())
std::cout << prefix << "-something found!\n";
return 0;
}The code above shows two calls to std::find_if. In the first scernario, we capture prefix and compare the input value against prefix + "bar"s. Everytime the lambda is invokes, a temporary value that stores the sum of those strings has to be created and computed.
The second call to find_if shows an optimisation: we create a captured variable savedString that computes the sum of the strings. then, we can safely refer to it in the lambda body. The sum of the strings will run only once and not with every invocation of the lambda.
Capturing a class data member
An initialiser can also be used to capture data members without worrying about *this pointer. We can capgture a copy of a data member and not bother with dangling references.
Generic Lambdas
The early specification of lambda expressions allowed us to create anonymous function objects (closure types) and pass them to various generic algorithms from the standard library. However, closures were not generic on their own. For example, you couldn’t specify a template parameter as a lambda parameter.
Fortunately, since C++14, the Standard introduced generic lambdas and now we can write:
const auto foo = [](auto x, int y){ std::print("{}, {}", x, y); };
foo(10, 1)
foo(10.1234, 2);
foo("hello world",3);Please notice auto x as a parameter to the lambda. This is equivalent to using a template declaration in the call operator of the closure type:
struct{
template<typename T>
void operator()(Tx, int y) const{
std::print("{}, {}", x, y);
}
} someInstance;If there are more auto arguments, then the code expands to separate template parameters:
expands into:
Variadic Generic Arguments
If we need more function parameters, then we can also go variadic.
#include <iostream>
template <typename T>
auto sum(T x) {
return x;
}
template <typename T1, typename... T>
auto sum(T1 s, T... ts) {
return s + sum(ts...);
}
int main() {
const auto sumLambda = [](auto... args) {
std::cout << "sum of: " << sizeof...(args) << " numbers\n";
return sum(args...);
};
std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
}Perfect forwarding with generic lambdas
With generic lambdas, we’re not restricted to using auto x, we can add any qualifiers as with other auto variables such as auto&, const auto& or auto&&. One of the handy use cases is that we can specify auto&& x which becomes a forwarding(universal) reference.
#include <print>
#include <string>
void foo(const std::string& ) { std::println("foo(const string&)"); }
void foo(std::string&&) { std::println("foo(std::string&&)"); }
int main()
{
const auto callFoo = [](auto&& str){
std::println("Calling foo() on: {}", str);
foo(std::forward<decltype(str)>(str));
};
const std::string str = "Hello World";
callFoo(str);
callFoo("Helo World Ref Ref");
}Output:
Lambdas in C++17
C++17 brought one major enhancement to lambdas: they can be declared constexpr:
How about some more practical examples? It’s fun to write a simple compile-time hash function for strings.
#include <algorithm>
#include <numeric>
#include <string_view>
constexpr auto hornerHash = [](std::string_view s) {
auto hash_value = std::accumulate(
s.begin(), s.end(), 0ull, [](unsigned long long accum, char element) {
return (accum * 31 + element);
});
return hash_value;
};
int main() {
constexpr auto hashCode = hornerHash("hello world");
return 0;
}You can also capture constexpr variables in lambdas.
Overloaded pattern
It might be surprising to see, but you can derive from a lambda! Since the compiler expands a lambda expression into a function object with operator(), we can inherit from this type.
#include <print>
template<typename Callable>
class ComplexFn : public Callable{
explicit ComplexFn(Callable f) : Callable(f){}
};
template<typename Callable>
ComplexFn<Callable> MakeComplexFunctionObject(Callable&& callable){
return ComplexFn<Callable>(std::forward<Callable>(callable));
}
int main()
{
const auto func = MakeComplexFunctionObject(
[]{
std::println("Hello, complex function object!");
}
);
func();
}In the example, there’s the ComplexFn class which derives from Callable which is a template parameter. If we want to derive from a lambda, we need to do a little trick, as we cannot spell out the exact type of the closure type(unless we wrap it into a std::function). That’s why, we need the MakeComplexFnObject function that can perform template argument deduction and get the type of the lambda closure.
The ComplexFn apart from it’s name is just a simple wrapper without much of a use. Are there any use-cases for such code patterns?
We can extend the code above and inherit from two lambdas and create an overloaded set:
#include <print>
template <typename TCall, typename UCall>
class SimpleOverloaded : public TCall, UCall {
public:
SimpleOverloaded(TCall tf, UCall uf) : TCall(tf), UCall(uf) {}
using TCall::operator();
using UCall::operator();
};
template <typename TCall, typename UCall>
SimpleOverloaded<TCall, UCall> MakeOverloaded(TCall&& tf, UCall&& uf) {
return SimpleOverloaded<TCall, UCall>(std::forward<TCall>(tf),
std::forward<UCall>(uf));
}
int main() {
const auto func = MakeOverloaded([](int) { std::println("Int!"); },
[](float) { std::println("Float!"); });
func(10);
func(10.0f);
}This time we have more code and we derive from two template parameters, but we also need to expose their call operators explicitly. It’s because when looking for the correct function overload, the compiler requires the candidates to be in the same scope.
Now, how about using a variable number of base classes, which means a variable number of lambdas?
Deriving from multiple lambdas
In C++17, we have a handy pattern for this:
#include <print>
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int main() {
const auto test = overloaded{
[](const int& i) { std::println("int: {}", i); },
[](const float& f) { std::println("float: {}", f); },
[](const std::string& i) { std::println("string: {}", i); },
};
test("10.0f");
}Let’s deep-dive into the basic mechanics of this pattern. It benefits from three features:
- Pack expansions in
usingdeclarations. - Custom template argument deduction rules - that allows converting a list of lambda objects into a list of base classes for the
overloadedclass. (not needed in C++20!) - Extension to aggregate initialisation - before C++17, you couldn’t aggregate initialise type that derives from other types.
The using declaration is important for bringing the function call operator - operator() into the same scope of the overloaded structure. In C++17, we got a syntax that supports variadic templates, which was not possible in the previous revisions of the language.
Let’s try and understand the remaining features:
Custom Template Argument Deduction Rules
We derive from lambdas, and then we expose their operator() as we saw in the previous section. But, how can we create objects of this overload type?
As you know, there’s no way to know up-front, the type of the lambda, as compiler has to generate some unique typename for each of them. For example, we cannot just write:
The only way that could work would be some make function (as template argument deduction works for function templates):
template<typename... T>
constexpr auto make_overloader(T&&... t){
return overloaded<T...>{std::forward<T>(t)...};
}With the template argument deduction rules that were added in C++17, we can simplify the creation of common template types and the make_overloader function is not needed. For simple types, we can write:
There’s also the option to define custom deduction guides. The standard library uses a lot of them, for exmaple, for std::array:
and the above rules allows us to write:
For the overloaded pattern, we can specify a custom deduction guide:
Extension to aggregate initialisation
The functionality is relatively straightforward: we can now aggregate initialise a type that derives from other types. From the specification:
An aggregate is an array or a class with: - no user-provided, explicit or inherited constructors - no
privateorprotectednon-static data members - novirtualfunctions - novirtual,privateorprotectedbase classes.
For example: