Hey there!! If you've been messing around with C++11 or later, you've likely used lambda functions. They're super handy for writing quick, inline functions without all the boilerplate of regular functions or functors. While tinkering on a side project, I hit a wall when I couldn't modify a variable captured by value using [=]
. This blog dives into my journey to understand why that happened. Spoiler alter: Under the covers, the compiler turns them into classes. Yeah, you heard that right, actual classes!!!!.
Before we get into the internals, let’s recall what a lambda looks like. Introduced in C++11, a lambda expression is an anonymous function object that can capture variables from its surrounding scope. The basic syntax is:
[ captures ] ( parameters ) specifiers -> return_type { body }
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4); // Outputs: 7
At compile time, the C++ compiler translates your lambda into an unnamed class. This class:
#include <iostream>
int main()
{
int x =10, y=20;
auto product = [x,y]() -> int{
return x * y;
};
std::cout << product();
}
Lets inspect the above code from the eyes of a complier
#include <iostream>
int main()
{
int x = 10;
int y = 20;
class __lambda_7_20
{
public:
inline /*constexpr */ int operator()() const
{
return x * y;
}
private:
int x;
int y;
public:
__lambda_7_20(int & _x, int & _y): x{_x}, y{_y} {}
};
__lambda_7_20 product = __lambda_7_20{x, y};
std::cout.operator<<(product.operator()());
return 0;
}
Key points:
()
operator).#include <iostream>
int main()
{
int x =10, y=20;
auto bodmas = [x,y](auto z) -> int{
return x * y + z;
};
std::cout << bodmas(100);
}
From the eyes of a compiler:
#include <cstdio>
#include <iostream>
int main()
{
int x = 10;
int y = 20;
class __lambda_7_19
{
public:
template<typename type_parameter_0_0>
inline /*constexpr */ int operator()(type_parameter_0_0 z) const
{
return (x * y) + z;
}
// Template instantiation
#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<int>(int z) const
{
return (x * y) + z;
}
#endif
private:
int x;
int y;
public:
__lambda_7_19(int & _x, int & _y): x{_x}, y{_y}{}
};
__lambda_7_19 bodmas = __lambda_7_19{x, y};
std::cout.operator<<(bodmas.operator()(100));
return 0;
}
It can be seen that the parameter z of type auto is deduced to the template type parameter T.
This is where it gets interesting, and honestly, the heart of why understanding the class model matters. When you capture by value ([=] or [var]), the compiler copies the variable into a class member. But because operator() is const by default, that member is treated as read-only inside the lambda body. Trying to modify it? Compiler says no. Let's take the same example of multiplying two numbers.
int main()
{
int x =10, y=20;
auto product_tweaked = [=]() -> int{
x = x+1; // error
return x * y;
};
std::cout << product_tweaked();
}
The complier sees it as:
#include <iostream>
int main()
{
int x = 10;
int y = 20;
class __lambda_7_20
{
public:
inline /*constexpr */ int operator()() const // const locks it down
{
x = x + 1; // modification prohibited in a const method :)
return x * y;
}
private:
int x;
int y;
public:
__lambda_7_20(int & _x, int & _y): x{_x}, y{_y} {}
};
__lambda_7_20 product = __lambda_7_20{x, y};
std::cout.operator<<(product.operator()());
return 0;
}
The const qualifier means the method promises not to change the object's state. Since x is a member, incrementing it violates that.
The Fix: Add mutable
Slap on mutable after the params, and it removes the const from operator():
int main()
{
int x =10, y=20;
auto product_tweaked = [=]() mutable -> int{
x = x+1; // can successfully modify
return x * y;
};
std::cout << product_tweaked();
}
#include <iostream>
int main()
{
int x = 10;
int y = 20;
class __lambda_7_28
{
public:
inline /*constexpr */ int operator()() // No const – free to modify
{
x = x + 1;
return x * y;
}
private:
int x;
int y;
public:
__lambda_7_28(int & _x, int & _y): x{_x}, y{_y}{}
};
__lambda_7_28 product_tweaked = __lambda_7_28{x, y};
std::cout.operator<<(product_tweaked.operator()());
return 0;
}
Boom, it works. You're modifying the copy inside the lambda's "object," not the original variable.