Advertisement

Moving to higher ground

October 01, 2003

Dan_Saks-October 01, 2003

There are a whole lot of pluses to C++. If you're already using C, the leap up is not as large as you might think.

I've been writing my "Programming Pointers" column in Embedded Systems Programming for a little more than five years now. The column's purpose has been to explain those aspects of C and C++ that programmers find most confusing and to present programming techniques and stylistic suggestions to help programmers use these languages more effectively.

I've focused on C and C++ because they're by far the most widely used languages for embedded systems, at least by those who read this magazine and attend the Embedded Systems Conferences. And, it just so happens that C and C++ are the languages I know best, which works out well all around.

For the most part, I've stuck to topics of interest to both C and C++ programmers. Although I've written about some features that C++ doesn't share with C, such as reference types and function overloading, I've tried to stay close to the interests of C programmers as well. Consequently, I've shied away from the more powerful parts of C++, notably classes, virtual functions, and templates.

It was never my intent to suggest that the high-end features of C++ are unimportant. Quite the contrary—I find it hard to imagine myself doing any serious programming without them. In fact, I can think of only a couple legitimate reasons for using C instead of C++. First, you might not be able to find an adequate C++ compiler for your target platform. Second, you might need to use the new support for numerical computing in C99 that's not available in C++; but even there, I'll bet you'd do better to write the nonnumerical parts in C++ rather than the entire application in C.

I'm not saying anything about the relative merits of C++ compared to Ada, Java, or any other language that I might slight by failing to name it. That's a completely separate discussion. I'm just saying that I can see little reason to use a C compiler if you can use a C++ compiler just as well.

C++ provides nearly everything that C does, plus a whole bunch of other good stuff. Since a C++ compiler can compile C about as well as a C compiler can, all those added goodies are essentially free.

Little to lose
In truth, not all C code will make it through a C++ compiler without complaint. C++ is not quite a superset of C—only a short list of C constructs won't compile as C++. Aside from a few features to support numerical computing, most of the differences arise where C++ rejects questionable conversions that C accepts. Fortunately, you can easily rewrite most C that isn't C++ as C that is also C++.

For example, C permits conversions between enumerations and integers in both directions. C++ permits conversions from enumerations to integers, but not vice versa. Thus, given:

enum color { red, blue, green };
typedef enum palette palette;

color c = blue;
int n;

both C and C++ accept:

n = c; // OK in C and C++

but only C accepts:

c = ++n; // OK in C but not C++

You can easily rewrite the latter with a cast so that it compiles as both C and C++:

c = (color)++n; // OK in C and C++

This statement generates the same code as it did without the cast.

In short, compiling C code with a C++ compiler costs next to nothing. It doesn't really diminish C's expressiveness. It may force you to add a cast here or there in the source, but the compiled code should be essentially the same as if you had omitted the casts and compiled it with C. In the few places where a substantive difference may exist between C and C++, you can write selected functions in C and call them from C++. You lose little while you gain easy access to the power of C++.

Much to gain
The most compelling reason to use C++ instead of C is that C++ supports classes. A class in C++ is essentially a struct with additional capabilities. Whereas the members of a C struct can be only data, the members of a class can be functions, constants, and types, as well as data. A class can also have access specifiers, which support compile-time detection to preserve the separation of a class's implementation from its interface.

In truth, you can uses structs in C to mimic classes in C++, but it's tedious and error-prone. Since classes are incredibly useful and C++ makes it so much easier to use them, why would you want to struggle with C?

A common complaint about C++ is that it's too complicated. Unfortunately, this complaint has some validity. Some parts of C++ are more complicated than they should be, and the complexity invites errors. Nonetheless, classes are so useful, I just can't see doing without.

In the coming months, I'll be showing practical examples to illustrate the benefits of using classes to implement embedded systems.

Corrections
In my last column ("Enumerations with Noncontiguous Values," August 2003, p.42), I presented a couple of functions for controlling the surface-mounted LEDs on an ARM Evaluator-7T single-board computer. The turn_on function turns LEDs on by setting bits in a special register called IOPDATA. The turn_off function turns LEDs off by clearing bits in IOPDATA.

I defined masks for the bits as enumeration constants:

enum LED_name
    {
    LED_1 = 1 << 7,
    LED_2 = LED_1 >> 1,
    LED_3 = LED_2 >> 1,
    LED_4 = LED_3 >> 1
    LED_mask =
        LED_1|LED_2|LED_3|LED_4,
    LED_all = LED_mask
    };
typedef enum LED_name LED_name;

I then presented definitions for turn_on and turn_off as inline functions:

inline void turn_on(word n)
    {
    IOPDATA |= (n & LED_mask);
    }

inline void turn_off(word n)
    {
    IOPDATA &= (~n & LED_mask);
    }

and as macros:

#define turn_on(n) \
    (IOPDATA |= (n & LED_mask))

#define turn_off(n) \
    (IOPDATA &= (~n & LED_mask))

In both the functions and macros, I used LED_mask to ensure that the functions alter only those bits in IOPDATA that control LEDs. Unfortunately, as a couple of alert readers observed, turn_off (as either a function or macro) is incorrect. The ( and ~ in the function body are in the wrong order. Rather than:

    IOPDATA &= (~n & LED_mask);

the function body should be:

    IOPDATA &= ~(n & LED_mask);

Similarly, the replacement string in macro definition should be:

    (IOPDATA &= ~(n & LED_mask))

Both macros should also have a set of parentheses immediately enclosing the parameter n in the replacement string. That is, the macros should be:

#define turn_on(n) \
    (IOPDATA |= ((n) & LED_mask))

#define turn_off(n) \
    (IOPDATA &= ~((n) & LED_mask))

Without those added parentheses, a call such as:

turn_off(LED_1 | LED_3);

expands as:

(IOPDATA &= ~(LED_1 | LED_3 & LED_mask))

which groups as:

(IOPDATA &= ~(LED_1 | (LED_3 & LED_mask)))

In this particular case, the regrouping doesn't affect the result, but with other expressions it might. It's good practice to enclose each macro parameter in parentheses when it appears as a subexpression in the macro definition's replacement string.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. You can write to him at dsaks@wittenberg.edu.

Loading comments...