Reputation: 4491
In the code below, I create a lambda that captures a local variable by reference. Note that it is a pointer, so, if C++ lambdas are true closures, it should survive the lifetime of the function that creates the lambda.
However, when I call it again, rather than creating a new local variable (a new environment) it reuses the same as before, and in fact, captures exactly the same pointer as before.
This seems wrong. Either, C++ lambdas are not true closures, or is my code incorrect?
Thank you for any help
#include <iostream>
#include <functional>
#include <memory>
std::function<int()> create_counter()
{
std::shared_ptr<int> counter = std::make_shared<int>(0);
auto f = [&] () -> int { return ++(*counter); };
return f;
}
int main()
{
auto counter1 = create_counter();
auto counter2 = create_counter();
std::cout << counter1() << std::endl;
std::cout << counter1() << std::endl;
std::cout << counter2() << std::endl;
std::cout << counter2() << std::endl;
std::cout << counter1() << std::endl;
return 0;
}
This code returns:
1
2
3
4
5
But I was expecting it to return:
1
2
1
2
3
Further edit:
Thank you for pointing the error in my original code. I see now that what is happening is that the pointer gets deleted after the invocation of create_couter, and the new create simply reuses the same memory address.
Which brings me to my real question then, what I want to do is this:
std::function<int()> create_counter()
{
int counter = 0;
auto f = [&] () -> int { return ++counter; };
return f;
}
If C++ lambdas were true closures, each local counter will coexist with the returned function (the function carries its environment--at least part of it). Instead, counter is destroyed after the invocation of create_counter, and calling the returned function creates a segmentation fault. That is not the expected behaviour of a closure.
Marco A has suggested a work around: make the pointer passed by copy. That increases the reference counter, so it does not get destroyed after create_counter. But that is kludge. But, as Marco pointed out, it works and does exactly what I was expecting.
Jarod42 proposes to declare the variable, and initialize it as part of the capture list. But that defeats the purpose of the closure, as the variables are then local to the function, not to the environment where the function is created.
apple apple proposes using a static counter. But that is a workaround to avoid the destruction of the variable at the end of create_function, and it means that all returned functions share the same variable, not the environment under which they run.
So i guess the conclusion (unless somebody can shed more light) is that lambdas in C++ are not true closures.
thank you again for your comments.
Upvotes: 8
Views: 4941
Reputation: 21
Lambda expression — A lambda expression specifies an object specified inline, not just a function without a name, capable of capturing variables in scope.
Closures -
Closures are special functions that can capture the environment, i.e. variables within a lexical scope*.*
A closure is any function that closes over the environment in which it was defined. This means that it can access variables, not in its parameter list.
In c++ a lambda expression is the syntax used to create a special temporary object that behaves similarly to how function objects behave.
The C++ standard specifically refers to this type of object as a closure object. This is a little bit at odds with the broader definition of a closure, which refers to any function, anonymous or not, that captures variables from the environment they are defined in.
As far as the standard is concerned, all instantiations of lambda expressions are closure objects, even if they don’t have any captures in their capture group.
https://pranayaggarwal25.medium.com/lambdas-closures-c-d5f16211de9a
Upvotes: 2
Reputation: 43662
The shared pointer is being destroyed at the end of the function scope and the memory is being freed: you're storing a dangling reference
std::function<int()> create_counter()
{
std::shared_ptr<int> counter = std::make_shared<int>(0);
auto f = [&]() -> int { return ++(*counter); };
return f;
} // counter gets destroyed
Therefore invoking undefined behavior. Test it for yourself by substituting the integer with a class or struct and check if the destructor actually gets called.
Capturing by value would have incremented the usage counter of the shared pointer and prevented the problem
auto f = [=]() -> int { return ++(*counter); };
^
Upvotes: 12
Reputation: 218343
As mentioned, you have dangling reference as the local variable is destroyed at end of the scope.
You can simplify your function to
std::function<int()> create_counter()
{
int counter = 0;
return [=] () mutable -> int { return ++counter; };
}
or even (in C++14)
auto create_counter()
{
return [counter = 0] () mutable -> int { return ++counter; };
}
Upvotes: 5
Reputation: 2230
If the variable is captured by vaule, then it is copy constructed from the original variable. If by reference, you can treat them as different reference to the same object.
Upvotes: 0
Reputation: 10624
if you want 1 2 3 4 5
, you can also try this
std::function<int()> create_counter()
{
static int counter = 0;
auto f = [&] () -> int { return ++counter; };
return f;
}
Upvotes: -1