Embedded software applications are most commonly written in C. For many years, C++ has been seen as the natural successor and has found greater acceptance, but the increase in its usage has been much slower than anticipated.
There are a number of reasons for this. Firstly, embedded developers are quite conservative and prefer to use solutions that are proven rather than novel " "if it ain't broke, don't fix it".
There is also the lesson of experience. Many developers have attempted to use C++ for embedded applications and failed. Such failures may sometimes be attributed to shortcomings in development tools, but most often it is the inappropriate use of the language " treating an embedded system like a desktop computer " that is to blame.
In this two part tutorial, an attempt is made to address some of these issues, to give guidelines to the effective use of C++ for embedded applications and to show that the language has some true benefits to the embedded developer.
One of the key reasons why C++ has gained popularity in the computing world in general is its history. Although it is a modern language, with object oriented capabilities, it has backward compatibility that makes its adoption " learning and application " straightforward.
Figure 1 below gives some insight into the evolution of the language. Later in this series we will look at C/C++ compatibility in more detail.
![]() |
| Figure 1. The Genealogy of C++ |
Limitations of C
Although C is widely used, it has limitations, as it was not designed
for embedded applications or for projects of a scale that is now
commonplace. Key limitations include:
1) C is extremely powerful and flexible and can therefore be dangerous.(It has low level capabilities - which are useful for embedded " but also represent many pitfalls for the unwary.)
2) Programmers need to be very methodical and disciplined
3) Programmers need to understand how the program behaves at low and high levels (large projects are thus hard to maintain)
4) Programmers need an expert knowledge of the application
However, C++ has powerful object oriented capabilities which can help significantly with addressing the limitations of C:
1) it encapsulates and hides areas of high expertise from non-experts into "objects;" (A test case will demonstrate the encapsulation of expertise later in Part 2 in this series).
2) Objects can be used intuitively by non-experts to implement conceptual designs at a high-level
Language Overview
Like ANSI C, C++ features many enhancements to the original C language
in addition to its object oriented capabilities. C++ is not simply a
super-set of C, as the two languages evolved in parallel.
However, the language may be learned and applied incrementally, as will be illustrated later in this paper. For the moment, here is a summary of some of the useful language features:
1) Dynamic memory allocation operators. The operators new and delete are alternatives to the library functions malloc() and free() and lead to more readable and less error-prone code.
2) Function prototypes. Iintroduced in C++ and adopted into ANSI C, their use is mandatory.
3) Function parameter default values. A function may have default values for trailing parameters to enhance code readability.
4) Reference parameters. Function parameters may be passed by reference instead of by value (copy). This gives the efficiency of using pointers without the great potential for errors that results from their use. Here is a simple example function:
void swap(int& a,
int& b)
{
int temp = a;
4) Inline functions. The code for a (small) function may be declared as being inline " i.e. a copy of the actual code is included at each call site. This improves the speed of execution, but may increase code size. This may be achieved using the inline keyword or by including the code in the body of a class definition. This is only advice to a compiler, which will probably take account of current optimization settings.
5) Function overloading. Multiple functions may be defined with the same name. The compiler would distinguish between them by their unique number/type of parameters. This results in more readable code, with less contrived function naming.
6) Typesafe linkage. In C++, all function names are "mangled" " their names are modified to reflect parameter and returned data types. This enables the linker to perform additional cross-module checking without being required to "know" about C++. This is also the mechanism whereby function overloading is implemented.
Object Oriented Features
C++ is often described as an object oriented language, but this is not
strictly true. It is really a procedural language (like C) with some
object oriented capabilities.
The key language feature is the concept of a class. A class is very similar to a structure in C, but has some important differences and enhancements:
1) A class is defined using the keyword class.
2) A class may contain code as well as data (not just pointers to functions)
3) Class members may be declared as private or public, enabling key functionality to be hidden (encapsulated)
4) A class is, in effect, a new data type with the name of the declared class; the data and possible operations on that data are defined
5) Operators may be defined (overloaded) to manipulate the data in a class, which leads to readable, intuitive code
6) Member functions may be included that are automatically called when an object is created and destroyed (constructors and destructors)
7) One class may be derived from another, inheriting all of its characteristics, which may be augmented or replaced
Templates
A much-vaunted feature of C++ is templates. This concept may be applied
to function or class definitions. The idea is simple: the programmer
can define the complete structure and logic of a class/function, but
leave the specification of data types (for parameters and internal
variables) open.
This acts as a "recipe" for the compiler to generate code/data when the programmer instantiates a template (and indicates which data types are required).
Here is a simple example of a function template:
template
{
X temp;
temp = x;
x = y;
y = temp;
}
This might be utilized like this:
int i, j;
float a, b;
...
swap(a, b); // instantiation #1
wap(i, j); // instantiation #2
Templates provide all programmers " desktop and embedded alike " with a very useful capability to create highly reusable code. But embedded developers always want to know the "cost" " what is the overhead incurred by the use of templates?
In theory, no overhead would be anticipated, as a template does not itself generate code " code is only created by the compiler when required. But this is the cause of a problem: compilers only process one module at a time, so the template instantiations are on a per-module basis.
The result of this is the same code " i.e. a template instantiated with an identical set of data types " might be generated in numerous modules of the application. This may increase the memory footprint very significantly. This may be of minimal concern to the desktop programmer, but is likely to be of great importance to the embedded system developer.
A possible solution to this problem " apart from simply understanding what is happening " is to have tools which are optimized for embedded work. One approach is to have a linker which can take "hints" from the compiler and eliminate redundant (copies of) code.
Exception Handling
In almost any kind of software there exists the possibility for error
conditions to occur, which may be detected by the software. Often, this
requires some kind of "emergency exit" to some code to deal with the
error gracefully.
Unfortunately, structured programming languages (like C, C++ etc.) do not conveniently handle such a possibility. The C++ Exception Handling System (EHS) was created to address this problem.
The EHS is manifest in three additional C++ keywords: try, throw and catch. A try block is used to control when exception handling is active. A throw statement is used to instigate the processing of an error (exception). And a catch block " of which there may be many, each identified by an object type " contains the code to process the error.
Here is a simplistic example which shows how the EHS syntax works:
void scopy(char* str)
{
if (sizeof(store)+1 < strlen(str))
throw -1;
strcpy(store, str);
}
void get_string()
{
char buff[100];
cin >> buff;
scopy(buff);
}
main()
{
try
{
...
get_string();
...
}
}
catch (int err)
{
cout << "String too long!";
}
Although the EHS simplifies the coding required to deal with emergency situations, it does come with a cost: extra code is generated to enable throw statements to work correctly. An embedded developer is always wary of extra overheads, so the size of this extra code should be monitored.
There are two additional points about the EHS that embedded developers need to appreciate:
First, since the compiler has no way to know whether a particular piece of code will be called from within a try block, the extra code will be generated for all the application program modules. What is worse is that some compilers have EHS enabled by default and it is the programmer's responsibility to turn it off. So, if the EHS is not being used, it is essential to check the options on the compiler to ensure that no overhead is being incurred.
Second, the EHS is designed to facilitate a selection of different responses to a wide variety of error conditions. This makes complete sense for desktop applications. However, many embedded applications have quite simple error handling needs.
Often, a system reset is performed if a "difficult" error is detected, as this is the best way to get back to a stable operating state. It may quite reasonably be concluded that the EHS would be overkill under these circumstances. However, there are two ways to use it with lower overhead, which may tip the balance in its favor:
* If no matching catch statement is found when a throw is effected, a library function terminate() is called. This function could be replaced by a routine to perform a system reset.
* Just a generic catch handler may be provided, using this notation:
catch (...)
This will catch exceptions of any type and results in a measurably lower code overhead.
Language Extensions
One of the big advantages of using a standardized language, like C++,
is that the code and the programmer's expertise are very portable. On
this basis, it would seem foolish to add extensions to a standardized
language, as this would lead to non-portability.
In an ideal world this would, indeed, be true. However, C++ (like C) was designed for desktop computing application, not embedded. Embedded developers are numerous, but still represent a small minority of C++ users. As a result, a small number of language extensions may be considered useful and acceptable:
1) Many compilers, designed for embedded use, include a feature to enable data to be packed, making best use of memory by eliminating alignment bytes in arrays and structures.
For this facility to be really effective, the programmer needs the facility to turn it off for specific data structures, to increase speed of access or provide format compatibility with some other system or software. This override is usually implemented by the unpacked extension keyword. A corresponding packed keyword may be available to permit individual objects to be packed when the global option is switched off.
2) Traditionally, interrupt service routines (ISRs) were coded in assembly language for two reasons: execution speed efficiency and context saving. Modern compilers generate code which is certainly fast enough for an ISR, but a regular function does not facilitate the full context save and alternate return mechanism that is required. An additional keyword, interrupt, is commonly provided to declare a function as being an ISR.
3) Sometimes the use of assembly language is unavoidable. This is usually because a CPU feature is inaccessible from C++ " enable/disable interrupts is a common example.
Although there is always the option of calling a function written in assembly language, the overhead may be reduced by being able to insert the necessary lines of assembler code in line. Commonly the extension keyword asm is available to facilitate the declaration of assembly language pseudo-functions. (#pragma asm may also be a possibility.)
4) To embedded software developers using C, the keyword volatile is essential, as it is common to have variables that are shared between threads or represent I/O devices. This keyword was not always implemented in early C++ compilers, but is obviously still required.
A Strategy for C to C++ Migration
The big advantage of the backwards compatibility between C++ and C is
that migration can be done gradually. A possible strategy is to apply a
three stage process:
1) Exploit reusability " write all new code in C++
2) Treat C as C++ " clean up existing C code to comply with C++ requirements
3) Start using C++ language features more widely
At some later point, the object oriented capabilities of C++ can be exploited more fully.
Applying Reusability. A potential incompatibility between C and C++ is apparent at link time. A C++ compiler, as mentioned previously, will mangle function names to incorporate information about the number and type of parameters and return data type. This facilitates function overloading and results in typesafe linkage. A C compiler will not do this, so linking C and C++ modules would result in errors.
Fortunately, in C++ a mechanism is provided to work around this problem: the linkage of a function may be declared as being compatible with C using the extern "C" construct, thus:
#ifdef _cplusplus
extern "C"
{
#endif
extern void alpha(int a);
extern int beta(char *p, int b);
#ifdef _cplusplus
}
#endif
Cleaning up C code for C++
The next step in migration to C++ is to review and modify existing C
code so that it is truly C++ compatible and can be processed by a C++
compiler without any problems. The resulting code may be called Clean
C. Here are the key issues to consider:
1) Function prototypes " These are mandatory in C++.
2) Type casts " Implicit casting is much stricter in C++; use explicit cast operators like (char).
3) Enumerated types " These are not taken very "seriously" by C compilers, being considered a means of making typed, symbolic constants; in C++ enum declarations result in true data types.
4) String array size " In C and C++ a char array is commonly used to contain a NULL terminated character string. This requires the array to have sufficient capacity for the characters and the terminator.
In C, it is not permitted to initialize an array with a string larger than the declared array size, but the NULL terminator may be discarded. This leniency is not extended in C++. A simple strategy is to leave the array size unspecified and allow the compiler to allocate the space, thus:
char str[] = "xyz";
5) Nested structures " In C, if a struct is defined within another struct, the inner struct may also be used declared by itself elsewhere in the module. In C++, the scope of the inner struct is strictly confined to the outer one. Hence, the following code would not be Clean C:
struct out
{
struct in { int i; } m;
int j;
};
struct in inner;
struct out outer;
6) Multiple declarations " In C, it is quite legal to declare a variable in a module, outside of a function and then declare it again later in the module. This only causes problems if the redeclaration is inconsistent. In C++, multiple declarations are illegal.
7) Additional keywords " C++ introduces a number of additional keywords on top of those already reserved in C. This may be a problem if there is a clash with identifiers in the C code. The best strategy is to introduce a naming convention and apply it to the code. For example, all functions, types and variables have an upper case initial letter and all constants are fully capitalized.
C+: A better C?
The last step in the migration process, prior to considering object
oriented techniques, is to incrementally apply C++ language features.
This results in a hybrid language " a "better C" " that we might call
C+. Possibilities include, but are not confined to, the following C++
features:
1) New comment notation " C++ introduces a new, end of line comment notation using the // operator. This is neater and addresses issues with nesting when using the traditional " /* ... */ " notation (which is still available, of course).
2) Reference parameters " In C++ by default, parameters are mostly passed by value (copy) the same as in C. However, there is also the option to pass by reference to avoid complications of explicit pointer usage.
3) Variable declaration placement " In C, automatic variables must be declared at the head of a block (commonly they are just placed at the head of a function). This limitation is relaxed in C++, enabling variables to be declared close to their point of first use. For example:
for (int x=0; x<3; x++)
...
4) Constructors in structures " In C++, a class definition may include constructor and destructor member functions, which are automatically called when an object comes into and goes out of scope, respectively. Since a struct is, in most respects, identical to a class, it too may include a constructor. This is useful to facilitate the automatic initialization of the struct variable.
5) Memory leaks " By use of paired constructors and destructors, memory leaks may be avoided by ensuring that allocated resources (like dynamic memory) are deallocated when no longer required.
6) Exception handling " The EHS, introduced earlier in this paper, is not strictly an object oriented language feature and, hence, its use may logically be considered when using C+.
Next in Part 2: "Encpsulation of expertise using C++ objects."
Colin Walls has over twenty-five years experience in the
electronics industry, largely involved with embedded software. A
frequent presenter at conferences and seminars including the Embedded
Systems Conference he is a member of the marketing team of the Embedded
Systems Division of Mentor Graphics.
Click here to see Part 2.