Combine C and C++ in STM32 application development
Overview

As has already been stated, C is not a strict subset of C++. C is its own language. That said, compilers are usually (not always, though) designed to generate object code files which are then read up by a linker tool. Linkers are often very general tools and so long as they can properly read the generated object files and can resolve the unknowns they find in those multiple object files in order to produce a final binary package of some kind, they will happily link outputs from different compilers. Even so, that doesn't mean the resulting package will do anything, correctly.

Let me discuss some problems to consider, assuming that you can in fact use a specific C compiler and a specific C++ compiler (or use the same compiler with different options so that it acts as a C or a C++ compiler, but not both) and where the linker can read the object files fine and will attempt to resolve symbols and map out addresses just fine for some specific target device in mind.

Before reaching main()

Without getting into intimate details, both C and C++ tend to assume they can provide the linker with a "starting point" for your code. You need a starting point, so that when a target system starts up there is a particular place in your code to begin. But, in reality, neither C nor C++ actually starts running code with main(). They start somewhere else, because there are lots of little details that need to be set up for you before that takes place. The code for the starting point in C, for example, usually is found in something generically called "crt0." Here, the memory block needed to initialize heap management functions, malloc() and free(), is found and set up. Here, if necessary, all of your static lifetime variables are set up with their initial values (which may have to be copied out of flash into ram.) Etc. C++ has similar needs, as well. But they are different in C++ than in C. For example, C++ uses a different heap space memory manager by default.

So, who's run-time start-up code gets executed?? How does the linker guess about this? Normally, the linker is provided with a repeated record inside all of the object files that over and over states the same thing -- link 'crt0' (or the equivalent C++ need) into the project. It is here that the call to main() exists, for example. It is here that all that initialization gets done before calling main(). And this is usually provided through hidden linkages that you don't get to control (or, at least, not in an obvious way.)

(Another option for this initialization, which was used in the early days with Unix, was to have the executable on disk provide the necessary segments and to let a program loader within the O/S do this initialization work. The O/S provided heap space management, directly. The starting point was then always assumed to be the first binary byte of the code space found in the file.)

You'll need to work out how all this gets done with the linker and how it might be over-ridden, or else how you might write your own 'crt0' code that deals with the libraries of both C and C++. It's also possible that your compiler tools will make this easier for you. I can't say. But at least you must be aware of this hidden behavior and have a plan for managing it, correctly.

Library compatibility

This is often a big problem. If your C++ code refers to library code and your C code also refers to library code, it's unlikely (unless the compiler vendor is the same for both compilers and has worked to achieve this) that these will be compatible in their initialization requirements. As mentioned above, there is initialization code that executes before starting main(). But not only initialization code. There is also exiting code (for example, in C there is atexit() code that takes place if your C program should exit -- not something usually done in an embedded application, but that doesn't mean it never happens.)

There is almost a certainly going to be competing issues if the compilers come from different vendors. And even if from the same vendor, they may not have smoothed out this road for you. So this is another "sand trap" to get through.

C++ name-mangling

C++ buries its ability to resolve functions with the same name by looking at the parameter types and number in something usually implemented as "name mangling." This just means that the name of the routine (which the linker looks for) is modified so that it includes some "secret code" as part of its name. This extra code added to the function name allows the linker to match up the right calls to the right routines.

But this presents a problem for C. You have NO WAY of knowing how the C++ compiler mangled the names. So you can't know how to call them from C. Most C++ compilers intended for multi-language linking will include some way of making their C++ functions work with fixed naming. Often, this is where you provide a separate function, compiled by the C++ compiler, that is emitted as a "C"-interface function but where this "C"-interface function is compiled by the C++ compiler and can call the C++ function you wanted. Then the linker can work out the rest of the details. Of course, that requires that the C++ compiler supports this idea. I don't think it's a requirement of the standard.

Call invocation

If you are using different compiler tools for C and C++, it's possible that the compilers assume different things about how calls are made. (Beyond the name-mangling issue.) These are assumptions about how parameter values are passed (they aren't required to be on the stack, as compilers are free to choose alternatives), which registers must be preserved across a function call and which can be scratched, how local variable allocation is performed, and so on. Lots of assumptions are buried in the prologue and epilogue code for every compiled function. And it is vital that these things are compatibly arranged. You can't assume they will be. So you must verify.

The rest of the story

Let's assume that your compilers and linker work together well and you can use a mixed-language approach to your code.

I'm still very cautious of C++ when coding embedded applications. Some reasons include:


partial template specialization
vtables
virtual base object
exception handling
activation frame
activation frame unwind
use of smart pointers in constructors, and why
return value optimization


That's just a short list.

Let's take a quick look at C++ exception semantics to get just a flavor.

A C++ compiler must generate correct code for compilation unit \$A\$ when it has absolutely no idea what kind of exception handling may be required in separate compilation unit \$B\$, compiled separately and at a different time.  

Take this sequence of code, found as part of some function in some
compilation unit \$A\$:

   .
   .
   foo ();
   String s;
   foo ();
   .
   .


For discussion purposes, compilation unit \$A\$ doesn't use 'try..catch' anywhere in its source. Neither does it use 'throw'. In fact, let's say that it doesn't use any source that couldn't be compiled by a C compiler, except for the fact that it uses C++ library support and can handle objects like String. This code might even be a C source code file that was modified slightly to take advantage of a few C++ features, such as the String class.

Also, assume that foo() is an external procedure located in compilation unit \$B\$ and that the compiler has a declaration for it, but does not know its
definition.

The C++ compiler sees the first call to foo() and can just allow a normal activation frame unwind to occur, if foo() throws an exception. In other words, the C++ compiler knows that no extra code is needed at this point to support the frame unwind process involved in exception handling.

But once String s has been created, the C++ compiler knows that it must be properly destroyed before a frame unwind can be allowed, if an exception occurs later on. 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 do), the compiler must have placed code designed to handle the destruction of String s before letting the usual frame unwind occur. This is different than the code required for the first call to foo().

(It is possible to add additional decorations in C++ to help limit this problem. But the fact is, programmers using C++ simply must be far more aware of the implications of each line of code they write.)

Unlike C's malloc, C++'s new uses exceptions to signal when it cannot perform raw memory allocation. So will 'dynamic_cast'. (See Stroustrup's 3rd ed., The C++ Programming Language, pages 384 and 385 for the standard exceptions in C++.) Compilers may allow this behavior to be disabled. But in general 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. (Stroustrup has publicly lamented this.)

Without partial template specialization (not all C++ compilers support it), the use of templates can spell disaster for embedded programming. Without it, code bloom is a serious risk which could kill a small-memory embedded project in a flash.

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."

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.

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 (when you feel you can even afford exceptions in your embedded code.)

C++ compilers can automatically generate constructors, destructors, copy constructors, and assignment operators for you, with unintended results.  It takes time to gain facility with the details of this.

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++ may copy 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() in C 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 but they should make themselves aware of these issues before using them.

Summary

The above is just a broad discussion. Exception handling in C++ requires a certain arrangement with the activation frames, which must somehow include ways to track down and use exception handlers. C doesn't have this requirement for its activation frames. So if C++ code is called by C code, and the C++ code in any possible way also calls some C code before returning, then it is quite possible that the sequence of activation frames will include some "illegal frames" that cannot be processed by the exception handling routines used by C++. And this could lead to fatal results.

The upshot is that you need to know a great deal about your compiler tools (and linker tool) to be sure you can apply both to the same application.

Educate yourself.