C++17’s useful features for embedded systems(interrupt.memfault.com) |
C++17’s useful features for embedded systems(interrupt.memfault.com) |
https://www.digitalmars.com/ctg/ctgLanguageImplementation.ht...
case 0b0001'0000:
case 0b0101'1000:
(edit, fixed formatting, but I also wanted to agree with a lower down comment asking for 0b001_0000 instead; a better choice.)
case 0b0001_0000:
case 0b0101_1000:
which indeed looks much nicer. I have no idea why C++, when copying the feature, didn't use _.P.S. I copied this feature from 1983 Ada. AFAIK, it was a completely forgotten feature until D had it, then other languages started copying D.
https://gist.github.com/th-in-gs/7f2104440aa02dd36264ed6bc38...
I’m just shaving some bits off - but I guess in principle you could do anything that’s `constexpr` evaluatable. Gzip compression of static buffers?…
Godbolt example:
https://github.com/PhilippeSigaud/Pegged is a D library that generates a parser generator for you based on a grammar (string) at compile time.
But if you're going to uncompress it all right at startup, than it's not worth it at all— microcontroller flash is much cheaper and more plentiful than RAM.
uint8_t b = 0b1111'1111;
I would rather have uint8_t b = 0b1111_1111;
This ' thing is hard to get right on some non-us keyboards. And yes, I've the same problem with Rust.[1] https://en.cppreference.com/w/cpp/language/user_literal
[2] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n34...
Still, I have rarely seen C++ compilers for embedded systems. Although the latter definition more and more includes PC hardware.
0b1111_1111
0b11111111u8
0b1111_1111_u8
0b11_11_11_11_u8
https://doc.rust-lang.org/book/ch03-02-data-types.htmlif constexpr is neat (along with a bunch of the other constexpr/compile-time features that have been coming along), but I feel like this will have both... good and bad uses, and I fear for the astronauts who will go crazy with this.
The enhanced conditionals, this I kind of like though it would take some while to get used to... kind of surprised this got in, being such a departure from C.
Small thing: hardware_destructive_interference_size is nice. Wish I had this in Rust.
Looks like it was asked for (https://github.com/rust-lang/rfcs/issues/1756) but went nowhere.
While completely gratuitous incompatibilities with C are not welcome, C compatibility in general was abandoned in new features long ago (consider range based `for` or the venerable `nullptr`).
So don't use them. C++ is a multi-paradigmatic language, you don't have to (and typically can't, really) put all features to use.
I'm not such a great fan of them either, but it's not like they would confuse me if I saw them in somebody else's code.
https://www.youtube.com/watch?v=cWSh4ZxAr7E&list=TLPQMjkwNTI...
I would say no. Run-time polymorphism is overrated IMHO, and more so for embedded systems, again IMHO. C++ in general has been moving towards preferring things happening statically rather than dynamically.
> that allows preventing runtime memory allocation after the init phase
That's not what allows preventing runtime memory allocation after an "init phase". Unless I'm misunderstanding what you mean.
... oh, I think I get it: As a general rule, avoid using standard library data structures with allocators. They may work fine as boilerplate, but are usually not what you want when you have any non-trivial requirements. std::vectors are fine if you don't mind the allocations - but you do, so not even those. You could use custom allocators, but that whole mechanism is totally broken design in the opinion of many; see Andrei Alexandrescu's talk about this subject: https://www.youtube.com/watch?v=LIb3L4vKZ7U
I migrated a rust LoRa driver from traditional blocking to async/await in order to test two modules in simultaneous send/receive mode as part of the test suite on a single STM32 chip. Aside from pleasing the HAL abstraction by bubbling all the generics throughout, it was an entirely pleasurable experience and made much better use of available resources than the traditional approaches without having to manually manage state across yield points or use a dynamic task scheduler. No OS, no runtime, no manual save/restore of stack, no global variables. It is really the future of the truly micro barebones embedded development.
State machines to deal with interrupts (to name the most basic kind of async event) is the ABC of embedded "bare metal" programming.
Whether async/await is cleaner, easier, readable, etc. is a matter of taste.
My point is that it varies at runtime but the type in the standard is constexpr so you can't actually rely on it unless you actually control where it executes.
EDIT: naturally I understand that compile makes sense for e.g. statically sizing array sizes etc.
...and that's a good thing
Each feature adds superlinear cost to understanding the language and the code you're reading.
Unfortunately the preprocessor is still needed for stringyfication and other symbol manipulation.
C++26 (2126 of course) will introduce enough reflection to finally get rid of the preprocessor.
Similarly constexpr itself is also genuinely ridiculous: (I have said this on hackernews before) It's such a stupid idea to require an annotation everywhere you want to evaluate things at compile time, practically everything will inevitably be evaluatable at compile time, and you need the implementation anyway, so just let it fail rather than ask for permission everywhere.
Having the keyword for variables and constants is fine (i.e. top down and bottom up constraints need to be dictated) but you shouldn't need to write constexpr more than that.
template<typename T>
auto length(const T& value) noexcept {
if constexpr (std::integral<T>::value) { // is number
return value;
}
else {
return value.length();
}
}My c++ is super old. But thanks that is great I could not figure it out.
km!1000
for 1000 kilometers. Here, km is a template that takes an integer argument: struct Kilometer { int v; }
template km(int V) { Kilometer km = Kilometer(V); }But oh how I hate `. I had to edit my layout to make it typable for stupid haskell infix functions
In D, you would have:
ubyte b = 0b1111_1111;Nodiscard is a way of “enforcing” a contract with the user about how your code needs to be used in order to avoid undefined behaviour.
(I say “enforcing” in quotes because, instead of being an actual constraint, it’s merely an attribute - so a conforming compiler can happily ignore it)
Here are two examples of where it is useful:
- Returning a success code that must be checked before proceeding with an operation that could have bad consequences if the previous operation failed. Unchecked operations are one of the major sources of bugs in the wild, so no discard at least points the user to the potential problem.
- Returning something that doesn’t make any sense to immediately discard. This is usually down to a mistake - such as calling vector::empty thinking that it’s going to clear the vector, when it actually returns a bool telling you if it’s empty or not (an awful name, but then so is vector…). It makes no sense to check if it’s empty without using that result, so the warning indicates that the user has made a mistake.
Using it to enforce that every return value is handled is stupid as it can lead to error blindness and you’ll ignore it when you actually need it.
The function is entirely useless and pointless to call without using the returned value, and [[nodiscard]] pointed this out to me. Perfect.
And making constexpr-ness an explicit contract makes sense to me: if it's not that it can be an unexpected property, and can break at any change of implementation.
Yes requiring a function being marked requires that the implementor do it, but it also means they have considered this use-case and made it officially part of the API. It's not a trivial promise to make.
> and you need the implementation anyway, so just let it fail rather than ask for permission everywhere.
"Let it fail" is the issue, if it's implicit a user can assume this is working by design, then find their program stops compiling on the next release not because the maintainer wilfully broke the API, but because they changed an implementation detail of the function and constexpr-ness is not something they considered (or assumed correct) in the first place.
Maybe the language should work the other way around and everything should be constexpr by default and functions should opt out of constexpr-ness, but that's 40 years too late for C++. And I can't think of any langage which does that. And frankly it feels like the wrong default for the reasons above.
So constexpr isn't a guarantee of it being evaluated at compile time, and non-constexpr isn't a guarantee of it being evaluated at runtime. Cool, huh?
D does. And it's a very popular feature. In fact, D goes even further - only the path taken through a function needs to conform when running it at compile time, not 100% of the function.
> it feels like the wrong default for the reasons above.
In about 16 years of very extensive use, it has never been brought up as a problem.
That's how lambdas behave in C++, so it's definitely feasible. gcc has a flag, -fimplicit-constexpr I think, that does this for regular functions, and it doesn't appear to cause any significant issues. I think there has been some talk around making this the language behavior at some point in the future.
It's actually very trivial. 99% of code I write can be run at compile time.
> maintainer wilfully broke the API
Did they break the API or did they change the semantics? The API is tittle-tattle, the point is that you cannot run it at compile time anymore (e.g. accidentally introduced a opaque new dependency by accident? Oops)
> And I can't think of any langage which does that
Being able to opt out is not what you want, but D allows everything to at least attempt to be run at compile time. This has been very profitable.
If you're in embedded and you're not pushing everything you can into constexpr, you're missing out on correctness and code size benefits.
to me it's been a very useful tool for reflection, for instance
if constexpr (requires { foo.someMember; }) {
use(foo.hasSomeMember);
} else {
some_fallback_case();
}1. Do this in types. 2. Do this without introducing a new scope inside a function.
int square(int x) { return x * x; }
static assert(square(3) == 9);
This allows one to write unit tests that are checked at compile time, whereas: int main() { assert(square(3) == 9); }
is always a runtime check.Compile time unit testing is a significant win:
1. it's always more productive to find problems at compile time rather than run time
2. it isn't necessary to conditionally turn off compilation of the tests for the release build
3. you can't forget to run the tests before shipping
That way you get benefits by default. Whereas right now it's not allowed to.
Note that I mean at the declaration not the callsite.
Maybe you'd like to do something like:
template<typename T>
struct ABC {
int x;
int y;
if constexpr (is_3d<T>) {
int z;
}
};
But yeah, that's not what if constexpr does.If you publish a library as "constexpr" you are indicating to your users that they can use it in a compile-time context and that future changes to your implementation will remain compile-time executable. If you just say "anything that can be computed at compile time gets auto-deduced to constexpr" then you rely on some library that is compile-time executable by coincidence but you really really really need it to be compile-time executable. Now when that library owner makes an edit that means it cannot be executed at runtime your code breaks.
This.
Constexpr should have been at the eval site, i.e something like:
consteval auto x = foo();
And foo() is just a normal function, if the compiler can eval it at compile time - all good - if not, compiler error. void test() {
int square(int y) { return y * y; }
int x = square(3); // evaluate at square at run time
enum y = square(3); // evaluate at square compile time
}
(Although, when the optimizer gets through inlining square(), etc., it will wind up at compile time anyway.)Rust is making good progress despite these issues but it's still a pain to use in anything but the most common systems. As soon as you're using SoCs that have an FPGA part, for example, you're forced to use proprietary vendor tools and good luck getting Rust to work with those in the next few decades...
Starting a job in a week which is embedded Rust, but more on the Linux-on-SoC side, we'll see it goes.
I believe this misconception on your part stems from a lack of awareness (or understanding) of C++ design goals as a language.
Have a look at this writeup: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p21... (caveat: not formally adopted)
constexpr supports the following goals:
* Provide the programmer control over every aspect of performance: Compile-time vs run-time execution is an aspect of performance.
* Excellent ergonomic: Using constexpr/consteval improves on earlier mechanisms for achieving compile-time evaluation, such as macros, template meta-programming etc. Those are unwieldy, often difficult to read, and inflexible (without jumping through hoops). constexpr is not.
etc. etc.
and marginally supports the following goals:
* Code should perform predictably: Control over what exactly runs at run-time improves predictability.
* Leave no room for a lower level language: While constexpr is neither low nor high as a language feature, one can argue that having it undercuts the potential for, say, a "C + constexpr"-like language, or another lower-level language with a more elaborate macro system.
etc. etc.
I could go on naming goals constexpr supports, there are lots more. I challenge you to find goals from which it detracts.
Not only was P2137 "not formally adopted" it was purposefully written so that the committee could say explicitly "No" to these goals. It's in some sense a manifesto for one of the 2022 "C++ successor" languages, Carbon not for C++.
If you want to cite actual goals, you won't get much from WG21, which prefers not to get tied down by having any principles to speak of. Bjarne himself however wrote a book in 1994 "Design & Evolution of C++" which might help you determine some goals.
I don't see constexpr functions in particular as a success. There are too many caveats, it's easy to find C++ programmers who've written what they assume is a constant evaluated expression but it isn't, for one of many reasons their compiler has decided it's runtime evaluated, and the programmer was surprised.
constexpr, amongst other uses, cleans up code that was hard-to-understand templated code.
Static assert cleans up other complicated constructs.
Make_unique together with heaps of other primitives tries to clean up cumbersome memory management.
[[fallthrough]] tries to solve the fact that break is easily forgotten, but this time not forgotten, but explicitly not there...
You can name so many of new constructs that solve an earlier problem.
If you went through all the cpp standards upgrades, and knew the history, you understand why all these improvements exist. If not, it all looks overly complicated, which in fact, if you look at it objectively, it is.
Executables from Nim also tend to stay pretty lightweight even when using generics / templates.
What is fucked up is us not being perfect machines and not producing ideal design from the first try and then having to deal with compatibility. Well duh.
Zig may be a better option when it comes to compile-time evaluation.
but - PMR allocators also have that weird fixation on types, which they should not; plus, dynamic allocation is expensive. It's particularly expensive to let your containers do reallocation willy-nilly on their own. I'd go with `std::array`, if I wanted something dynamic - a dynarray (which the standard doesn't even have).
After sum types (and pattern matching), constexpr is the next great language feature that will be everywhere soon.
I would love for Rust to fully adopt constexpr.
> you have to do that yourself
Yes. And it has a cost. Plus the cost of executing that state machine (changing states, etc.), which is hidden from you.
One can always pull a more optimized and fine-tuned state machine made by hand (sitting on WFI for example), and here comes the "taste" factor.
Has shades of the OCaml/StandardML "let ... " scope:
StandardML:
let
val a = 1
val b = 2
in
a + b
end
It's nice for making it clear that the scope of the assignment is restricted to this lexical scope, though there are other ways to do this in C/C++.I work in Rust these days so haven't had a chance to use much of this, but I wonder if this has some nice RAII type use patterns? I may now go explore.
[0]: https://devblogs.microsoft.com/oldnewthing/20230424-00/?p=10...
C++11 std::atomic had similar scope creep where it had atomic::is_lock_free as a runtime parameter. Nobody ever used it as is simply not something you care at runtime. So C++17 added is_always_lock_free as a compile time query which can be actually actionable.
Well, why not? They doubled it from 32B to 64B between the Pentium III and Pentium IV.
Apple could go to 128 because they were rolling out a whole new ISA, so were breaking compat anyways.
That said, they have amazing performance on the M1, and I wonder how much of that has to do with the wider L1 size.
> for some foot guns
There just aren't any with this feature.
The bigger issue is that you are at the whims of llvm as to what your codegen looks like. Regressions as llvm versions are upgraded are routine and can sometimes result in significant regressions on micro benchmarks. The good news is the rust team treats these pretty seriously even if they do take time (and several versions) to get fixed but you’ll never see the issue closed out in the issue tracker. All of performance regressions, codegen bloat regressions, and llvm optimization failures (eg not stripping out a panic handler in safe rust when it should be possible to deduce that the panic cannot be reached) are all tagged and monitored and tests are often added to catch regressions once fixes land. The good news is that when you run into these in safe rust you can usually work around them with unsafe rust - unreachable_unchecked() is your best friend here and is amazingly easy to use either with an if or with a match to disavow a potential state before the code that handles it.
In practice you do get guaranteed evaluation wherever the result is required at compile time (for example as a non-type template parameter or as an array size).
As for constexpr - you only get ambiguity if you assign a constexpr expression/function call to a non-constexpr value. If you use constexpr variables to hold the results of your computation, compile-time evaluation is guaranteed. I don't think it's that confusing.
What kind of embedded systems? Even AVR has a C++ compiler.
Yes, AVR has a C++ compiler, but I could imagine it doesn't support all language features. If it does it would still be discouraged to use dynamic memory allocation, which is pretty essential to leverage many advantages of object orientated languages.
Of course embedded systems aren't restricted to that anymore and you are perfectly fine to use C++ for the average ARM system that isn't as restricted memory wise.
void foo() {
if (auto [a, b, c] = std::make_tuple(x, y, z); true) { /* [...] */ }
}
Yeah you can still read this, but it's stupid to support constructs creating scope, doing control flow, and initializing arbitrarily many variables simultaneously (which may invoke constructors of their own). The relatively minor benefits to things like iterators are not outweighed by the burden of supporting this stupid code. void foo() {
{
auto [a, b, c] = std::make_tuple(x, y, z);
/* [...] */
}
}I'm of two minds on this. I can see the impulse and why you'd reach for it: Block lexical scopes with no if/while/etc statement attached read a bit odd. Introducing an "unattached" block means as a reader/reviewer I want to know why the scope has been created. So in this new syntax I suppose makes it "clearer" (in some respects) that what is being done here is introducing a new lexical scope specifically for the given variables. Like I commented elsewhere, this is somewhat similar to the ML-languages "let <assignments> in <block>" syntax, which I have always found admirable, as it makes clear to the reader (and compiler) what scope and state are being dealt with.
On the other hand, this is so out of step with C/C++ style generally, and it seems so excessively "clever" that I think it's going to piss people off. And because it's bolted onto the conditional expr, you get the pointless ;true there.
Having a with ( ... ) syntax would have been nicer?
with (auto [a, b, c] = std::make_tuple(x, y, z)) {
}
I'm curious what Titus Winters and the Google C++ style guide is saying about this.The goal is to get rid of this using C++20 modules (and potentially reduce compile times due to repeated expansion of header files).
> the file and line macros
This is addressed by C++20's std::source_location [0]
> I still need it for string interpolation
If you mean the `#` macro operator, then yeah that is still needed.
The point is to provide ways of doing these tasks that don't require a weird text replacement macro language and instead do them with C++. Obviously due to legacy codebases the preprocessor is going to stick around.
[0] https://en.cppreference.com/w/cpp/utility/source_location
IDK what to say....a 40+ year old language has some warts and it has old code using those warts.
Doesn't mean you have to be stuck actually using them forever and ever
I do keep hoping for something else to get big enough we can use it because C++ sucks, but is still better than the alternatives we can use.
Good for you, you missed the point.
> Did they break the API or did they change the semantics?
What are you even on about?
> The API is tittle-tattle
What are you even on about, part 2.
> the point is that you cannot run it at compile time anymore
Yes, the API allowed one thing, now it does not. That's no different from changing an optional parameter to mandatory or any other thing which breaks the API.
>> Did they break the API or did they change the semantics?
> What are you even on about?
Constructive.
One is the provider of the API promising that the function supports compile-time evaluation.
The second is the consumer of the API ensuring that an expression has been compile-time evaluated. It's nice that the consumer can make sure, but that's not helpful if the provider of the API never specifically intended for a function to be evaluatable at compile-time.
> Whereas right now it's not allowed to.
Of course it's allowed, but you're at the mercy of the compiler's decisions.
It's not. If you to use a function in a constexpr context then it will complain it needs to be marked as such. So the API creator needs to label every function in case a client wants to use it.
I know there are a lot of people out there still writing C and I've come to accept that I will never understand or agree with those people, but to be writing C++ and passing raw pointers around in 2023 is a hell of a choice IMO.
Having said that: It's not really unlike C++, since you have it in for loops:
for (int x = 0; x < n; x++) { do_stuff(); }
which is like {
int x;
for (x = 0; x < n; x++) { do_stuff(); }
}
anyway, I wouldn't mind the syntactic sugar of "with X=Y do Z" or "let X=Y in Z"If I'm writing new code, probably not. But since it's a 40+ year old language, I'm doing more comprehension, maintenance, and extension of old code than writing new, and every new feature makes that task harder over time.
Also in the meantime, most of the cool stuff in D is showing up on the languages that come on those SDKs (C++, Java, C#), weakening the argument to look outside of SDK supported languages.
Doesn't matter if D had it first, or if it has a better implementation, worse is better, when ecosystem, tooling, IDE and technical support are part of the equation.
So it remains a language for hobby coding.
> most of the cool stuff in D is showing up on the languages
It's true that many aspects of D are copied by other languages, but badly.
Badly copied doesn't matter, it is one reason less to look for where it came from.
constexpr is a guarantee that you can use the thing in a constexpr context, and this is where the "evaluated at compile-time" guarantee can come from:
template<typename T>
auto func() {
// here some compilers can still choose to evaluate x at run-time - and very likely all of them if no optimizations are enabled
constexpr int x = f();
// but here it becomes mandatory for this use of x to be evaluated at compile-time, since the number is literally going to be part of the compiled binary as part of the function name mangling
return std::integral_constant<int, x>{};
}Except of course the subset of parameters for which it is constexpr callable can't be checked at function definition time (if it exists at all), only at function invocation time.
Which makes the constexpr annotation useless and it is in only because the authors couldn't otherwise get the paper through some committee objections.
In this case though the underlying reason is that its part of the type (system) not because of the mangle specifically.
For an assertion that something is evaluated at compile time I’d assume a variable-level annotation (in a non-constexpr function). Though I don’t know c++ enough to have any idea whether that’s the case.
And contant evaluation optimisation has always been a thing so it’s not really surprising.
The problem isn't that you're able to call it at runtime with runtime data, it's that if you give it compile-time data you have no idea when it will be run.
> For an assertion that something is evaluated at compile time I’d assume a variable-level annotation (in a non-constexpr function). Though I don’t know c++ enough to have any idea whether that’s the case.
Oh, were you under the impression that constexpr was just for functions? It applies to variables too, and it's not a guarantee on them. You need to use other, newer annotations.
Forgive me, but can you be clearer than "mentioned"? Is the mangling required to contain template parameters for return types?
> In this case though the underlying reason is that its part of the type (system) not because of the mangle specifically.
I'm not sure. The compiler knows it will always be the same type, so under many uses of this function I could easily imagine a compiler that doesn't actually fill in .value until runtime.
The mangling will contain template parameters, as you can have:
foo.hpp
template<typename T>
T f();
foo.cpp template<>
int f<int>() { return 123; }
template<>
float f<float>() { return 123; }
bar.cpp std::cout << f<int>();
and the right function has to be found. demo: https://gcc.godbolt.org/z/rMjYoEzaKSo in the example several layers above, T uniquely identifies the function. I don't see any need to involve std::integral_constant<int, x> in the mangling.
I think this subset of C++ templates is probably undecidable still.