Why C++ Sucks

C++ sucks because it is a mis-designed pile of crap.

All you have to do is read Bjarne Stroustrop's book on the design and evolution of C++ to understand why: C++ is the iMac of computing languages.

It was designed to have those features necessary to achieve popular success--not those features necessary to be a good programming language.

In the case of something like the iMac, much of the "prettiness" does not necessarily come at the expense of functionality--but this is not always the case. Reducing the iMac's expandability makes it simpler to use--but also potentially consigns it to a gutter market a few years down the road.

The Big Mistake: C Compatibility

In Stroustrop's mind, making C++ compatible with C was instrumental, crucial to its success.

I don't disagree. Plenty of other good object oriented languages are out there, and they've never found much success. Certainly the overhead of learning a brand new language is undoubtedly a significant barrier to acceptance.

I don't think it's any coincidence that Java chose to use C syntax for its core constructs, either.

But C++ went far further than Java in compatibility. C code works nearly unchanged in a C++ compiler. The model of how a C++ program can be separately compiled (i.e. split into multiple separate files) is identical to that of C.

But this introduces hosts of other problems, some which C++ addresses, some of which it addresses with problematic design, and some of which it simply falls down on.

This is not to laud Java--I do not have a particularly high opinions of that language. It does many good things, but owing to its history it also has some odd design elements. (Foremost amongst them being reliance on interpreters during its initial introduction.)

Keep It Simple, Stupid

C++'s biggest problem is its size. People will tell you that this is no big deal; just subset C++ down to whatever part of it you're willing to use; that can be a C subset or something a little larger, or the whole thing.

This logic can be used to justify an infinitely complex language, so one should hesitate to accept it at face value.

The biggest flaw with the argument is that it forces compiler writers to implement a larger language; correctness will be harder to get right, and optimization will suffer. In the case of C++, however, this no longer matters; there are essentially no "just C" compilers anymore to compare to, so any lowered performance of C++ is no doubt also seen by C.

Another significant problem is that it requires an investment of effort to select an appropriate subset. Moreover, you will have difficulty finding books that teach this subset; and, indeed, if you acquire a C++ algorithms book, the odds that the subset chosen for that book matches that of yours is low.

In other words, subsetting is the same as fragmenting the language into lots of separate dialects; it has all the same problems as that, with the added cost of none of those dialects having unique names. What does it mean to say "I've used C++ for six years" on a resume?

Similarly, learning a subset does you no good if you work with other people's code, and they do not use the subset you are expecting; you must be able to understand their subset, and even write it.

Finally, learning a subset doesn't guarantee that you won't avoid being bit by something that's not in the subset you've chosen. For example, you might choose (wisely, IMHO) to avoid using function overloading. You can't tell the compiler this, however, and thus you might unintentionally name two functions the same thing, and cause unimaginable problems thereby. Sure, you'll eventually figure it out, it'll just be another dumb bug, but why use a language that has any number of such gotchas lurking around every corner?

Suppose you choose to just use the C subset of C++. One of the changes C++ makes to C rules is that you can no longer automatically cast from (void *) to other pointer types. The reason for this is clear; in C++, (void *) types are used sufficiently often that hidden bugs might occur. (There is a counter-argument to this even in a C++ context: C++ encourages you to typecast more often than necessary, possibly masking bugs because your typecast hides what would be a real warning.)

Is this a real problem for C? No, it's not, it just means you need to do some extra casting. The following code doesn't work:

    x = malloc(sizeof(x[0]) * num_elements);

Instead you must code it as

    x = (mytype *) malloc(sizeof(x[0]) * num_elements);

Of course, if you are familiar with the idiom found in the first example, you see the flaw in the second; the first version avoids a bug by not explicitly naming x's type; thus if that type changes, the code still (most likely) does the right thing. With the second code, if you change x's type, C++ will complain about the type error, so it won't introduce an error--just unnecessary typing to fix it.

If you're using C++ proper, you would just use the new operator to sidestep this... ignoring the fact that new requires the name of the type...

Too Much Typing

I guess I'm just a whiner if many of my problems boil down to C++ requiring too much typing. Typing is such a tiny fraction of programming that it's not that big a deal. Yet it grates to see such extra 'accidental' (in the sense of Fred Brooks 'No Silver Bullet', i.e. in opposition to 'essential') work required for no good reason.

Example #1

