
Practical Guide to Bare Metal C++ - adamnemecek
https://arobenko.gitbooks.io/bare_metal_cpp/content/
======
vvanders
Nice to see some concrete examples of where you don't want exceptions, rtti or
dynamic memory.

Quite a few developers believe that if you don't have these features you
aren't writing "real C++" but it's common practice in quite a few industries
where size/performance is critical.

~~~
jandrewrogers
Maybe my experience is atypical but RTTI and exceptions are features of C++
that I never see used in real-world C++ almost regardless of the application.
When I have seen them used, it is by people new to C++ that haven't fully
figured out the common development idioms of the language yet.

~~~
jlarocco
I've never seen RTTI use in production code. Even with modern C++, RTTI almost
always adds too much runtime overhead and negative performance impacts. Most
importantly, it's usually a sign of bad design.

My experience with exceptions is different, though, and I see them used quite
a bit. It depends a lot on the age of the code base and the group working on
it. Old code is less likely to use them, and developers with a heavy C
background, or a lot of pre-C++98 experience are less like to use them. Except
for maybe some tiny embedded systems (like microcontrollers), the overhead of
exceptions isn't that bad any more.

~~~
fauigerzigerk
The strange thing about exceptions in C++ is that they make ubiquitous use of
RAII a necessity, but at the same time they must not be thrown in destructors.

And then C++ exceptions don't come with a stack trace that makes quick and
dirty exceptions based code so convenient in Java or C#.

I was never a principled opponent of exceptions, but I find myself removing
them from more and more code the more thoroughly I think about all the
possible error states of code that should really be robust.

~~~
maxlybbert
I don't find it weird that exceptions shouldn't be thrown from destructors. I
remember being surprised when I learned that rule, but the reasoning seems
sound to me: what _can_ you do when the cleanup failed? Clean up harder?

~~~
fauigerzigerk
The short answer is that action B may depend on whether or not action A was
able to finish successfully, including any cleanup.

~~~
maxlybbert
The official reason destructors are discouraged from throwing exceptions is
that one reason destructors get called is that something already threw an
exception, and there is currently no way to handle two exceptions in flight at
once, so in that case the process gets killed.

It's also true that with more experience, we've figured out that cleanup code
is special. When I throw an exception during normal processing, I want to
abandon what I'm doing. But if I discover that one step of a destructor cannot
be completed, I do _not_ want to abandon the rest of the cleanup. If I throw
an exception in a destructor, I can't come back later to finish any remaining
steps.

And, as I asked earlier, if I throw an exception because I can't close a file,
what do I expect the exception handler to do? I guess it could try closing the
file again, but I'm not going to hold my breath on that working. Perhaps the
file is on a network share and the network's gone down. Perhaps closing the
file will involve writing some cached data to disk, but the disk is full. In
my experience, in this case your only real options are "ignore it, either
because there's nothing you can do or because you're already dealing with an
exception and you've simply discovered another symptom of the original
problem" or "kill the program because there is no hope." You can log the fact
that a step failed, but remember that it's possible for logging to fail, if
the disk is full.

~~~
fauigerzigerk
_> It's also true that with more experience, we've figured out that cleanup
code is special._

In my experience the exact opposite is true. Error handling or "cleanup" code
is in no way special. Errors are not exceptional and there is nothing we can
say in general about what error handling code will or will not need to do.

Error handling code needs to be able to use the entire language and reuse all
the regular code that is used elsewhere. A language should not have two modes,
an error mode and a normal mode with completely different non-local semantics.

 _> And, as I asked earlier, if I throw an exception because I can't close a
file, what do I expect the exception handler to do?_

That depends entirely on the context of the program you're writing. It might
want to do things like writing a record to a database that marks the file as
invalid/corrupt. It may need to notify some other system about the failed
action. It might want to roll back a transaction or initiate a compensating
transaction. It may want to abort some session and schedule it to restart at a
later time.

There is simply nothing we can know in general about what a caller of an
action that failed to complete cleanly may want to do as a result of that
failure.

Using exceptions for error states that may require a complex response is just
not a good idea and in C++ it's an even worse idea.

[Edit] But to be clear, my initial thought wasn't about exception handling but
rather about ubiquitous use of RAII for all sorts of stuff that needs to get
done at the end of a scope. Much of that has nothing to do with errors at all.
Maybe if you think of these situations it will become clearer why I think that
arbitrary limitations of code that runs in a destructor is problematic. C++
has no finally block. Destructors is all we have.

~~~
quicknir
I don't agree with much in this post, but in particular "Destructors is all we
have": destructors + lambdas + templates allow you to write ScopeGuard, which
is a pure superset of finally blocks. Modern C++ has zero need for finally.

~~~
fauigerzigerk
ScopeGuard relies on destructors, so it inherits the no exceptions in
destructors issue.

~~~
quicknir
Throwing from a destructor in ScopeGuard is equivalent to calling a function
that can throw in a catch or finally block, which you can do in most (any?)
languages with exceptions. This no exceptions in destructors "issue" is not a
C++ issue. It's a fundamental issue in error propagation. What do you do when
propagating error correctly, causes a new error? You can't propagate the first
error, because you can't do it correctly. You can't propagate the second
error, because that would mean dropping the first.

Classic example is logging an error on failure. This means calling a logging
function in the catch block, and then letting the exception propagate. But
what if the call to the logging function fails? In Java, coded naively you'd
simply drop the original exception. Usually that's not what you want.

