Re: C++ in embedded systems
From: Jonathan Kirwan (jkirwan_at_easystreet.com)
Date: 08/14/03
- Next message: Dingo: "Re: C++ in embedded systems"
- Previous message: Grant Edwards: "Re: C++ in embedded systems"
- In reply to: Grant Edwards: "Re: C++ in embedded systems"
- Next in thread: steve at fivetrees: "Re: C++ in embedded systems"
- Reply: steve at fivetrees: "Re: C++ in embedded systems"
- Reply: George Neuner: "Re: C++ in embedded systems"
- Reply: Mogens Hansen: "Re: C++ in embedded systems"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Thu, 14 Aug 2003 18:52:45 GMT
On 14 Aug 2003 17:34:12 GMT, grante@visi.com (Grant Edwards)
wrote:
>In article <Xns93D77EA71860jpmvrealtime@216.128.74.129>, Martin Vuille wrote:
>
>> Aside from that, there is _nothing_ in C++ itself which makes it
>> more or less suitable for embedded applications than any other
>> language.
>
>I disagree. I think that in embedded work, it's import for the
>programmer to have a thorough understanding the the language
>and what the compile is going to do. From what I've seen,
>that's possible with C, Modula-[23], Ada, and various other
>languages. It doesn't seem to be possible with C++. The
>_language_ itself is too complex and baroque to understand
>completely enough for it to be used effectively in some
>embedded environments.
Agreed. I've spent some time getting familiar with the code
generated by C+ compilers for various constructs. Mostly, this
has been with compilers designed for the x86 used in PCs, but
it's not limited to that. There's also a few articles and a
book or two around which discuss the implementation details,
too.
Templates instantiated for a data type often cause functions I
don't actually use to be generated __and__ linked, for example.
I know that if everything is done which should be done and the
linker and compiler work together flawlessly, it's possible to
correctly eliminate almost all of this. But what chance is
there that an embedded compiler (other than GNU?) tool chain
will get the time put in, here? Even in the case of x86
compilers, where there is a great deal of effort and time spent
in development, they fail to get this done well enough for many
smaller embedded systems.
For these smaller systems, too, partial template specialization
is almost a requirement. Who provides that for embedded work?
So, don't use templates, you say?? Okay. Then what about
exception handling?
The compiler still generates defensive code even when I don't
use any syntax in my code for them; due, in part, to the
separate compilation requirement, if nothing else.
So, don't use exceptions, right?? But a properly functioning
C++ compiler must generate correct code for source code unit A
when it has absolutely no idea what kind of exception handling
may be required in source code unit B, compiled separately and
perhaps at a very different time. And linkers I'm aware of
don't cover this issue, either.
Take this sequence of code, found as part of some function in
some arbitrary source code file:
.
.
foo ();
String s;
foo ();
.
.
For purposes here, let's assume this fragment occurs as part of
a function sitting in a module that nowhere in it uses or
handles expections in any way. Let's say, in fact, that this
module *could* be compiled by a vanilla C compiler, except for
the use of class objects illustrated above. That's the only
aspect of C++ that's in use, here. In fact, let's say that this
use of a string is the *only* non-C aspect in the explicit code.
Okay?
Now assume that foo() is an external procedure and that the
compiler has a declaration for, but does not know its definition
at this point in time.
The C++ compiler sees the first call to foo() and can just allow
a normal stack unwind to occur if foo() throws an exception. In
other words, no extra code is needed at this point to support
the unwind process. So this call to foo() requires nothing
special, no C++ "fluff," so to speak. The compiler generates
nothing it wouldn't generate if it were a C compiler.
But once String s has been created, the C++ compiler knows that
it must be properly destroyed before an unwind can be allowed if
an exception occurs later on. It doesn't matter whether the
function in this module uses exceptions. Or even if the module
itself ever does. It only matters that the exception *might*
happen, even outside the module, because foo() is an external
function and the C compiler cannot verify that foo() doesn't
call some other function which *can* throw an exception.
So the second call to foo() is semantically different from the
first. If the 2nd call to foo() throws an exception (which it
may or may not) in foo(), the compiler must have placed code
designed to handle the destruction of String s before letting
the usual unwind occur. This means that the first foo() gets
coded up differently than the second foo() call.
Now, the above is a case where a common, garden variety embedded
programmer would imagine that the two calls to foo() would take
the same time, require the same resources, and otherwise be the
same.
How about this simple bit of C++ code:
struct T { ~T() { /* assume some non-trivial code here */ } };
extern void foreign ();
void example ();
void example () { T s; foreign (); }
This specifies a destructor for T which must be called in case
of exceptions. example() calls a foreign function which may
generate exceptions. Since the compiler of this code has no way
to be certain that foreign() cannot throw an exception, it must
assume that it might. Accordingly, the compiler needs to
generate the necessary code to handle the destruction call to
obliterate the object T if such an exception does occur in
foreign().
In other words, exception handling quite often comes at a price,
even in functions that the programmer knows cannot generate
exceptions. And this is especially true because of the
semantics of classes, and their associated destructors and
constructors.
Unlike C's malloc, C++'s new is supposed to use exceptions to
signal when it cannot perform raw memory allocation. In
addition, so will <dynamic_cast>. (See Stroustrup's 3rd ed.,
The C++ Programming Language, pages 384 and 385 for the standard
exceptions in C++.) Many compilers allow this "proper behavior"
to be disabled, of course. But if you stay true to C++ form,
you will incur some overhead due to properly formed exception
handling prologues and epilogues in the generated code, even
when the exceptions actually do not take place and even when the
function being compiled doesn't actually have any exception
handling blocks in it.
Stroustrup has publically lamented this practical reality.
So, don't use classes with constructors and destructors?
Right.
Some more thoughts,
When a C++ function returns an object an unnamed compiler
temporary is created and destroyed. Some C++ compilers can
provide efficient code if an object constructor is used in the
return statement, instead of a local object, reducing the
construction and destruction needs by one object. But not every
compiler does this and many C++ programmers aren't even aware of
this "return value optimization." I know that they should be
and that when you use C++ features, you need to know what they
cost you. But it remains one of those common risks which C
exposes (because it doesn't support these semantics directly)
and which C++ can too easily hide.
Providing an object constructor with a single parameter type may
permit the C++ compiler to find a conversion path between two
types in completely unexpected ways to the programmer. This
kind of "smart" behavior isn't part of C. C++ compilers can
automatically generate constructors, destructors, copy
constructors, and assignment operators for you, with unintended
results.
A catch clause specifying a base type will "slice" a thrown
derived object, because the thrown object is copied using the
catch clause's "static type" and not the object's "dynamic
type." A not uncommon source of exception misery (assuming you
can afford exceptions in your embedded code, in the first case.)
Passing arrays of derived objects to a function accepting arrays
of base objects, rarely generate compiler warnings but almost
always yields incorrect behavior.
Since C++ doesn't invoke the destructor of partially constructed
objects when an exception occurs in the object constructor,
handling exceptions in constructors usually mandates "smart
pointers" in order to guarantee that constructed fragments in
the constructor are properly destroyed if an exception does
occur there. (See Stroustrup, page 367 and 368.) This is a
common issue in writing good classes in C++, but of course
avoided in C since C doesn't have the semantics of construction
and destruction built in. Writing proper code to handle
the construction of subobjects within an object means writing
code that must cope with this unique semantic issue in C++; in
other words "writing around" C++ semantic behaviors.
C++ copies objects passed to object parameters. For example, in
the following fragments, the call "rA(x);" may cause the C++
compiler to invoke a constructor for the parameter p, in order
to then call the copy constructor to transfer object x to
parameter p, then another constructor for the return object (an
unnamed temporary) of function rA, which of course is copied
from parameter p. Worse, if class A has its own objects which
need construction, this can telescope disasterously. (A C
programmer would avoid most of this garbage, hand optimizing
since C programmers don't have such handy syntax and have to
express all the details one at a time.)
class A {...};
A rA (A p) { return p; }
// .....
{ A x; rA(x); }
longjmp doesn't have a portable behavior in C++. (Some C
programmers use this as a kind of "exception" mechanism.) Some
C++ compilers will actually attempt to set things up to clean up
when the longjmp is taken, but that behavior isn't portable in
C++. If the compiler does clean up constructed objects, it's
non-portable. If the compiler doesn't clean them up, then the
objects aren't destructed if the code leaves the scope of the
constructed objects as a result of the longjmp and the behavior
is invalid. (If use of longjmp in foo() doesn't leave a scope,
then the behavior may be fine.) This isn't too often used by C
embedded programmers and they should make themselves aware of
these issues before using them. So don't use longjump()?
How many people using C++ know exactly how the vtable mechanism
works? In the face of multiple inheritance? With virtual base
objects? How many people how a C++ compiler supports dynamic
casts or know what causes the C++ compiler to generate (or not
generate) support for it? What mechanism does a C++ compiler
use for exception handling? What does it cost? Where? If one
stays away from a mechanism, how much of it is still present?
And all the above is just from the cuff -- there is much more.
Jon
- Next message: Dingo: "Re: C++ in embedded systems"
- Previous message: Grant Edwards: "Re: C++ in embedded systems"
- In reply to: Grant Edwards: "Re: C++ in embedded systems"
- Next in thread: steve at fivetrees: "Re: C++ in embedded systems"
- Reply: steve at fivetrees: "Re: C++ in embedded systems"
- Reply: George Neuner: "Re: C++ in embedded systems"
- Reply: Mogens Hansen: "Re: C++ in embedded systems"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Relevant Pages
|