This article appeared in Dr. Dobb's Journal, Fall 1997 Careers issue.

C++ in the Real World: Advice from the Trenches

by Nathan C. Myers

C++ has taken a lot of criticism: it's a big language that takes a long time to learn; standardization has taken a long time, which has made it hard to write portable code; newer languages, notably Java, draw more media attention. Still, among languages that support an object-oriented style, C++ is by far the most heavily used, and its usage is still growing rapidly. Why?

Some of the complexity of C++ is inherited from C, or results from its evolutionary history, but most is a consequence of the language's power. For an easy problem, any language will do; a hard problem demands a powerful language. Each feature of C++ exists because it has proven important for some area of industrial programming. With the language standard nearly complete, compilers that implement most of the new standard features are available now on most architectures.

Real-world programmers are more interested in problems than in languages: a programming language is a way to solve a problem. When you use the right mix of languages and language features, the solution to a problem is much easier to describe and implement, with better results. C++ remains an essential tool for software engineers not because anybody thinks it's the best possible language, but because it's a single, portable language that works better than any alternative in each of several areas. This article explores the strengths of C++, and how to exploit them in your projects.

Why Use C++?

C++ is a general purpose programming language designed to make programming more enjoyable for the serious programmer. [Stroustrup, 1985] For many uses, C++ is not the ideal language. You might prefer Tcl/Tk for writing a user interface, SQL for relational database queries, Java for network programming, or Yacc for writing a parser. C++ is used because it works well when the ideal language is (for whatever reason) not available, and because it interfaces easily with the libraries and the other languages you use.

It's no accident that you can interface C++ with almost any language interpreter or library you find. You rarely find a big program written all in one language, or without using libraries, so easy integration with other languages and libraries was a key design goal.

Most problems have no specialized language to solve them; some because none has (yet) been worth creating, and others because an interpreter would add too much overhead. When you can't afford a specialized language for part of a problem, a library may suffice. C++ was designed with libraries always in mind, and its most useful features are those that help you write portable, efficient, easy-to-use libraries.

C++ as Glue

A solution for any big problem depends on solutions to a variety of smaller problems. For some of those smaller problems you may find a specialized language or library ideally suited for the job. For the rest, you must write your own code, and then interface it with the other parts.

For example, in a business application you might use SQL to talk to a database server, Postscript to talk to a printer, and Tcl/Tk for its user interface; and link with libraries for fixed-precision arithmetic and encryption. In a multi-player game you might use Java applets in the client machines, VRML3D to describe a virtual world, and Scheme for describing non-player characters; and link with libraries for networking and multithreading. The Microsoft NT kernel network driver contains a small Prolog interpreter to help puzzle out network interface cards.

C++ is, by design, well suited to tying together the various parts of a programming project. It has features specifically for calling libraries written in other languages, and designers of other languages are careful to make them easy to connect to C++. It also includes features to help organize big programs, and to keep libraries from interfering with one another or with your main-line code. Most importantly, it imposes no upper limit on the size of problem it can address; as your program evolves and grows, you won't discover one day that the language just isn't up to the job any more.

The most familiar feature in C++ for interfacing with other languages is its C subset. Anything you can do with C you can do with C++; hence, any interface designed for C is usable from C++. Similarly, the asm(...) construct gives you access to lower-level code and libraries that don't present a C interface. To call a C library, or to export C interfaces, you just wrap the declarations in the extern "C" { ... } block.

Interpreters are valuable when the desired semantics are unknown until runtime, or when the compile-link cycle is just too slow. To get around performance bottlenecks, most let you patch in C++ subroutines. In fact, some interpreters are written in C++ themselves, and their "built-in" features are implemented just that way. Some are also available as a library, and let you create an interpreter object in your C++ program whenever you need one. Such interpreters are called "extension languages"; good ones include Python, Korn shell (ksh95), Tcl, and Scm (a Scheme variant).

