Why I Like C++

This document is an attempt to explain why I like C++ and use it as my main programming language. Most of the comparisons are to C, Java and Ocaml (a dialect of ML), as those are the languages I'd most consider using instead.

It is not a proof that C++ is the "best language" (it is not). First, for most projects, the choice of language is made for nontechnical reasons, and this document contains only technical reasons. Second, language preference is a personal choice.

I recommend experimenting on your own to decide the merits of each language rather than accepting anyone's opinions blindly.

(This document is still a work in progress, with unwritten parts marked "[TODO]".)

Contents

1. Expressive power

The primary reason why I like C++ is that it puts a wide range of powerful abstraction mechanisms and techniques into the hands of the programmer. As each situation or design consideration arises, C++ presents me with many options so I am not forced to treat every situation as a nail for hitting with one hammer. Since I see programming as an essentially creative task, this flexibility is creative freedom, and programming without it feels like censorship. I choose not only what to say, but how to say it.

1.1 Macros

The C preprocessor has many flaws, and I will talk more about them in a later section. However, here I want to stress the benefits of having a preprocessor such as the C preprocessor ("cpp").

First, it's standard: the simple act of using it does not jeopardize portability, or force some would-be user to go find another tool.

Second, it is well-integrated into the lexical structure of the language. By this I mean:

Macros are not appropriate for all purposes, but what follows is a list of situations where they work well.

1.1.1 Assert

In C, "assert" is provided by a library instead of the language because cpp is powerful enough to let it be so. In particular, cpp can stringize the assert condition and insert file/line info into the message. Neither of these are possible with functions. Since "assert" is not built into the language, users are free to implement similar variants. For example, in my own code I have three custom variants, used in slightly different contexts.

1.1.2 Wrappers, name substitution

For various reasons (debugging, performance testing, system isolation) it is useful to systematically replace all calls to some function within a program with calls to some other function. Macros make this particularly easy, so these system-level diagnostic techniques are very easy to apply in C and C++. It's usually not a good idea to leave such things in the code long-term, but as a quick hack to test something it's great.

1.1.3 Debugging code, special compile modes

Conditional compilation (discussed below) has a key drawback in that it is syntactically awkward, especially when the conditionality desired is for a fine-grained element like an expression or statement. But with macros, I can (for example) create a macro 'D' like this:

  #ifdef DEBUG
    #define D(stmt) stmt
  #else
    #define D(stmt) /*nothing*/
  #endif
and then I can simply wrap a debugging statement in a call to this new "D" macro. This is impossible with a function: the argument is always evaluated.

