Specifics of C++ lambdas

Mutable capture by value: no sharing

First let’s consider the following JavaScript example:

function makeCounter(init = 0) {
let value = init;
return {
increment() {
return ++value;
},
decrement() {
return --value;
},
};
}
const counter = makeCounter(0);counter.increment(); // 1
counter.increment(); // 2
counter.decrement(); // 1
// Attempt 1: mutable lambda, capture by value.
// Doesn't allow sharing.
auto makeMutableValueCounter(int init = 0) {
auto value = init;
return std::make_pair(
[value]() mutable {
return ++value;
},
[value]() mutable {
return --value;
}
);
}
...auto c1 = makeMutableValueCounter(0);c1.first(); // 1
c1.first(); // 2, works!
c1.second(); // -1, not really!

Stack references: no Upwards Funarg

Due to C++’s memory organization — in particular due to allocating local variables on the stack — C++ lambdas do not easily and directly support Upwards Funarg.

// Attempt 2: capture by reference: undefined behavior
//
//
NOTE: invalid solution, never refer stack variables
// if the frame is already deallocated.
auto makeReferenceCounter(int init = 0) {
auto value = init;
return std::make_pair(
[&value]() {
return ++value;
},
[&value]() {
return --value;
}
);
}
...auto c2 = makeReferenceCounter(0);c2.first(); // 1
c2.first(); // 2, works?
c2.second(); // ?, not really, UB!

Shared objects: correct Upwards Funarg

In fact, terms ”closure” and ”stack-allocated” don’t play well together.

// Attemp 3: correct solution via shared pointer.auto makeSharedCounter(int init = 0) {  // Share value allocated on the heap.
auto value = std::make_shared<int>(init);
return std::make_pair( // First closure, increment.
[value] {
return ++*value;
},
// Second closure, decrement.
[value] {
return --*value;
}
);
}
...auto c3 = makeSharedCounter(0);c3.first(); // 1
c3.first(); // 2, works!
c3.second(); // 1, yes, works!

Syntactic sugar for functors

What is lambda anyway in C++?

int x = 10;auto fn = [x](int i) {
return i + x;
};
int x = 10;class __lambda_7_14 {  // Captured binding `x`.
int x;
public: __lambda_7_14(int _x) : x{_x} {} inline int operator()(int i) {
return i + x;
}
};
__lambda_7_14 fn = __lambda_7_14{x};

gcc: Leaking captures

Some compilers though may leak the captured values as mangled public properties. For example, gcc just prepends double-underscore to the captured name, and stores it in the public section:

int main() {
auto fn = [x = 2]() {
return x;
};
fn.__x = 4; return fn();
}
main:
movl $4, %eax
ret

No capture for structured bindings (yet)

Another feature of C++ lambdas is that they can’t capture structured bindings.

auto [x, y] = std::make_tuple(1, 2);// error: 'x' in capture list does
// not name a variable:
auto fn = [x] {return x; };
int x, y;std::tie(x, y) = std::make_tuple(1, 2);auto fn = [x] { return x; }; // OK!

Compile-time evaluation

C++ has a pretty advanced optimizing compiler, and can evaluate the whole code at compile-time — as long as it’s a constexpr or when it can infer this automatically.

#include <memory>int main() {
auto fib = [i = 0, n = 1]() mutable {
return (i = std::exchange(n, n + i));
};
fib(); // 0
fib(); // 1
fib(); // 2
fib(); // 3
fib(); // 5
return fib(); // 8
}
main:
movl $8, %eax
retq

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Dmitry Soshnikov

Dmitry Soshnikov

Software engineer interested in learning and education. Sometimes blog on topics of programming languages theory, compilers, and ECMAScript.