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. 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 gpiouartforwarder {
peripheral::gpio gpioin {};
peripheral::gpio gpioout {};
peripheral::uart uart {};
evt::callback<bool> gpiotouart { gpioin.onchange, & {
uart.write(high ? '1' : '0');
} };
evt::callback<char> uarttogpio { uart.onchar, [&](char c) {
if (c == '1') gpioout.set(1);
else if (c == '0') gpioout.set(0);
} };
}
The only way this is currently possible is using type erasure, i.e. `std::[moveonly]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 gpiouartforwarder {
...
auto gpiotouart = evt::callback { gpioin.onchange, [&](bool high) { ... } };
// OR
evlp::callback uarttogpio { uart.onchar, & { ... } };
}
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.
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