Of course, this technique need not be limited to debug modes; in certain high-performance algorithm kernels (I'm thinking specifically of the Elkhound parser generator) there may be many algorithm parameters to tune, and having explicit conditionals scattered throughout the code is very awkward and cluttered. Simply wrapping relevant statements in macros that identify the mode they are used in makes the code far more readable.

1.1.4 New syntactic forms

I find the macro facility very useful for assisting with commonly-used looping constructs, especially when those constructs make use of an iterator. Iterator names are usually long (e.g. ObjListIter<T>), and as C++ does not have type inference (a feature I think does more harm than good; but that is a separate argument), ordinarily one must type out the iterator name to declare an instance. However, with a macro, one can collapse the redundant information.

For example, I can use a macro like

  #define FOREACH_OBJLIST(Type, list, iterName)
    for(ObjListIter<Type> iterName(list); !iterName.isDone(); iterName.adv())
to write loops like this
  FOREACH_OBJLIST(string, the_list, iter) {
    ...
  }
The macro here is expanding to a partial "for" statement, but the user provides the loop body (and the braces). Such constructs (I have several) significantly ease the writing of such loops, and facilitate changing the kind of data structure when necessary.

(I acknowledge that a language with closures can abstract the looping behavior as a function, obviating the need for this form. Several years ago I played around with a poor-man's closure mechanism for C++, but in the end decided that I preferred to see the loop structure explicitly in most cases. In any case, it's an advantage over Java if not ML.)

1.1.4 COMMON_STUFF_HERE (in unusual syntactic contexts)

Occasionally there arises some unusual situation where I would like to abstract some structure, but the language does not provide an existing facility to do so. Examples include:

In each case C++ does not have a built-in way to adequately abstract the given design dimension:

I like the fact that I can abstract these ad-hoc dimensions using macros, without having to wait for the language designers to implement a new abstraction mechanism just for that dimension.

1.2 Conditional compilation

Conditional compilation is extremely powerful. Languages like Java and ML attempt to get its benefits by demanding that implementations recognize and optimize away constructs like if (false) { ... }. However, this only gives conditionality at the statement level, and the code inside the "if" is still required to be syntactically valid.

What the "if (false)" approach cannot do, but is the most important feature that conditional compilation provides, is the ability to control what members of data structures are included. I frequently write data structure implementations that include fields that are only used for debugging, or only used when some performance-impacting feature is enabled, and control those fields' inclusion using conditional compilation. Then, in the mainline code, fragments that refer to those "optional" fields are similarly conditional (they would not compile in the mode where the field is omitted).

Similarly, I can use conditional compilation to control the list of parameters and arguments to a function. For example, the CCured error handler function can (depending on the configuration mode) accept additional parameters specifying the file/line where the error occurred. Those parameters are useful for debugging, but significantly impact performance when enabled. We make those parameters optional using conditional compilation. Again, this is impossible with "if (false)".

In general, conditional compilation makes it possible to create a design that has many different design parameters exposed, without every such parameter imposing a performance cost on the system even when it is disabled, or being syntactically awkward like with templates. This is invaluable for performance measurement and tuning (e.g. "how much do I save if I turn this off temporarily?"), and for code reuse generally.

1.3 Classes

Classes provide C++ programs with a unit of design granularity larger than a function but smaller than a module. Simply by organizing related data and functionality together, the programs become much easier to comprehend. When programming in a language without classes, I'm constantly trying to find ways (documentation, naming conventions, etc.) to express the notion of relatedness that classes provide so elegantly.

1.4 Templates

First of all, I think templates are best used when they are small, independent entities. Large, interdependent, highly parameterized libaries like STL (now part of the standard C++ library) are very difficult to understand. Further, I think inevitably some degree of understanding of a library's implementation is necessary to effectively use it.

The main use I make of templates is to write type-safe wrappers around polymorphic functionality (classes or functions) that uses void* to express genericity. The template wrapper typically consists entirely of (very) small inline functions that just cast into and out of void*. This minimizes object code bloat while providing type safety to clients of the interface. Naturally, some care is needed when writing the wrapper itself since the casts and use of void* ensure the type checker cannot help, but this is rarely a problem in practice.

The second most common use I make is writing simple generic container classes, such as growable arrays. Here again the code is small (though it is not all inline), so object code bloat is not so bad. Also, since debugging template code can be difficult (debuggers often do a bad job on template code), these data structures are written with extensive unit tests before they are pressed into service.

I think the limited genericity suggested by the above examples is about the right amount for use with templates as they exist currently. Writing large templatized classes (etc.) is awkward ("template <...>" everywhere), somewhat error-prone (the type checker is only supposed to check methods that are invoked somewhere), difficult to debug and leads to object code bloat.

I would like to have available (in addition to templates as they exist now!) a version of generics that was more like ML or Java generics, where only a single version of the code is generated, and all manipulated data must be "boxed" (stored in heap-allocated objects). But, the technique of using void* plus a thin template wrapper is usually an adequate approximation.

1.5 Operator overloading

Operator overloading is an extremely convenient way to allow natural manipulations on user-defined datatypes. The main advantage of overloading is it allows user-defined *infix* operators. I have a lot of experience reading and writing Lisp-like sexp code where all the functions are prefix, and I still find it very difficult both to read and write. It's not a matter of getting used to it; I find prefix notation is just harder to comprehend for frequently-used operations, in part because you have to do a lot of manual parenthesis matching. Consequently, the ability to define infix operators is crucial to the creation of readable, maintainable code that does lots of manipulation of user-defined data.

The example of complex numbers is well known and compelling, so much so that this data type has been added to the latest (1999) revision of C. I'll give another nice example: bit-sets. I frequently represent sets of elements drawn from a finite universe using the well-known encoding of one bit per element. An enum is ideal for this purpose:

  enum Set {
    first      = 0x01,
    second     = 0x02,
    third      = 0x04,
    ...
  };
The problem with this is that the following code
  Set s1, s2, s3;
  s1 = s2 | s3;        // union of s2 and s3
does not compile in C++ because the compiler thinks that I might be creating an element that is not among those listed in 'Set'.

However, operator overloading solves the problem:

  Set operator| (Set s, Set t) { return (Set)((int)s | (int)t); }
Now I can freely use bitwise operators like "|" to perform set computations. The cast is isolated in the operator definition, and the compiler will ensure that I don't accidentally mix elements from unrelated sets. Perfect!

1.6 Type casts

Of course I avoid type casts whenever possible. But, the designers of C++ (inherited from C) recognized that no decidable static type system is sufficient to prove the safety of all type-safe code. Moreover, type-safe code that violates any particular decidable type system is sufficiently common and useful that the language lets the programmer disable the type checker at key points using casts. Such casts shift the burden of soundness from the type system to the programmer, and hence must be used with substantial care; but I here argue that the benefits are sufficient to justify making the mechanism available. (But unlike in C, you can write substantial programs with no casts at all, and in practice need far fewer even for aggresive memory usage schemes.)

One very common example from my code is casting away constness. In some cases, it's a matter of implementing something like polymorphism for const: I implement one version that (say) accepts and returns a const pointer, and then use a cast in an inline wrapper to effectively give me a version that accepts and returns a non-const pointer. This way I avoid duplicating the code. In other cases I may want to use a piece of functionality, that in some specific case I know will not modify the object, but in general can modify said object, so I use a cast and document (via comments) my justification. A strictly const-correct language would require me to manually specialize the functionality for the const-only case.

(Yes, I think const polymorphism would be nice. But disciplined use of casts lets me do without while I wait for the language to acquire that feature.)

Another example that isn't very common but is still essential to getting good performance out of certain algorithms is the use of custom allocators. Naturally, custom allocators must take a block of memory and cast it to the type consistent with its use while "allocated" (in the sense of the custom allocator). While this example is well-known, I mention it to stress how important it is for performance: any algorithm that allocates in its inner loop will be slow, and a custom allocator interface that permits aggressive and locality-preserving reuse is a way to remove inner loop allocation.

Finally, I point out that even Ocaml (a dialect of ML, a language often touted for its type safety) has Obj.magic, which can be used for type casts. This is necessary (among other reasons) because an LR parser generator cannot be implemented without it (or its moral equivalent, a disjoint union of all semantic value types, with lots of match-or-abort constructs): the parse stack is a heterogeneous array of semantic values, but the parsing algorithm has the property that all of the action function calls are nevertheless well-typed.

1.7 Destructors

[TODO] (Wes's thesis provides some great evidence here.)

2. Flexibility

Besides expressive power within the language, C++ implementations (ultimately due to features of the language) are very flexible in their interaction with the programmer and the toolchain. These dimensions of flexibility are rarely (if ever) discussed by language theoreticians, but are very important in practice.

2.1 Physical code organization

If you're a Java programmer, you put one class in each file, including all of its methods. If you're an ML programmer, typically one piece of functionality (say, pretty printing) is a file, even though that functionality may cut across an existing type hierarchy. So, Java and ML programmers "slice" their programs differently: in Java organization is dominated by type, in ML it is dominated by behavior.

C++ allows both styles to be freely mixed. You can write all the methods of a given class in one file, or put all the 'print' methods from a family of related classes in one file. Especially in a program like a compiler, where there are many classes but comparatively few functions across them, the latter organization is much easier to comprehend because all the related functionality is adjacent.

Furthermore, the use of function prototypes and forward declarations permits the programmer to put pieces of the program in almost any order. Again, I find that it is very important to comprehensibility to have the program in an order that keeps related things together. It's like writing a textbook: you want to maximize the locality of related concepts, while keeping the number of forward references down. By constrast, Ocaml's insistence that functions be defined before they are called confines the order of the program (of course, you can make put all the functions in a "let rec", but that has its own problems).

Finally, I like the separation of header files and code files. Header files are the interface, and code files the implementation. Javadoc extracts an interface automatically, but I find that having separate HTML documentation creates an extra barrier to both reading and updating that documentation. Ocaml has interface files, but they are not as good because all of the type declarations have to be repeated in the implementation file (rather than textually shared).

2.2 Class vs. function

As another example of stylistic mixing, C++ is equally happy with classes and with stand-alone functions. Stand-alone functions are often a much better choice than having to pick some class with which to associate a given function. By contrast, if you are forced to make everything a function (C, ML) or everything a class (Java), then it can be hard to emphasize when some relationships are more important than others.

2.3 Build process

Due in large part to the simple way C++ programs export declarations, namely via the (admittedly rather primitive) #include mechanism, the C++ build process is very flexible. Below I illustrate with a few examples.

The file dependency graph is easy to extract (just follow the #includes), so a tool like make can easily recompile only what is necessary. By contrast, the Java and ML dependency graph requires at very least parsing all of the code, because explicit references like System.out.println do not require any import statement or equivalent.

Since C++ declaration exporting is purely lexical, done by cpp, the input to the compiler for a single translation unit can be captured in a single file (namely, the preprocessor output). This can be used to help track down build problems (e.g. compiler bugs, or mysterious error messages), makes inserting additional preprocessing steps easier, and generally elucidates the underlying processes (which inevitably show through in all language environments).

3. Interoperability

Because it imposes few constraints on its runtime environment, C++ (like C) is capable of operating in a wide range of circumstances. Again, I think the practical benefits of such interoperation are ignored in typical language-theoretic discussions.

3.1 Seamlessly integrate with C

It's as easy to write a program entirely in C++ as it is to write in a mixture of C and C++. The data representations, calling convention, debugger support, etc., are all the same. Libraries written in C, and APIs with C bindings, are ubiquitous due to the popularity and flexibility of C itself. C++ rides C's coattails to get all these benefits, with no additional wrappers needed.

3.2 Tool support

In part because C is so well supported, there are many tools available to help with C++ development.

3.2.1 Debuggers

Many source-level debuggers exist for C and C++. Many advanced features exist in the C debuggers:

(The Ocaml debugger has a nice "time travel" feature, but is otherwise rather spartan.)

3.2.2 Profilers

Profiling is very important for performance improvement. In particular, the gprof tool correlates profiling information with the call graph, enabling comprehensive analysis of performance problems.

While Ocaml does have the option to use gprof on Linux and x86, in typical ML programs many closures are used, and gprof sees the closure invocations as indirect calls via (e.g.) caml_apply2. Consequently, gprof thinks that most of the functions in the program are in a giant mutually-recursive ball, even when this is far from the truth. gprof could probably be modified to work better with Ocaml, but so far has not been.

3.2.3 binutils, etc.

Finally, the intermediate files produced during the build process, such as object files, libraries, and shared libraries can be manipulated with existing packages such as the GNU binutils suite. This transparency of the intermediate formats sometimes offers a valuable way to analyze build problems, and the extensibility of the .o format is a boon.

3.3 Shared library support

Almost all C++ compilers support creation and use of shared libraries. This is an important detail if one is trying to build an entire system, not just a single application.

3.4 Embedded systems (including OS kernels)

Among the benefits of imposing few runtime system requirements is the ability for the language to be used in unusual environments, including embedded environments and operating system kernels. Because of the C++ philosophy of avoiding hidden and/or distributed costs, C++ can follow C into any niche.

(One could argue that C++ constructors, operator overloading and virtual method dispatch have hidden costs. But those costs are much easier to understand, quantify and control than, say, garbage collection, closure allocation and write barriers.)

4. Performance

Now, I bet you're expecting me to start ranting about this or that benchmark. Actually, I'm not that interested in benchmarks, because they are fixed programs; what is important is being able to evolve a program so it is faster. The strategy I use is to initially write programs as simply and understandably as possible, then optimize it for speed as necessary. I think C++ is the best language for this strategy, because it provides the needed high-level features to write the initial program, as I've argued above, and the low-level features to get the speed, explained below.

4.1 Explicit memory management

Yes, I think explicit memory management is an advantage of the language. The problem with garbage collection is its cost is difficult to control. Now, explicitly managing memory is an additional design challenge, but I claim the abstraction mechanisms in the language combined with good design (a bit out of the scope of this document) are able to manage the challenge.

There are at least two important reasons why explicit management is important for performance. Primarily, reusing memory quickly is crucial to ensure high locality of access. If memory is reclaimed slowly, then the program exhibits poor access locality and dies in the memory hierarchy. Secondly, when memory is not reclaimed quickly, other programs in the system suffer because of the larger memory footprint. This is an important aspect of end-to-end system performance that is virtually ignored by benchmarks.

4.2 Object embedding

[TODO]

4.3 Function pointers

[TODO]

5. Static typing

C++ is statically typed. True, the language offers mechanisms for circumventing the type system, but by default programs will not compile if they contain type errors. In this respect I argue it is better than dynamically-typed languages such as Lisp or Perl.

Static typing catches bugs. Many common errors, such as mispelling the name of an identifier, transposing the order of arguments and confusing one container for another, are usually caught by the type checker. In a dynamically-typed language, such errors must be caught by testing, an expensive and unreliable process.

I want to point out that parametric polymorphism (available using templates in C++) is crucial to effective static typing. Container classes are the most common example of a situation where parametric polymorphism is helpful, for example:

  template <class KEY, class DATA>     // type parameters
  class Map {
    // ...
  public:
    void add(KEY k, DATA d);
    DATA get(KEY k);
  };
  
  void foo()
  {
    Map<int, float> m1;
    Map<Foo*, Bar*> m2;
    
    m1.add(1, 2.3);
    float x = m1.get(1);
    
    int y = m1.get(1);       // wrong DATA type; triggers a warning in gcc at least

    float z = m2.get(1);     // wrong map; triggers type error
  }

Parametric polymorphism lets one write code that works with arbitrarily typed data but also state the constraints among those types, for example that the type of the second argument to add() is the same as the return type of get().

The alternative, subtyping polymorphism (as in C or pre-1.5 Java), simply uses some catch-all supertype (e.g., void* or Object) in polymorphic interfaces. This is flexible, but because it does not allow one to constrain the relative types, permits one to write code that misuses Map, for example by trying to get an Apple out of a map containing Oranges. In Java, a nominally statically-typed language, this achieved by way of a (checked) downcast. The need for a cast undermines the static type checking, so those bugs, which are quite common when there are many containers being used, aren't found until run-time.

At the opposite extreme from subtyping polymorphism is a language like Pascal that is statically typed but does not offer anything like a cast. As Brian Kernighan explains in Why Pascal is Not My Favorite Programming Language, this leads to even more serious problems with maintainability, because the choice is either to duplicate the code or avoid precise types.

6. Response to key criticisms

[TODO]

7. Critique of alternatives

[TODO]

8. Things about C++ I'd like to change

C++ is far from the perfect programming language. Some of its defects could be corrected fairly easily. Others would require breaking some backward compatibility. The most ambitious features I'd like are potential research topics. In this section I mention some of the things I'd like to change in C++.

(This section is [TODO].)

8.1 Standard C++ ABI

8.2 New metaprogramming interface

8.2.1 New syntax

8.2.2 More compile-time checking

8.2.3 Implement much of current standard as a metaprogram

8.3 Fix templates

For now, this section isn't so much about how to fix templates, as a rant about problems caused by their misuse.

8.3.1 Problems with templates as containers and algorithms

Generally, I think templates work okay when used to implement containers and algorithms. However, there are also many problems, especially when the stack of layers gets deep:

8.3.2 Problems with templates for metaprogramming

As is relatively well known, templates are a Turing-complete metaprogramming language: you can implement arbitrary functionality at compile time. However, unlike (say) Lisp macros, templates are a very poor metaprogramming language.

8.4 Remove pointer to member

[TODO]

Valid HTML 4.01!