You can examine the issue with error codes, it's not any better.

------
Taniwha
He's talking about real time code here and completely missing one of the main
reasons why you have to use great care using C++, and even parts of the C
libraries ... all those hidden mutexes in new/malloc and other parts of the
standard libraries (stdio too)

I'm sure we all know what happens when your ISR quietly does a new while some
other part of your code is holding one of the locks deep in malloc.

But far more important is avoiding priority inversions when a low priority
thread is holding a lock in a library somewhere, they result in high priority
threads missing real-time deadlines - the sort of heisenbugs that are pretty
impossible to find .... and are best to avoid by design.

~~~
webkike
Well, if you're programming bare metal you shouldn't be using any library's
allocation routines but your own. Because, remember the cardinal rule: no
mutices in ISRs!

~~~
Taniwha
I was kind of pointing out that people know that mutexes in ISRs are a bad
idea, but don't know as much about the other dragons that live in the same
swamp ... they may also simply not be aware of what's inside their libraries
(which ones use mutexes? 'man printf' doesn't warn me)

my point is that there are hidden mutexes in libraries that people often don't
know about - his example of replacing new() with malloc() doesn't solve the
problem, instead you need to be able to code your C++ without new, or at least
the real-time bits of it (and that includes using stuff like strings and their
libraries that do new() behind your back, or even printf which can run foul of
the stdio hidden mutexes)

It's not just ISRs, priority inversion is a real problem that results in some
of the hardest bugs to find (I once had to chase one that happened only once a
month, resulting in satellite systems losing their provisioning)

~~~
erichocean
> _I was kind of pointing out that people know that mutexes in ISRs are a bad
> idea, but don 't know as much about the other dragons that live in the same
> swamp_

Honestly, these "people" seem like mythical creatures to me; I've certainly
never met one. If you're qualified to write an ISR in C++, you're qualified to
understand which parts of C++ are valid to use in an ISR and which aren't.

This is a non-problem that does not appear in practice, even though it could
_in theory_.

~~~
pjmlp
Somehow I can imagine that not being the case with offshored projects.

------
devbent
If you go down to micro-controller levels, you often times are stuck with C++
2003 and a vendor specific compiler, which means you will lack many of the
niceties in the article.

I agree with him that removing the entire standard library is needed. Of
course you then need to copy an implementation of printf from somewhere, and
likely set it up to only work in your debug builds. Then you quickly figure
out that the standard library has a lot of things you didn't even realize you
depended on. Math functions typically pop up next, you'll likely end up using
a vendor library (by which I mean ARM's), but if you are doing a bunch of math
heavy work, and even more so if your chip has some limited FP capabilities
(assuming it has an FPU at all), you may also find the need to re-implement
some functions based on your performance needs.

Embedded is fun. :)

All that said, I would kill for access to proper lambdas.

~~~
taneq
> If you go down to micro-controller levels, you often times are stuck with
> C++ 2003

This makes me feel so old. Back when I started, C on a micro-controller was a
rare treat and most things were done in some obscure chip-specific assembler.

I had a play with an Arduino knockoff a few weeks ago, kids these days don't
know how good they've got it!

------
amaks
Risking being down voted, I doubt that c++ is the right language for bare
metal development at all with its constraints like exceptions and RTTI. Rust
seems to be the modern and safer version of C that is more appropriate for
bare metal development.

~~~
pjmlp
For that Rust still needs to win the hearts of all those manufacturers selling
hardware development kits.

Most of us just get to use the toys already in the package and aren't allowed
to bring new ones to the party.

------
Animats
The Arduino development environment is really GCC C++ with a platform-specific
library. All the compile-time C++ features work. Some features that need run-
time support may not link.

------
Kenji
_The usage of single throw statement in the source code will result in more
than 120KB of extra binary code in the final binary image. Just try it
yourself with your compiler and see the difference in size of the produced
binary images._

What?? 120KB? I have to try that out right away.

Edit: That turned out to be completely wrong. Simple program without throw:
71'218 bytes. With throw (one additional line that throws a plain integer):
71'814 bytes. GCC 4.8.1 on Windows. Would have been surprised if that was
true.

Edit2: Okay, my bad, I should have linked statically. The statement sounded so
absolute, but of course it is conditioned on the bare metal environment which
is the subject of this article. Thanks for pointing out where I went wrong.

~~~
jibsen
Using mingw-w64 (GCC 6.1.0):

    
    
      g++ -Os -static -s on `int main() { }' 17.408 bytes
      g++ -Os -static -s on `int main() { throw 42; }' 139.776 bytes

~~~
nomel
Now what if you use two exceptions? If there's no significant increase, then
the whole point is moot.

~~~
jotux
Not really moot. I do C++ on embedded processors with as little as 32kB of
program space. Using a single exception means your code will not fit. My
general rule of thumb to use full-fledged C++ is a processor with at least
256kB of program space.

Here's a comment of mine from a previous discussion:
[https://news.ycombinator.com/item?id=11706840](https://news.ycombinator.com/item?id=11706840)

------
_pmf_
Template oriented programming is a tremendously useful technique for working
with overhead-less abstractions for embedded software, but C++ makes it so
hard. Most large users of template oriented development (automotive) use
external code generators for what should ideally be supported by the language
itself.