In C, I write a new function, and then add a prototype to the header file. Having to add that prototype is annoying 'accidental' effort; there is no reason this redundancy must be shoveled onto the programmer (it could easily be automatically generated, a la Java or Borland's Turbo Pascal). That something like a header file is needed for separate compilation is undeniable; but that I must maintain it by hand is largely silly.

Still, it's not that much work. Cut and paste, put an 'extern' at the front of the line and a ';' at the end of the line, and you're done:


C file:

   int myFooBar(Foo *foothing, Bar *barthing)

   {

      ...

   }



H file:

   extern int myFooBar(Foo *foothing, Bar *barthing);

Whether this worked out by chance or not, these are easy editting operations to perform without moving your hands from the keyboard; cut a single line (in 'vi', 'yy'; in a windows editor: home, shift-end, ctrl-c), switch buffers, paste, go to beginning of line and type 'extern', go to end of line and type ';'.

The effort is justified because all this information needs to be available for separate compilation.

Consider the equivalent thing for a method in C++:

CPP file:

   int Foo::myBar(Bar *barthing)

   {

      ...

   }



H file:

   class Foo

   {

      ...

      int myBar(Bar *barthing);

      ...

   };

Sure, in this example, the function declaration itself may be shorter, making C++ look better than C, but I'm comparing C++ to a similar, imaginary OO language that doesn't suck.

To make the C++ cut and paste, I don't need to add 'extern' at the front. Instead I have to reach into the middle of the declaration and delete the 'Foo::'. This is actually more work--at least for me, it takes longer, and more thinking, to do this. (You have to actually parse the declaration, which gets more complex as the return value type gets more complex.)

Example #2

Worse yet, C++ makes this necessary in circumstances that it shouldn't be.

Suppose that class Foo in the example above inherits from Baz; and Baz includes as a member in its declaration virtual int myBar(Bar *barthing);. Now, when I want to go implement Foo, I choose to override the definition of myBar found in Baz.

C++ makes me spell out in the declaration of class Foo exactly which methods I'm going to override.

Even though the whole point of virtual functions is that the dispatch occurs at run-time--no compile-time support needed.

Pointless.

Oh, and did I mention that this sort of thing leads to extra unnecessary recompilation?

Why?

I think I know why C++ does it this way. The thing is, if I subclass Foo to make, say, Biz, then if Biz doesn't define myBar for itself, it will need to store a pointer to Foo::myBar in its virtual function table. Thus, the compiler needs to know about everything that goes on under the hood with Foo to build Biz correctly. (Similarly if Biz defines it itself, but calls ::myBar.)

That means, of course, that everything 'under the hood' must be exposed in the class definition. The entire 'private' section must be exposed to subclasses (and also so that 'sizeof' works correctly).

You can try to work around the excess recompilation introduced by this by having multiple header files with differing levels of detail in them; the subclasses and the implementation of the class see the full description, whereas the rest of the world only sees the public definition, unless they need to sizeof... well, as you can imagine, I don't know anyone who actually tries to do that. (It would help if you could flag a class definition as 'incomplete' so inclusions of the wrong header file would fail to compile, instead of producing bugs.) I'm not actually sure that doing this is legal C++, anyway.

This all misses the point. Part of C++'s success is that it didn't require rewriting the linker (after all, initially it just was translated into C code). Separate compilation could be done without needing to see the innards of other classes if the virtual function tables were built up at link time. Even without rewriting the linker, the patching could be done at runtime, during startup. This does not need exposure. (The sizeof problem would still remain.)

Example #3

Yet another case is that of the C-style "static function". Suppose I decide I want to break Foo's implementation of myBar down into multiple smaller steps, using helper functions. Since the code is based around an object, I still want to make these be methods of the class so that I get a 'hidden this' and can refer to instance variables conveniently.

  /* C code: */

     static void myFooBarHelpFunction(Foo *foothing, int intermediate_value)

     {

        ...

     }



     int myFooBar(Foo *foothing, Bar *barthing)

     {

        int value = computeSomething(foo,bar);

        myFoobarHelpFunction(foo, value);

        ...

     }



  // C++ code:

     void Foo::myBarHelpFunction(int intermediate_value)

     {

        ...

     }



     int Foo::myBar(Bar *barthing)

     {

        int value = computeSomething(bar);

        myBarHelpFunction(value);

        ...

     }  

The C++ example is incomplete. As you can see, it lacks the static keyword. This is because, to implement this in C++ like this, you have to add a declaration of this function to the class definition. That's right, to do a local, hidden modularization of this function, which cannot be seen or used by anybody else, including subclasses, you have to touch the class definition, which normally (as noted above) is exposed in the header file to anyone and everyone who interacts with the class. (At least this seems to be the case. Am I missing something?)

Oh, thanks.

And don't forget to delete Foo:: when you add it to the header file.

You can work around this by privately subclassing the type, thus allowing you to create a local class MySubClass type with local, non-exposed declarations. You still end up with a declaration and a definition, as opposed to C where you only need the definition if you put the functions in the right order. And you will have to downcast pointers that are passed in. But it avoids the header dependency.

Pet Peeves

Don't get me wrong. The above three examples aren't just pet peeves. I think of them as serious design flaws. I have pet peeves about the language and the typing therein as well, but they lean more towards personal taste:

Indirection

Indirection is the source of nearly all that is good about computer programs. Pointers or handles are crucial to writing code that does more than formula processing.

A relatively crucial element of object-oriented programming is the introduction of indirect function calls. Sure, imperative programming has them as well, but most OO languages make them ubiquitous; many people consider virtual the most important keyword distinguishing C++ and C--that is, if you never use virtual, you may be using classes, but you could just as easily be writing in C.

The thing is that unlike, say, Smalltalk, not all indirection in C++ is at run-time. Stroustrop considered this an important element of C++'s success--by providing multiple mechanisms, you can select the one with the appropriate trade-off of power vs. performance overhead.

But more is not necessarily better. One can imagine a language in which a compiler makes these trade-offs automatically for you. You can imagine a language in which a single keyword changes the underlying implementation, with no syntactic or semantic variations visible.

Not so C++.

In C, a function call can only happen one way:

... foo(x,y); ...

If 'foo' is a variable that is a function pointer, this call is indirect; if not, it is direct. You generally can't tell from syntax, although many people choose to use one of two conventions to distinguish them: either a naming convention (function pointer variables include an extra word in the name), or a syntactic convention for function pointer variables (which is actually legal with function names as well, if I recall correctly):

... (*foo)(x,y); ...

(There are actually some cases where the syntax is unambigous about which, for example (foo->bar)(x) must be an indirect call--that is, any expression where the name would go.)

Assuming you use one or the other convention, then, the two modes of function call are unambiguous to distinguish. Assuming the call is direct, there is a simple mechanic for finding the callee; search back through the source, looking for a prior definition of 'foo' which is now in scope. If not found, grep the header files for exported functions. Only one function named 'foo' can be exported without introducing linker errors, so the result is unambiguous.

If a function call is indirect, the exact same search will tell you where the function variable is defined. An arbitrary effort may be necessary to be expended to determine where that call goes.

Object-oriented languages attempt to make indirection more useful by structuring it. Instead of going "just anywhere", a message send must go to one of the subclasses of a given class, and share that name.

Improving the ability of a programmer to understand indirect function calls is surely a laudable goal. Object-oriented languages are rich with designs people would be unlikely to attempt with C's unwieldy do-it-yourself function indirection methodology.

But there is much to dislike about C++'s execution.

Syntax

As noted above, there is exactly one syntax in C that leads to function calls (the variant syntax in the latter example stands for the exact same semantics); one syntax, but two semantics.

In C++ there are eight syntaces and quite a few semantics.

No joke:

  1. regular function call (expression context): foo(a,b)
  2. constructor call (declaration context): Foo foo
  3. constructor call (declaration context): Foo foo(a,b)
  4. constructor call (expression context): new Foo
  5. constructor call (expression context): new Foo(a,b)
  6. destructor call (block end): }
  7. destructor call (statement context): delete foo;
  8. overloaded operator (expression context): foo+bar
