My last complaint was a bit trivial, I admit. So this time let me tackle what is perhaps the worst design decision in the C++ language: all members must be pre-declared. To understand why this is so awful you have to understand something about software engineering. Not just programming like you might do as an assignment or class project, but the assembly and coordination of very large software products, the kind that span continents and decades.
Creating anything complex requires breaking it down into less complex functional units and putting them together in a coherent manner. While it's true that the parts all have to fit together correctly, the amount of information --- the level of detail -- required to mate the parts together is much less than that needed to build each of the parts. When assembling we only care about the ways the parts touch each other -- their interfaces -- and not so much about anything that happens in between those interfaces.
As a simple example, suppose you're making something that uses AC power. There's a massive infrastructure behind your wall outlet: generators, transformers, transmission lines and their control electronics, all the way to the breaker box outside your home. None of that matters. All you need to know is the if you make something with metal prongs of the right form factor you can tap into AC current of a known frequency and voltage. Following a one-page specification your appliance can get power in any home in America.
Software design follows the same principle. Every component in a large software system is defined by two things: interface and implementation. The implementation is how the component works, and it can be very complex indeed, but as long as it works correctly the only people who have to worry about it are the engineers who built it and fix it when problems are found. They're like the technicians who keep the power grid humming; unless we're having brownouts no one ever needs to think about them.
The interface, on the other hand, is everyone's concern. It's like the specification for how the software component is used -- its control system. And like all control systems (or computer user interfaces) simpler is better. Cars used to have chokes and required double-clutching. If cars were software we would have said that the wires of the implementation were poking through into the interface. Today cars have fuel injection and automatic transmissions, so while the implementation has gotten more complex the interface has become simpler.
Moving complexity from the interface to the implementation is exactly the kind of progress we want to see. If drivers don't have to spend time or mental effort on using their clutch correctly then they can spend that on more productive matters, and we're all better off as result.
Apparently the designers of C++ either didn't know any of this, or they didn't care. C++ makes it impossible -- literally impossible -- to separate the interface from the implementation.
The unit of functionality in C++ software is the class, which is defined as a collection of data and operations on that data. For example:
class CBox {
int x, y, w, h;};
void Add (class CBox &);
My hypothetical Box class (prefixed with C by C++ convention) is given by a 2-D position and size (called member variables) and an "Add" operation (called a member function or more formally a method) which allows this box to be added to another box. This is necessarily simple as an example, but it can stand in for something much more complex. The member variables and methods could be multiplied dozens of times, and many of the methods could be common internal operations that the class would use in its implementation but not intended for outside usage. But suppose this Box class represents something like that; it would be totally unsuitable as a component of a large software project.
Why? Because the implementation -- the x, y, w, h variables (and perhaps internal methods) -- are exposed in the interface. That means that the implementation cannot change. The wires of the implementation are poking through the interface, and if a better way to represent a box is discovered later this class cannot use it. It is forever committed to a specific implementation.
There is a "solution" for this in C++. It's called public and private.
class CBox {
public:
void Set (int x, int y, int w, int h);
void Get (int &x, int &y, int &w, int &h);
void Add (class CBox &);private:};
int x, y, w, h;
Just so we're all totally clear here, what we've done is labeled part of the interface as public -- the part we can use -- and the rest as private -- out of bounds. You can still see the implementation. It's right there! But the C++ compiler will raise an error if you attempt to use it. So problem solved, right? The implementation can change and other code will still work correctly because it's forced to limit itself to the public part of the interface.
It's temping to think so, and obviously the designers of C++ thought so too. But it's wrong. The problem comes from the fact that in large-scale software systems the different parts are not always developed at the same time, or even by the same companies. So class A could be implemented and deployed, and then class B could be created that uses class A, and then some time later class A could be fixed or improved. Class B should also be improved, shouldn't it? If it was all written in C++, the answer is a huge no. The only way that B can benefit from the improvements in A is if the developer recompiles the class with the new interface.
Imagine, by analogy, that every time your electrical grid did a major upgrade to its internal systems, all of your household outlets slightly changed. You'd have to buy new appliances that fit the new outlets, or appliance manufacturers would have to figure out how to adapt to new outlets, adding to the cost and complexity of their products.
Why did they do this? Once again it was laziness. Put simply, they wanted to make it easier to write a C++ compiler. If you know the size of a class and you know the layout of the vTable, then it's very easy to handle inheritence. It's not that they didn't know how to do it right -- they did. Despite my imprecations otherwise these were very smart people. They just made the decision that getting lots of C++ compilers on the market sooner, by making the language easier to implement, was more important than making a language that supported large-scale development efforts.
And we've all suffered for it ever since. Thanks guys, way to go.
- jack*
UPDATE: part 3
Comments