I know C++ fairly well. I have used lambdas and closures in other languages. For my learning, I wanted to see what I could do with these in C++.
Fully knowing the "danger" and expecting the compiler to reject this, I created a lambda in a function using a function stack variable by reference and returned the lambda. The compiler allowed it and strange things occurred.
Why did the compiler allow this? Is this just a matter of the compiler not being able to detect that I did something very, very bad and the results are just "undefined behavior"? Is this a compiler issue? Does the spec have anything to say about this?
Tested on a recent mac, with MacPorts-installed gcc 4.7.1 and the -std=c++11 compile option.
Code used:
#include <functional>
#include <iostream>
using namespace std;
// This is the same as actsWicked() except for the commented out line
function<int (int)> actsStatic() {
int y = 0;
// cout << "y = " << y << " at creation" << endl;
auto f = [&y](int toAdd) {
y += toAdd;
return y;
};
return f;
}
function<int (int)> actsWicked() {
int y = 0;
cout << "actsWicked: y = " << y << " at creation" << endl;
auto f = [&y](int toAdd) {
y += toAdd;
return y;
};
return f;
}
void test(const function<int (int)>& f, const int arg, const int expected) {
const int result = f(arg);
cout << "arg: " << arg
<< " expected: " << expected << " "
<< (expected == result ? "=" : "!") << "= "
<< "result: " << result << endl;
}
int main(int argc, char **argv) {
auto s = actsStatic();
test(s, 1, 1);
test(s, 1, 2);
test(actsStatic(), 1, 1);
test(s, 1, 3);
auto w = actsWicked();
test(w, 1, 1);
test(w, 1, 2);
test(actsWicked(), 1, 1);
test(w, 1, 3);
return 0;
}
Results:
arg: 1 expected: 1 == result: 1
arg: 1 expected: 2 == result: 2
arg: 1 expected: 1 != result: 3
arg: 1 expected: 3 != result: 4
actsWicked: y = 0 at creation
arg: 1 expected: 1 == result: 1
arg: 1 expected: 2 == result: 2
actsWicked: y = 0 at creation
arg: 1 expected: 1 == result: 1
arg: 1 expected: 3 != result: 153207395
When a lambda object outlives one of its reference-captured objects, execution of the lambda object's function call operator results in undefined behavior once that reference-captured object is accessed. Therefore, a lambda object must not outlive any of its reference-captured objects.
Capturing Local Variables by value inside Lambda Function To capture the local variables by value, specify their name in capture list i.e. }; // Local Variables std::string msg = "Hello"; int counter = 10; // Defining Lambda function and // Capturing Local variables by Value auto func = [msg, counter] () { //... };
Capture clause A lambda can introduce new variables in its body (in C++14), and it can also access, or capture, variables from the surrounding scope. A lambda begins with the capture clause. It specifies which variables are captured, and whether the capture is by value or by reference.
In C++, lambda expression constructs a closure, an unnamed function object capable of capturing variables in scope.
Returning a lambda that captures a local variable by reference is the same as returning a reference to a local variable directly; it results in undefined behaviour:
5.1.2 Lambda expressions [expr.prim.lambda]
22 - [ Note: If an entity is implicitly or explicitly captured by reference, invoking the function call operator of the corresponding lambda-expression after the lifetime of the entity has ended is likely to result in undefined behavior. —end note ]
Specifically, the undefined behaviour in this case is in lvalue-to-rvalue conversion:
4.1 Lvalue-to-rvalue conversion [conv.lval]
1 - A glvalue (3.10) of a non-function, non-array type T can be converted to a prvalue. If T is an incomplete type, a program that necessitates this conversion is ill-formed. If the object to which the glvalue refers is not an object of type T and is not an object of a type derived from T, or if the object is uninitialized, a program that necessitates this conversion has undefined behavior.
The compiler is not required to diagnose this form of undefined behaviour, although as compiler support for lambdas improves it is likely that compilers will be able to diagnose this case and offer an appropriate warning.
Since lambda closure types are well defined, just opaque, your example is equivalent to:
struct lambda {
int &y;
lambda(int &y): y(y) {};
int operator()(int toAdd) {
y += toAdd;
return y;
};
} f{y};
return f;
In general terms, C++ solves the funarg problem by making it the responsibility of the programmer and providing facilities (mutable lambda capture, move semantics, unique_ptr
etc.) to allow the programmer to solve it efficiently.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With