Search This Blog

Tech Book Face Off: Effective C++ Vs. More Effective C++

I spend most of my work time programming in C++. I learned the language nearly twenty years ago. I've even read a couple of books about it - a beginner text from way back and Bjarne Stroustrap's classic text on C++ (recommended, by the way, but it's a pretty dense read). But I've never read a book on how to write effectively in C++ using the accepted idioms and methods in common use. I picked up this pair of books to finally change that, and I really shouldn't have waited this long to do it.

Oddly enough, Effective C++ and More Effective C++ were the oldest and newest books on the list of C++ books I'd like to read, and More Effective C++ is the older one. That's because Effective C++ is on its third edition, while More Effective C++ is still on its original version. We'll see how that affects the comparison of the two books.

The books are both organized as a set of "ways to improve your programs and designs," 55 ways in Effective C++ and 35 ways in More Effective C++. That's 90 ways in total, which is more ways than I can possibly cover adequately in this review. I'll do my best to provide some interesting detail while keeping things focused.

Effective C++ front coverVS.More Effective C++ front cover

Effective C++, Third Edition

Scott Meyers does an impeccable job of describing the ins and outs of building better C++ programs in a clear and engaging way. I ate up every one of the 55 items he covers in Effective C++ as if it was C++ programming manna. The book is packed full of valuable information that every C++ programmer should know. Meyers goes into minute detail on many of the tricky and subtle design decisions and implementation methods that programmers will run into. Being aware of, or better yet, being well-versed in these idioms and practices will help you come up with much better solutions to nearly every kind of programming problem.

Such valuable information does come at a cost, though. The learning curve is quite steep. Things start off fairly easy with some basic C++ best practices and guidelines for constructors, destructors, and assignment operators. Then the difficulty starts ramping up with resource management and implementations.

By the time Meyers is discussing inheritance and object-oriented design practices, the details of the design considerations are getting pretty hairy. Then the chapter on template programming takes another big step up in difficulty. Luckily, as the difficulty goes up, the likelihood that you're ever going to use the concept goes down because the concepts get further away from the common solutions to everyday problems. Of course, that means you'll have to look it up when you need it, because you're probably not going to remember the details.

One thing that struck me as I got deeper into this book is how ugly C++ really is. It is a truly flexible language that allows you to have control of almost every aspect of the code that makes up your programs, but that means that if you want it to perform correctly, you have to maintain control of every aspect of the code that makes up your program. You have to constantly think about a staggering number of details and diligently write code for the specific conditions that apply to each parameter, variable, function, and class.

For example, Item 31 of the book goes into detail on the pimpl idiom, which means "pointer to implementation," and how it improves compilation by reducing dependencies. This idiom is referred to in numerous other places in the book, so it leaves the impression that it's an important practice, especially when creating large systems with long compile times. What the idiom does is manually emulate something most other popular languages, Java and C# included, do by default, and that is to handle all objects as references.

The pimpl idiom is implemented by creating a class with the full interface that is required, and then include one pointer (preferably a smart pointer) to the implementation class as a data member. The implementation class contains the actual data members that are needed for the class, and a complete duplication of the interface. The interface class methods then simply exist to call the corresponding implementation class methods through the smart pointer. I am not kidding.

If you don't use this idiom in C++, you'll have a mess of dependencies that generally results in having to compile the world every time you make a change anywhere. That is generally okay for small systems, but it's a big problem for large systems. To do the right thing and correctly decouple interfaces and implementations, you have to write a crap-load of extra boilerplate code. Like I said, this is ugly, but that's C++ for you.

Templates are another example of C++ complexity, except they take it to a whole new level. Templates exist partially to emulate the relaxed type system of dynamic languages, but the code is compiled instead of interpreted. With templates, you don't have to rewrite the same logic over and over again for different data types, but the amount of complexity involved in coding this way is astounding. The issues that you must keep in mind are fascinating, but in the end you have to ask yourself, what problem am I really trying to solve here? Taken to the extreme, all of the template programming items would result in a massive code base that implements features of dynamic languages in a hideous way. It reminds me of Greenspun's tenth rule:
Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp.
If you need the features of a dynamic language, why not use a dynamic language? You can call tightly written C/C++ programs for those parts of the program that need to be really fast.

Taken as a whole, this book has some definite issues with YAGNI. If you do most if this stuff, most of the time, you're probably not being very efficient. These practices should be used when they're needed, not before. Otherwise, how could you ever make any real progress? At the same time, you need to be more than aware of these idioms to use them when needed; you need to be well-practiced in them.

Regardless of the YAGNI issues, this book is a gold mine of good C++ practices that will help you write correct, robust programs, and Meyers does an excellent job covering all of the intricate details in a clear, logical way. I highly recommend reading it; just don't try using all of the advice all of the time or you'll never make progress with your programs again.

More Effective C++

More Effective C++ has a very similar style and structure to that of Effective C++, as they are written by the same author. What surprised me about this book was the amount of overlap that it had with Effective C++. About half of the 35 items were redundant with material in the other book. It seems that during the two revisions of the first book, much of the best material of the second book was included in it, making about half of the second book irrelevant.

Of the remaining items, about half of them were outdated, covering topics that have been superseded by new language features, additions to the standard library, or better programming practices. The book was written at a time when C++ was changing significantly, and most compiler vendors were not yet supporting all of the features of the language, so Meyers covered a fair number of workarounds that are no longer necessary. Interestingly, I first learned C++ a couple years after this book was published. At the time, I was an ignorant high school student. I had no idea how much C++ was in flux at the time, and I missed out on all the fun of worrying about compiler incompatibilities and unimplemented features.

There were a few gems to be found, like this one when Meyers was talking about the 80/20 rule of performance optimization:
The way most people locate bottlenecks is to guess. Using experience, intuition, tarot cards and Ouiji boards, rumors or worse, developer after developer solemnly proclaims that a program's efficiency problems can be traced to network delays, improperly tuned memory allocators, compilers that don't optimize aggressively enough, or some bonehead manager's refusal to permit assembly language for crucial inner loops. Such assessments are generally delivered with a condescending sneer, and usually both the sneerers and their prognostications are flat-out wrong.
I couldn't agree more. A lot of programmers tend to substitute urban legends and vague incantations for precise measurement when doing optimizations, and they would be much more effective at targeting code that needs improvement if they would just pull out the stop watch and profiler to benchmark the program.

The set of items on code efficiency were generally a good review of optimizing algorithms in different contexts. Meyers didn't shy away from giving contradicting advice because the best optimization depends on the behavior of the inputs and the expectation of the outputs for any given piece of code. The same optimization does not apply everywhere, so what is exactly the right approach in one case may by exactly the wrong approach in another. Good programmers understand when to use different approaches and know a wide variety of them.

One topic that I could not agree with was Item 32: Program in the future tense. Most of the explanation for this item flies in the face of YAGNI. Here's a representative quote:
To program in the future tense is to accept that things will change and to be prepared for it. It is to recognize that new functions will be added to libraries, that new overloadings will occur, and to watch for the potentially ambiguous function calls that might result. It is to acknowledge that new classes will be added to hierarchies, that present-day derived classes may be tomorrow's base classes, and to prepare for that possibility. It is to accept that new applications will be written, that functions will be called in new contexts, and to write those functions so they continue to perform correctly.
If you spend the time that you would need to predict all of those things about the functions and classes that you write, you are wasting a ridiculous amount of time designing and implementing stuff that may never be used. Software is flexible and malleable. That's why it's called software. Sure it would be nice to design for every contingency, but at what cost? If your design is clear and your code is clean, you should be able to change it and add to it the things that are necessary when they become necessary.

If you are trying to save programmers from themselves by making your code future-proof, you are on a fool's errand. For one, this is an insurmountable task, and for two, programmers are very adept at shooting themselves in the foot and passing on the blame. Your attempts to make the design work for their future needs will likely be futile. They need to stumble and learn; that can't be prevented.

I think there's some amount of hindsight bias underlying this advice. Every programmer has been in situations where they're trying to add features or optimize code, and they get buried in unwanted complexity. The natural response is to think that if only the original programmer had designed the system some other way, making this change would be so easy, but now I have to make all of these other changes, tear up this code, and rewrite this algorithm; and holy cow, this is so much work!

Think about that. The basic thought process is, if only the original programmer could have predicted what you need to do now, you wouldn't have to do all of this work. Your job would be easy because it would have been prepared in advance. I would posit that designing and implementing software for every possible future feature and performance requirement is infinitely more work. That's what it would take to never have to rewrite sections of a program or dismantle and reassemble an architecture to add new features. Let's face it, unforeseen requirements are going to happen, and a simple, clean code base is much easier to adapt in those situations than the monstrous edifice of code that would result from overzealous contingency planning.

This assessment may not be completely fair to Meyers because he probably doesn't mean for his advice to be taken to such an extreme, but I can easily see people interpreting it that way. The broad application of all of the items in these books would certainly lead in that direction, and there are plenty of programmers that would naively go down that path. Later in the topic, Meyers dialed things back a bit to something I could definitely agree with:
Design your code so that when changes are necessary, the impact is localized. Encapsulate as much as you can; make implementation details private.
That is sound advice, and much more focused and realistic than advocating the design of future-proof software. Really, most of the advice in More Effective C++ is sound and logical, if kept in context and used when necessary. The main problem with this book today is that it's mostly outdated and redundant with Effective C++, so it isn't terribly useful anymore.

The Bottom Line

From my disagreement with Meyers' advice on programming in the future tense, you may think that Effective C++ and More Effective C++ were overflowing with unnecessary complexity, but that is definitely not the case. I very much enjoyed reading them, and the advice on proper C++ coding style and the correct use of idioms was almost all excellent, well presented, and worthwhile. The trick is to know when and in which contexts it is most appropriate to use each piece of advice, and that knowledge will only come with time, experience, and experimentation.

Meyers does a remarkable job of exposing and examining C++, with all of its faults, traps, and complexity. He shows the way to tame the beast, and every C++ programmer should know the idioms and practices that he covers in Effective C++. Since More Effective C++ hasn't been updated and covers a lot of the same ground as Effective C++, it's probably not worth a read. But definitely give Effective C++ a look. It will open your eyes to a much bigger C++ world, and it will dramatically improve your understanding of good C++ programming.