(I'll fold copy/assignment constructors in with overloaded operators.)

Even if you disagree with my splitting the constructors up that way, there'd still be six; moreover, unambiguously, there are four different contexts in which function calls occur (declaration, expression, statement, and block end).

Constructors and Destructors

Of course, if you use constructors and deconstructors in the "right" way, this isn't as bad as it sounds. Constructors and deconstructors only do "good things"; the constructors and deconstructors happen at "times" when such things are best suited to run.

But, nonetheless, this doesn't necessarily make programs easy to comprehend. An example off the top of my head: if an object in a deconstructor removes itself from a hash table, introducing a bug because the hash table shrinks itself, screwing up the currently executing hash iterator, you may spend a long time discovering what is going on.

If we accept, though, that the constructor and destructor calls are there because that leads to better, more comprehensible semantics--that any object-oriented language is going to need something like constructors and destructors--we are only left with two syntaces to discuss: plain function calls and overloaded operators.

Overloaded Operators

Many style guides strongly recommend disallowing overloaded operators. Some advocate allowing operator overloading for mathematical data structures, like bignums, complex numbers, vectors, matrices, and the like. (Care and handling of copy and assignment constructors is more complex, so I'll simply dispense with attempting to argue about them.)

The argument for avoiding overloading operators is often this simple one: it is to easy for someone reading the code to not realize that there are function calls going on. An ordinary syntax that does not normally resemble a function call is suddenly potentially a function call.

The argument for allowing it for math is simple: the expediency of the syntax overwhelms the argument against it. Nothing particularly surprising is going on under the hood, except possibly the performance overhead.

I cannot argue against this philosophy. I choose not to apply it, as the amount of actual addition or subtraction of vectors in my code is so inconsequential that the typing cost is insignificant; nor do I find the shorter, simpler syntax involving overloaded operators to cause me to introduce fewer bugs. But this is surely more a matter of taste than of logic.

Clearly, one would like operator overloading to follow the principle of least suprise. Operators which normally are side-effect free should remain side-effect free. One would hope operators which are normally commutive remain commutative, and associative associative; but this is not always the case (e.g. matrix multiplication). [Of course, it is not a violation of these rules if operators test for errors and exit, or collect statistics, or do any number of other not-side-effect-free effects. The important issue is that they be side-effect-free in terms of the actual computations of the program, as opposed to the above meta-computations.]

But in a short function, in which the types of the variables are obvious, one has trouble imagining operator overloading causing much trouble.

Idioms

The advantages of concise idiom are legion. I have an enormous number of C idioms I use without thought; idioms in the sense that if you are not familiar with them, the meaning of the code may not be immediately obvious. They are easy enough to figure out if you stop and think, but the power of the idiom comes from the lack of need to think; it is easier to understand a larger chunk of code all at once if the elements of it are idioms.

Here are two idioms I use frequently:

   // n loops from n-1 ... 0

   while (n--) {

      ...

   }



   // i = (i + 1) mod n

   if (++i == n) i = 0;

Notably, these idioms rely on preincrementing and postdecrementing, so the odds are high that a reader will have to stop and hesitate and think about the meaning of the code. (The idioms would not normally have the comments describing their meaning.)

Idioms make operator overloading doubly tempting. One aspect is that it allows the use of familiar idioms in new contexts:

   for(FooIter *i(foo); (bool) i; ++i) {

      ... *i ...

   }

(Something like that--I'm not very familiar with C++ operator overloading.)

A second aspect is that it allows the creation of new idioms. Expression syntax is much more powerful for idiomatic constructions than function call syntax. You may have seen this sort of construction in C, using a conventional return value to empower an idiom:

   x = listAdd(listAdd(listAdd(listAdd(newList(), a), b), c), d);

(Specifically, I've seen code like that used for adding elements to a window.)

The indirection and nesting there is ugly, and so you can see it as much clearer if you could use an idiom like:

   x = newList() + a + b + c + d;

I'm not suggesting that people would like this off the cuff; but they might find it tempting to allow operator overloading simply because it allows them to coin such idioms--not just to save typing, but because it becomes much more rapidly comprehenisble. (The nested listAdd()s above are also an idiom, but the difference in ease of comprehension is apparent.)

But this way lies madness!

Such idioms may be powerful, but they build on new, unrelated meanings of the underlying symbols.

It is (I imagine) exactly this reasoning that introduced the ubiquitous operator overloading found in the C++ stream library.

Ask a C programmer what this code does:

a << b << c << d << e;

She will tell you "nothing". None of the operators have side-effects. In C.

Do they have side-effects in C++?

It depends on what functions they call.

C++ programmers swiftly adjust to the use of <<. It seems natural and perfectly reasonable. But don't be fooled by it. Most style guides recommend against coining new forms of operator overloading. That supposed power of idiom is simply too fraught with peril.

Keep this in mind: the argument by analogy to C idioms is broken, because the C idiom is constructed of unambiguous items right there on the page. Comprehending an unfamiliar C idiom just requires parsing the code--an action the reader was already doing. There's no 'secrecy' at all--it just takes a little longer.

Semantics

As noted previously, there are two semantics for a plain C function call. Determining which semantic is in operation is as easy as searching back through the file for the name, and then grepping header files for the name.

Not so for C++. C++ has both run-time indirection and compile-time indirection. In fact, it has a number of flavors of the latter.

   foo(x,y);

  1. a plain C-style function call
  2. a plain C-style indirect function call
  3. a call to a non-virtual method in this class, or any parent class
  4. a call to a virtual method (again defined in any ancestor)
  5. a call to a templated function
  6. a call to a method in a templated class
  7. one of several functions of any of the above types, all with the same name, but different numbers of parameters
  8. one of several functions of any of the above types, all with the same name, the same number of parameters, but different formal parameter types

   foo->bar(x,y)

  1. a plain C-style indirect function call (e.g. bar is a public function pointer)
  2. a call to a non-virtual method in foo's class, or any parent class
  3. a call to a virtual method (again defined in any ancestor of foo)
  4. a call to a method in a templated class
  5. one of several functions of any of the above types, all with the same name, but different numbers of parameters
  6. one of several functions of any of the above types, all with the same name, the same number of parameters, but different formal parameter types

Some of the variants above may not seem like truely distinctive semantics; however, the distinction between run-time and compile-time dispatch is obvious, and the other distinctions are there to call attention to the effort required for someone to locate the implementation of the called function. Any of those cases could turn out to be true, and each is defined differently.

Templates offer the best example of my core complaint. At their heart (ignoring the committee-driven creeping featurism), templates are there to allow you to do something like define a generic hash table class, but specialize it to be implemented "directly" for some specific class, instead of having to pay indirect dispatches at runtime.

However, I've stated previously, I find this approach flawed, because it introduces an entirely new syntax and semantics. I would much prefer if you just defined the hash table as taking some abstract base class, defined your elements to be hashed as deriving from that base class, and then used a magic 'specialize' keyword to 'instantiate the template'. (Of course, personally I'd prefer a Smalltalk-like approach where you didn't need to use abstract base classes at all; the same sort of specialization is nonetheless entirely withint the realm of computability; and Java implementations may attempt to do JIT inlining to achieve the same effect, much as the academic language Self (something of a sequel to Smalltalk) did in the early 1990's.)

Moreover, those lists are far too short, as they don't call attention to the bewildering variety of problems introduced by function name overloading.

At least if all overloaded functions with the same name have different numbers of parameters, the result of the call is unambiguous. A grep for the name will turn up a number of matches, and if the line declaring the function is longer than a single line, some additional effort may need to be expended to figure out just which one. Annoying, but not impossible.

Far worse is the use of multiple names with the same number of parameters. You have to figure out the (compile-time) type of every parameter, exactly, before you can make the right call about which function is called. Go look up through the code to determine the type of any variable used; check in the header file to see what type is returned by this function; try to remember whether * means a dot product or a cross-product of two vectors.

Ok. Now you've got the types.

Go read the definition for how the "best" match for an overloaded function is resolved. I'll still be here. Go ahead.

Set intersection. I don't know about you, but I don't normally do much set intersection when I write function calls.

Ok, let's be fair. You can state it unambigously in English without reference to set intersection: the 'winning' function must have all its parameters "type match" at least as well as all the other candidates, and one of its parameters must "type match" better. (Set aside the rules for "type matching", and the inclusion of user-defined type conversions in them. This rant is already way too long.)

It's easy, in fact, to see how the specified rules underscore human intuition about best match. At least, each rule in isolation does so. I have my doubts about the combination.

Still, I find it a bit uncomfortable. I worry about the compiler's intuition not matching mine. I'd be more comfortable if the compiler only picked out a particular function for me if it was unambiguous; say, because every parameter was a better match for the "winner".

Problem is, that would preclude having, say, all the matching functions sharing, say, a common first element that is the same type. Such functions would always match equally. It's easy to see why C++ uses the rule it does.

The above considerations were based on a programmer who was trying to intentionally leverage function name overloading. What about one who isn't?

Suppose in C I define a function "foobar" in one module, and a define another one with the same name in another module, but with different argument types. In draconian fashion, C will produce a linker error, and force me to rename one or the other.

Is this so bad?

Consider the alternative found in C++: these two functions may be totally unrelated, but through a commonness of the English language (e.g. the same word having two different meanings; consider simply the word 'heap' in the sense of a semi-ordered data structure versus a pool of memory) share an identical name. In C++, name-mangling means those two functions can happily live within the same namespace, and within the same project.

Is this a problem?

What happens if I'm calling foobar() somewhere in my code, and then someone introduces a new #include in my code which now brings the other foobar() into scope? What if I was relying on some automatic type conversions in my call to foobar(), and the new foobar() now matches "better"?

And think about this: is it good that the different functions could come via different semantic mechanisms? So if I grep for "foobar", thinking it is coming from one sort of place, I may miss that a "better match" is being introduced through a different compile-time indirection?

And think about this: is it good that I can add "default arguments" to functions declarations, thus messing up my attempt to cull out possible function calls based on the argument counts not matching?

What a freaking pile.


[ HOME ] [ UP ] [ MAIL ]