Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11 Lambda closure involving a stack variable by reference that leaves scope is allowed but getting undefined behavior?

Tags:

c++

c++11

lambda

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
like image 749
Jonathan Seng Avatar asked Sep 17 '12 16:09

Jonathan Seng


People also ask

What happens when a Lambda goes out of scope?

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.

How to capture variables in lambda c++?

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] () { //... };

How do you capture variables in lambda?

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.

Are C++ lambdas closures?

In C++, lambda expression constructs a closure, an unnamed function object capable of capturing variables in scope.


1 Answers

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.

like image 127
ecatmur Avatar answered Oct 13 '22 00:10

ecatmur