Topic : Why C++ Sucks
Author : buzzard
Page : << Previous 2  Next >>
Go to page :


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:


Having to prefix every method definition with Foo:: is stupid. We should be able to wrap our definitions inside something like class Foo { ... } and not have to prefix with Foo:: inside it. Of course you could do exactly this, but you can only have one definition of a class, so you can't do this and also include a header file with the class declaration in it, so you need to include the full declaration locally. (Also, providing a mechanism like this would also make cutting and pasting into the header file easier.)
constructors and deconstructors return void, even thought C++ denies it. Yes, removing the need for void from the parameter list and as a prefix on constructors/deconstructors can be seen as an opportune 'fix' to reduce typing, since C++ doesn't back-support K&R C the way ANSI/ISO C does. Sorry, I don't buy it; the amount of typing saved is irrelevent. (It's one thing to save lots of cut&paste editting motion; saving typing four characters while one is already in the middle of typing characters is saving me something like 5 seconds a week.) It introduces a pointless inconsistency.
etc. (Why waste time on pet peeves when there's more juicy bits?)
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:

regular function call (expression context): foo(a,b)
constructor call (declaration context): Foo foo
constructor call (declaration context): Foo foo(a,b)
constructor call (expression context): new Foo
constructor call (expression context): new Foo(a,b)
destructor call (block end): }
destructor call (statement context): delete foo;
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

Page : << Previous 2  Next >>