Editor’s Note: A bare bones guide to the C++ language for C programmers, excerpted from Software engineering for embedded systems by Mark Kraeling.
There are a number of reasons developers may want to consider using C++ as the programming language of choice when developing for an embedded device. C++ does compare with C in terms of syntactical similarities, in addition to memory allocation, code reuse and other features. There are also reasons to take caution when considering C++ and its related toolsets.
One reason is that functionality and performance vary across compilers due to differing implementations of the standard by individual vendors and open-source offerings. In addition, C++ and its libraries tend to be much larger and more complex than their C language counterparts. As such, there tends to be a bit of ambiguity in the community around C++ as a viable option for embedded computing, and more specifically what features of the language are conducive to embedded computing and what features should generally be avoided.
When characterizing the cost of various aspects of using C++ for embedded software development, we characterize cost as something that requires runtime resources. Such resources may be additional stack or heap space, additional computational overhead, additional code size or library size, etc. When something can be done offline a priori by the compiler, assembler, linker or loader, we consider those features to be inexpensive and in some cases absolutely free.
As behaviors differ across compilers and vendors, the burden is ultimately placed on the developer and designer to ensure that said benefits are actually achieved with a given development environment for the target architecture. Lastly, development tools change over time as new functionality is added, features are deprecated, performance is tuned and so forth.
Development tools are highly complex interdependent software systems, and as such there may periodically be regressions in performance of legacy software as tools evolve. A periodic re-evaluation of features and performance is encouraged. The topics discussed in this section are furthermore presented as general trends, and not meant to be an absolute for any specific target or toolset implementation.
Relatively inexpensive features of C++ for embedded
In the following section I detail C++ language features that are typically handled automatically by the compiler, assembler, linker and/or loader effectively free. That is to say they typically will not incur additional computational or storage overhead at run-time, or increase code size.
Static constants. C++ allows users to specify static constants in their code rather than use C-style macros. Consider the example below:
#define DRIVE_SHAFT_RPM_LIMITER 1000
const int DRIVE_SHAFT_RPM_LIMITER 5 1000
Developers may take pause in that the C++ language implementation will require additional storage space for the variable DRIVE_SHAFT_RPM_LIMITER. It is the case, however, that if the address of said variable is not used within the code and rather the literal value 1000 is used in computation, the compiler will fold in the value as a constant at compilation time, thus eliminating the storage overhead.
Ordering of declarations and statements . In the C programming language, programmers are required to use a specific sequence whereby blocks start with declarations followed by statements. C++ lifts this restriction, allowing declarations to be mixed in with statements in the code. While this is mostly a syntactical convenience, developers should also use caution regarding the effect on the readability and maintainability of their code.
Function overloading . Function overloading pertains to the naming conventions used for functions, and the compiler’s ability to resolve at compile time which version of a function to use at the call site. By differentiating between various function signatures, the compiler is able to disambiguate and insert the proper call to the correct version of the function at the call site. From a run-time perspective, there is no difference.
Usage of namespaces. Leveraging code reuse has the obvious benefits of improving reliability and reducing engineering overhead, and is certainly one promise of C++. Reuse of code, especially in the context of large software productions, often comes with the challenge of namespace collisions between C language functions depending on how diligent past developers have been with naming convention best practices. C++’s classes help to avoid some of these collisions, but not everything can be constructed as a class (see previously); furthermore existing C language libraries must still be accommodated in many production systems.
C++’s namespaces resolve much of this problem. Any variables within the code are resolved to a given namespace, if nothing else the global namespace. There should be no penalty in using these name spaces for organizational advantage.
Usage constructors and destructors. C++ adds the functional of “new” and “delete” operators for provisioning and initializing heap-based objects. It is functionally equivalent to using malloc and initialization in C, but has the added benefit of being easier to use and less prone to errors in a multi-step allocation and initialization process.
C++’s “delete” functionality is also similar to “free” in C; however, there may be run-time overhead associated with it. In the case of C, structs are not typically destructed like objects in C++. Default destructors in C++ should be empty, however. One caveat with new/delete is that certain destructors may throw run-time exceptions which would in turn incur overhead. Run-time exceptions are described in more detail in the following subsections.
Modestly expensive features of C++ for embedded
The following groups of features do not necessarily need to impact the program run-time versus their C programming counterparts, but in practice they may have an effect depending on maturity and robustness of the compiler and related tools.
Inlining of functions. The subject of inlining functions for C++ is a very broad one, with far-reaching performance impacts ranging from run-time performance to code size and beyond. When designating a function to be inlined, typically the “inline” keyword is used.
Some compilers will take this as a hint, while others will enforce the behavior. There may be other pragmas available within a given toolset for performing this action in a forceable manner, and documentation should be revisited accordingly. One of the costs associated with function inlining is naturally growth in code size, as, rather than invoke the function via a call site at run-time, the compiler inserts a copy of the function body directly where the call site originally was.
Additionally, there may be performance impacts due to challenges in register allocation across procedure boundaries or increase in register pressure within the calling function. It is advised to closely consider the impact of inlining for your target when using C++.
Constructors, destructors and data type conversions. If a developer does not provide constructors and destructors for a given C++ class, the compiler will automatically provision for them. It is true that these default constructors and destructors may not ever be required; moreover the developer may have explicitly omitted them, as they were not required.
Dead-code elimination optimizations will likely remove these unused constructors and destructors, but care should be taken to ensure this is in fact the case. One should also take caution when doing various copy operations and conversion operations: for example, passing a parameter to a member function by value, in which a copy of the value must be created and passed using the stack. Such scenarios may inadvertently lead to invocation of constructors for the value being copied, which subsequently cannot be removed by dead-code elimination further on in the compilation process.
Use of C++ templates. The use of templates within C++ code for embedded systems should come with no overhead, as in principle all of the work is done ahead of time by the build tools in instantiating the right templates based on source code requirements. The parameterized templates themselves are converted into non-parameterized code by the time it is consumed by the assembler. In practice, however, there have been cases of compilers that behave in an overly conservative (or aggressive, depending on your view point) manner, and instantiate more template permutations than were required by the program. Ideally dead-code elimination would prune these out, but that has been shown to not always be the case on some earlier C++ compilers.
Multiple inheritance. With C++’s class inheritance, users can addand expand functionality of classes for a specific use case. Forexample, perhaps there is a packetized channel communications managerthat a user has an implementation of, which can be expanded to UDPmanagement versus TCP/IP management, both of which are derived byinheriting from multiple base classes. In order to accommodate this,compilers use what is called a virtual table to basically perform thebook-keeping of functions in the inheritance table. Multiple inheritanceexpands single inheritance to allow a given class to inherit frommultiple other classes. While this seems good on paper, as theinheritance hierarchy grows in complexity so does the size of thevirtual tables required to manage these resources. Use caution whenconsidering multiple inheritance, or consider using composition designmethods instead. The code snippets below detail how to achieve thefunctional benefits of multiple inheritance, while instead usingcomposition design to avoid expansive growth in virtual tables.
Shown below is an example of multiple inheritance with virtual tables:
#include ,stdio.h. class base_class_00
class final_class_minheret : public base_class_00,
// Compiler will need virtual tables to manage inherited classes
// and book keeping, this will increase size!!!
Theexample above illustrates the typical C++ use case of building a classusing multiple inheritance, which will also incur overhead due to thecode size requirements for maintaining the virtual tables. Rather thanincur this penalty, the developer may want to build similarfunctionality via the composition design model whereby, rather thaninheriting multiple classes, a class simply contains pointers toinstances of the formerly inherited class.
Below is an example of composition without virtual tables:
#include ,stdio.h. class base_class_00
_a 5 new base_class_00();
_b 5 new base_class_01();
delete _a; delete _b;
Encapsulation of architecture-specific data types .For certain embedded platforms, there may be non-standard data typessupported by the architecture. Consider a signal-processing architecturethat has a 24-bit data type with native support for 24-bit arithmeticin the processor. These types of arithmetic are often supported in C vianative intrinsics in the source code, as can be seen below:
void compute( fract24 sample_a, fract24 sample_b )
/* some computation */
product = fract24_mpy(sample_a,sample_b);
Thereason for supporting such computation in C is that the data types forsuch instructions typically do not map well to the standard “char,short, long” data types that are native within the language. Inaddition, the instructions themselves may be highly application domainspecific, such as multiply and accumulate with saturating arithmetic.
Assuch, typical C compiler implementations will rarely identify suchpatterns at compile time and need assistance in ensuring that the mostcomputationally efficient instruction is selected at computation time.The developer effectively aids in this process by using the intrinsic,in this case fract24_mpy(), that matches the instruction of choice onthe target processor.
C++ developers may be tempted to createcustom C++ classes for handling of such non- standard data typesassociated with these systems. For example, the fract24 data type couldbe abstracted into its own C++ class, with related public memberfunctions that support the arithmetic operations such as addition,multiplication, etc.
While this would allow for more portablecode, in that the functions can be ported to different architectures, itoften comes with additional overhead. This is evident in the fact thatwhile the C implementation will keep the fract24 data type as a numberpresumably in a register, the C++ implementation will require an objectto be created and stored in memory for each fract24 instance.
Thiswill require additional memory usage, constructor/destructors, as wellas having an impact on performance depending on how objects are handledby lower-level ABI functionality.
Typically costly features of C++ for embedded
Therest of this article details portions of the C++ language thatunfortunately have significant impact on program run-time behavior.Designers should seriously consider whether these features are allowedwithin their embedded project or coding style documents, perhapsallowing their usage on an as-needed case-by-case basis.
Run-time type identification (RTTI). This language feature is of value with relation to pointers to objectsin memory, and determining dynamically at run-time what those objecttypes are. As such, it requires maintaining a book-keeping hierarchy inplace at run-time to differentiate type information, somewhat analogousto the virtual table use case mentioned earlier.
Thus, thedisadvantages become two-fold: (1) the overhead required in order tomaintain the RTTI information dynamically at run-time in memory, and (2)the run-time computational overhead incurred in determining saidinformation and updating the table.
Exception handling .Exception handling provides a rather elegant means of handling abnormalrun-time behavior in a C++ application, but comes with significantoverhead typically not conducive to embedded systems. For starters,exception handling requires the maintenance of the RTTI tables describedpreviously. In addition, the ability to throw/catch exceptions requiressignificant run-time resources to perform the requisite book-keepingpertaining to call stacks, etc.
Objects need to be destructed,and open scopes must be analyzed to determine whether or not they canhandle the type of exception currently being processed. There is alsoadditional code size required for tracking this information. As such,while an elegant design feature within the language, it is recommendedthat designers avoid using this feature in their systems.
C++affords a number of features that lend themselves to the development ofembedded systems, more specifically application and systems-levelsoftware for a given product or platform. Managers and developers alikeshould take pause when introducing C++ to their development environment,taking care to observe both performance of code across tool releases,quality of code generated by software developers and engineers, as wellas maintainability and scalability of code bases within and acrossplatforms. With proper planning and diligence,
C++ provides anattractive offering for portability and performance in the embeddedenvironment beyond that of the C language, while not necessarilysuffering the overhead of various alternatives in the emulated orinterpreted spaces.
Mark Kraeling is Product Manager atGE Transportation in Melbourne, Florida, where he is involved withadvanced product development in real-time controls, wireless, andcommunications. He’s developed embedded software for the automotive andtransportation industries since the early 1990s. Mark has a BSEE fromRose-Hulman, an MBA from Johns Hopkins, and an MSE from Arizona State.
Used with permission from Morgan Kaufmann, a division of Elsevier, Copyright 2012, this article was excerpted from Software engineering for embedded systems , by Robert Oshana and Mark Kraeling.