Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ lambda lexical closure over local variables

Tags:

c++

c++11

lambda

Summary

In C++, when I return a lambda from a function that's captured a variable local to that function, what specifically happens, and why? The compiler (g++) seems to allow it, but it gives me different results than I was expecting, so I'm not sure if this is technically safe/supported.

Details

In some languages (Swift, Lisp, etc.) you can capture local variables in a closure/lambda, and they stay valid and in scope as long as the closure is in scope (I've heard it called "lambda over let over lambda" in a Lisp context). For example, in Swift, the example code for what I'm trying to do is:

func counter(initial: Int) -> (() -> Int) {
    var count = initial
    return { count += 1; return count }
}

let c = counter(initial: 0)
c() // returns 1
c() // returns 2
c() // returns 3

I tried to write a C++ equivalent of this like the following:

auto counter(int initial)
{
    int count = initial;
    return [&count] () -> int {
        count = count + 1;
        return count;
    };
}

However, the result I get is:

auto c = counter(0);
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 1

If I capture a variable that's still in scope, it works as I'd expect. For example, if I do all of the following in a single function:

int count = 0;
auto c = [&count] () -> int {
    count = count + 1;
    return count;
};
std::cout << c() << std::endl; // prints 1
std::cout << c() << std::endl; // prints 2
std::cout << c() << std::endl; // prints 3

So I guess my question is, in the first C++ example above, what is actually getting captured? And is it defined behavior, or do I just have a reference to some random memory on the stack?

like image 487
Chris Vig Avatar asked Jan 04 '23 20:01

Chris Vig


1 Answers

    return [&count] () -> int {

This is a capture by reference. The lambda captures a reference to this object.

The object in question, count, is in the function's local scope, so when the function returns, count gets destroyed, and this becomes a reference to an object that went out of scope and gets destroyed. Using this reference becomes undefined behavior.

Capturing by value seems to solve this problem:

    return [count] () -> int {

But your obvious intent is so that each invocation of this lambda returns a monotonically-increasing counter value. And merely capturing the object by value is not enough. You also need to use a mutable lambda:

 return [count] () mutable -> int
 {
    return ++count;
 };

But the pedantical answer to your question "what happens" is that a lambda is essentially an anonymous class, and what a lambda captures are really class members. Your lambda is equivalent to:

class SomeAnonymousClassName {

     int &count;

public:
     SomeAnonymousClassName(int &count) : count(count)
     {}

     int operator()
     {
          // Whatever you stick in your lambda goes here.
     }
};

Capturing something by reference translates to a class member that's a reference. Capturing something by value translates to a class member that's not a reference, and the act of capturing lambda variables translates to passing them to the lambda class's constructor, which is what happens when you create a lambda. A lambda is really an instance of an anonymous class, with a defined operator().

In a regular lambda, the operator() is actually a const operator method. In a mutable lambda, the operator() is a non-const, a mutable operator method.

like image 76
Sam Varshavchik Avatar answered Jan 09 '23 17:01

Sam Varshavchik