To help improve program organization, more recent C++ compilers implement the "namespace" feature. This lets you protect your programs against the chaos of global names found in operating system interfaces, C libraries, and even other C++ libraries. Namespaces let you group all your global names (functions, classes, and templates) in a separate scope (or scopes), so you can combine libraries (your own or others') without worrying about name collisions. Use them religiously.

C++ for Performance-Critical Sections

While many applications can tolerate almost any degree of inefficiency in the implementation, some can't tolerate any. In the past, you would be forced to trade expressive clarity for performance by rewriting your performance-critical code to eliminate abstractions, perhaps in assembly language or specially-optimized Fortran. With Standard C++ and modern compilers this is no longer necessary.

Each feature of C++ was carefully designed to be usable where performance matters. More precisely, each feature is as fast as what you would write in C to achieve the same purpose. Thus, an inline function is as fast as a C macro, and a virtual function call is about as fast as using a switch statement or a table to select a function to call.

The C++ template mechanism can perform significant computation at compile time, for results that consume zero runtime [example 1] and can express control flow at compile time, to expand an algorithm in-line [example 2]. Thus, straightforward C++ code using a cleverly coded library (Blitz++) and a good (but unspecialized) optimizing compiler has lately been able to beat Fortran performance on its home ground, numeric array processing. It is this combination of efficient basic operations with a uniquely powerful template mechanism that make C++ a good choice for implementing the performance-critical parts of any application.

C++ for Writing Libraries

I've already mentioned that C++ has features ideal for writing efficient, portable, easy-to-use libraries. This is no accident. To be useful for writing really big programs, the language must make it easy to assemble big programs from not-so-big pieces which hide complexity behind simple interfaces. Those pieces are libraries.

Libraries are not created only (or even usually) for "re-use"; it is typically very difficult to take a library out of one program and make it useful in other programs, even when it was written with that in mind. Rather, the purpose of good library interfaces is to make it possible to understand a big program. A big program can be understandable only if complexity doesn't leak out of the libraries it uses.

Part of hiding complexity is hiding the use of advanced techniques from users who may not (yet) understand them. In other words, language features and techniques that seem interesting only to a few users can be used to make better libraries for everyone else. This is abundantly evident in some libraries already available, such as the STL, which hide a potentially bewildering variety of advanced techniques behind simple interfaces.

One of the best things about a language well-designed for writing libraries is that people use it to write great libraries. You can use these libraries at many levels. They package useful code, and you can just call them; but they are also examples of good interface design, and sometimes brilliant implementation. Read them as literature, to inspire your own designs, and publish your own libraries, so others can learn from them and help you improve them.

Advice

Always use the very best, most advanced compiler available. At one time, using advanced language features was asking for trouble, but no longer. Seek out a really up-to-date compiler, and accept no substitutes. (Currently, compilers based on the Edison Design Group (EDG) front end implement the advanced language features most reliably.)

Learn C++ a little at a time. Start with the basics: variables, operators, functions, flow control. (Don't think you need to learn C first; C++ offers better ways to do many things.) Learn how C++ treats memory, and how constructors and destructors work. Learn the features needed to use libraries: how to create objects, call members, instantiate templates. Learn how to write exception-safe code. (At this point, given some essential libraries, C++ is already useful.) Be sure to put all your code in namespaces, right from the beginning.

It may take years to learn how to design classes and templates well; start by copying from good examples. (If you don't believe that designing a string class is hard, try finding a good one.) Learn templates thoroughly, by studying libraries that use them well: they will provide unending rewards. The C++ Standard Library offers both good examples (iterators, algorithms, and containers) and bad (string). Avoid overloading operators, except as part of using a library (such as operator<< for ostream), until you're an expert. Avoid automatic conversions; you will usually regret them, often after it is too late to do anything about it.

Don't be too impressed with inheritance and virtual functions. They are important, but not so important as many writers imagine; C++ offers other, often better, ways to express abstraction. In particular, be deeply suspicious of deep inheritance hierarchies. The Standard Library streambufs and locale facets use inheritance reasonably. (Although the streambuf member names are unfortunate, the interface is worth learning.) Inheritance doesn't need to be exposed in the library interface to be useful; in the Standard Library, streambufs are hidden behind streams, and locale facets are hidden behind locales.

C++ has been called an object-oriented language, and an "impure" one; avoid this labeling trap. C++ has features to support object-oriented, generic, and structured programming styles. It also has pointer arithmetic, bit-twiddling operators, and a "goto" statement. All are useful, in places. The categories mentioned above are more useful for writing about programming than for writing programs.

There is no substitute for Stroustrup's "The C++ Programming Language", 3rd Edition. You may begin with an introductory text, but graduate to Stroustrup. Use a good style guide, such as "Industrial Strength C++" by Henricson & Nyquist, and collect books of tips, such as "Effective C++" by Scott Meyers. Learn the vocabulary of patterns; most people start with "Design Patterns" by Gamma, et al. Remember that becoming a good designer takes years, no matter what language(s) you use to express your ideas.

By far the most important skill to develop is writing correct code. Testing always misses errors, and debugging is often impossible (such as in multi-threaded or real-time code), so there is no substitute for "getting it right". The language provides many tools to help prevent errors; use them all. Get used to proving that each statement and block does what is intended. Adopt a "contract" style of programming: for each interface, list the assumptions and guarantees you cannot enforce in code. A good programmer may be a thousand times more productive than a mediocre one; this skill accounts for much of the difference.

During the first two years that you use C++ you will often find better ways of doing things that allow you to delete hundreds of lines of code; welcome them. In general, never be reluctant to throw away or replace code; throwing away code is the (paradoxical) key to re-use. What is valuable in a library is not implementation code, but insight. A library becomes useful by responding to the experience of its users; code that implements the wrong features is worse than useless.

Don't try to use one language for everything; know several different kinds of languages, be willing to learn more, and be able to combine them to exploit their strengths. (But avoid proprietary, non-portable languages; they disappear quickly, taking your code with them.) The tools you have at hand often help to determine the projects you find yourself involved in; the best projects are reserved to those who have the sharpest tools.

Conclusion

C++ is one language among many, but its unusual combination of strengths make it an essential tool for the serious engineer. Although you may take years to fully understand some of its features, you can benefit from its power immediately.

 

More C++ articles...

 

Biography

Nathan has been relearning C++ since 1986. He designed the locale portion of the Standard C++ Library, and claims responsibility for the "explicit" keyword. Reach him via <http://www.cantrip.org/>.


Example 1: Compile-time Square Root Computation: ceil(sqrt(N))


// <root.h>:

template <int Size, int Low = 1, int High = Size> 
  struct Root;
 
template <int Size, int Mid>                     
  struct Root<Size,Mid,Mid> { 
      static const int root = Mid; 
  };
 
template <int Size, int Low, int High>          
  struct Root {
      static const int mean = (Low + High)/2;
      static const bool down = (mean * mean >= Size);
      static const int root = Root<Size, 
             (down ? Low : mean+1), 
             (down ? mean : High) >::root;     
  };                                                                  
 
// User code:
//   compute sqrt(N), use it for static table size                       

int table[Root<N>::root];                                        


Example 2: Inline Vector Dot Product Expansion


// Given a forward declaration:
template <int Dim, class T>
  struct dot_class;                                                 

// a specialized base case for recursion:          
template <class T>
  struct dot_class<1,T> {                        
    static inline T dot(const T* a, const T* b)
      { return *a * *b; }                        
  };                                           

// the recursive template:                       
template <int Dim, class T>
  struct dot_class {                           
    static inline T dot(const T* a, const T* b)
      { return dot_class<Dim-1,T>::dot(a+1,b+1) + 
               *a * *b; }
  };                                           

// ... and some syntactic sugar:                 
template <int Dim, class T>
  inline T dot(const T* a, const T* b) 
    { return dot_class<Dim,T>::dot(a, b); }

// Then                                      
int product = dot<3>(a, b);
 
// results in the same (near-)optimal code as
int product = a[0]*b[0] + a[1]*b[1] + a[2]*b[2];


Sources of excellent free library code include:

The Silicon Graphics STL
The Blitz++ Numerics/Array Library


Some sources of EDG-based compilers include:

 

For more information, see the Cantrip Corpus.
Email to ncm-nospam@cantrip.org. © Copyright 1997 by Nathan C. Myers. All Rights Reserved. URL: <http://www.cantrip.org/realworld.html>