Auto Non-Static Data Member Initializers are holding back lambdas in RAII (+ coroutine workaround)
TLDR: Auto non-static data member variables allow objects to store lambdas, thereby improving readability and reducing the need for type erasure.
Type deduction and `auto` variables are one of the defining features of modern C++, but unfortunately they are not available to class data members:
struct {
// error: non-static data member declared with placeholder 'auto'
auto x = 1;
// error: invalid use of template-name 'std::vector' without an argument list
std::vector y { 1, 2, 3 };
}
This blog post (from 2018!) by Corentin Jabot does a good job outlining this problem so I'll point to it first: [The case for Auto Non-Static Data Member Initializers](https://cor3ntin.github.io/posts/auto_nsdmi/). However, I would like to expand specifically on lambdas as they are mostly glossed over.
Lambdas can only be stored in `auto` variables because each lambda is given a unique type, even if two lambdas are identical in their definition. As pointed out in the blog post, even `decltype([]{}) foo = []{};` is not permitted.
Because of this it is not possible to store a lambda inside an object, even if the storage requirements can otherwise easily be determined.
**Real world example**
An embedded project I am working on makes heavy use of RAII: so much so that most of our subsystems have little to no functional code, just classes composed of lower level building blocks as data members (representing e.g. GPIOs, UARTs) and some minimal routing between them.
This routing usually takes the form of RAII event callback objects that store the callback function, register themselves in an intrusive list to receive the events, and unregister themselves on destruction. This ensures that we can freely shut down subsystems without worrying about lifetime issues - destruction is always in reverse order and easy to understand at a glance.
struct gpio_uart_forwarder {
peripheral::gpio gpio_in {};
peripheral::gpio gpio_out {};
peripheral::uart uart {};
evt::callback<bool> gpio_to_uart { gpio_in.on_change, [&](bool high) {
uart.write(high ? '1' : '0');
} };
evt::callback<char> uart_to_gpio { uart.on_char, [&](char c) {
if (c == '1') gpio_out.set(1);
else if (c == '0') gpio_out.set(0);
} };
}
The only way this is currently possible is using type erasure, i.e. `std::[move_only_]function`.
In an ideal world, we would instead have the callback templated on the function type:
template<typename Ev, std::invocable<const Ev &> Fn>
class callback {
Fn f;
...
}
And our class would look like:
struct gpio_uart_forwarder {
...
auto gpio_to_uart = evt::callback { gpio_in.on_change, [&](bool high) { ... } };
// OR
evlp::callback uart_to_gpio { uart.on_char, [&](char c) { ... } };
}
While an `std::function` might not seem like a huge price to pay, across an entire program it builds up to hundreds of unnecessary heap allocations, thousands of bytes wasted and extra indirections - all for type erasure that we don't actually need! We know all the types involved, and we own the storage ourselves.
**Proposal to fix**
The last time a formal proposal was made to fix this was way back in 2008 by Bill Seymour: [N2713 - Allow `auto` for non-static data members](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2713.html).
I understand there are complications in determining the size and layout of objects with auto members, but 18 years later this seems like pretty low hanging fruit compared to what has recently been achieved with reflection!
Edge cases such as recursive definitions and references to `this` or `sizeof` should simply be banned rather than resulting in the feature being disabled entirely.
**Coroutine workaround**
In my quest for a solution I have discovered that coroutines can be abused to get the best of both worlds.
If
Post #25208
12