Effective C++: 55 Specific Ways to Improve Your Programs and Designs by Scott Meyers

Effective C++: 55 Specific Ways to Improve Your Programs and Designs by Scott Meyers is a must-read for C++ programmers looking to get more serious in their C++ skills. It includes very useful tips that I may have taken much longer to figure out on my own, supercharging my previous understanding of the language. What’s more, this book teaches not just technical details specific to C++, but also useful guidance for programming and design in general. I feel that Effective C++ helped me take my C++ and programming skills to the next level.

Purpose

I read Effective C++ to take my C++ programming skills to the next level. This book has helped me move my C++ skills from school-taught, self-learned, online-course driven knowledge to scalability-focused, enterprise-driven, and ultimately professional-grade knowledge. Effective C++ has helped me think about how to marry good program design with the intricacies of C++, and I feel others can easily derive the same value from reading this book. To date, I have read this book twice, once as part of a book club at MathWorks (by the way, for those looking for employment, MathWorks has some pretty awesome perks, including free books of your choosing so long as they are directly career-related), and another read earlier this month.

There are a total of 55 specific, well-curated items that Scott Meyers presents in a very digestible manner. For this most recent reading, I knew that I was more interested in some items than others, and so I chose for this book review to summarize the book in two ways:

  1. Super high-level view of all presented items (highlighting the items I choose to describe in more detail), to give readers an overall sense of the text
  2. Major takeaways from a smaller subset of these items, subjectively chosen for my own interests, possibly helpful to a subset of readers.

Summary

“Major takeaway” items are highlighted in-line, below. Considering the high number of items that I consider important, not all highlighted items have further explanation yet. If any readers are super interested in my notes for a highlighted item that has not yet been elaborated, feel free to leave a comment!

Chapter 1: Accustoming Yourself to C++

Item 1: View C++ as a federation of languages.

C++ can be described as a “multiparadigm programming language”, consisting of 4 primary sublanguages that may work in harmony:

  1. C
  2. Object-Oriented C++
  3. Template C++
  4. Standard Template Library (STL)

I have heard others describe C++ as “C with object-oriented features”. Unfortunately, this is a poor simplification of the truth. When programming in C++, it pays to be wary of which sublanguage you are working in, because the rules may differ from one sublanguage to another.

Item 2: Prefer consts, enums and inlines to #defines.

#define preprocessor directives are commonly used to define contant values and functions; these directives work by essentially copy/pasting the #defined keyword for the #defined value. However, because #define directives are merely preprocessor directives, the compiler is not aware of these constants and functions. This can make debugging quite difficult.

const, enum, and inline are all compiler-aware C++ features that can many times replace the typical use cases fulfilled by #define.

There are a few “gotchas” you should carefully consider for this item:

  • Constant pointers: Declare both the pointer and the thing the pointer points to as const. e.g., const char * authorName = “Scott Meyers”
  • Member constants: Provide an initial value at the point of declaration, and if needed, provide an “empty definition” (if you refer to the address of the constant, or if the compiler [incorrectly] complains about lacking a definition):

// In Hacker.h:

class Hacker {
private:
static const int MaxNumProductsOwned = 100; // IMPORTANT: This is a declaration with an initial value!
std::string productNames[MaxNumProductsOwned];

};

// In Hacker.cc:

const int Hacker::MaxNumProductsOwned; // Empty definition, see above

Older compilers may complain about using the declaration with initial value / empty definition pattern, so for these cases, it may be necessary to instead provide an uninitialized declaration and then a definition with initial value:

// In Hacker.h:

class Hacker {
private:
static const int MaxIntelligenceQuotient; // Uninitialized declaration

};

// In Hacker.cc:

const int
Hacker::MaxIntelligenceQuotient = 300; // Definition with initial value

  • Know about the “enum hack.” Sometimes, compilers may (incorrectly) forbid classes from declaring a static const with an initial value (see: member constants, above), but you need to use this static const to declare another member value, like an array that depends on a constant int (see: Hacker::productNames, in member constants, above). You can use the “enum hack,” which looks like the following:

// In Hacker.h:

class Hacker {
private:
enum {MaxNumProductsOwned = 100}; // The enum hack makes MaxNumProductsOwned a symbolic name for 100
std::string productNames[MaxNumProductsOwned];

};

  • Using #define for function-like macros can yield surprising behavior. Imagine the following #define macro:

#define GET_MIN(a,b) a < b? a : b

Notice the following behavior:

int a = 5;

GET_MIN(++a, 100); // a is incremented once; becomes 6

GET_MIN(++a, 0); // a is incremented twice; becomes 8, NOT 7!

Notice that using an inline function will yield much more predictable results:

template<typename T>
inline int get_min(const T& a, const T& b) {
return (a < b? a : b);
}

If you want to know more about counter-intuitive behavior of function-like #define macros, see this handy gcc/GNU reference on the matter. Hopefully Meyers (or my explanation of his arguments) has you convinced by this point!

Perhaps to further drive the point home, Meyers also calls this item “prefer the compiler to the preprocessor.”

Item 3: Use const wherever possible.

const is a very clear indication to both compilers and fellow programmers that a particular object should not be modified. While its syntax may initially seem confusing, it is really not as bad as it might seem. In short: const is applied to whatever is to its immediate left. If there is nothing to its immediate left, it will be applied to whatever is to its right.

Meyers goes into more detail about making member functions const, including a discussion between bitwise constness (an object’s members cannot be modified, e.g. no bits should be changed) and logical constness (allows for modification of some of the bits in the object, but only in ways that clients cannot detect), and how to avoid code duplication when you need to have both const and non-const flavors of your member function. A few tidbits here:

  • When trying to ensure “logical constness”, it may be useful to make use of the mutable keyword to indicate the members that can be changed in const member functions.
  • For handing both const/non-const flavors of a member function (e.g, “foo”): Use the const_cast<T> ( static_cast<const CLASS_TYPE&>(*this).foo(…) ) idiom. Make sure that the casting is applied on the non-const, NOT the const member function.

When I was initially learning C++, I found Duramecho’s explanation on const immensely useful.

Item 4: Make sure that objects are initialized before they are used.

  • For built-in types, this usually means manually initializing the object before using it:

int x = 0;

  • For everything else, this usually means using a constructor. Prefer using a “member initialization list” over the constructor body when initializing values:

Person::Person(const std::string first, const std::string last, const int a):
firstName(first),
lastName(last),
age(a)
{}

See cppreference for a more complete specification of initializer lists.

Chapter 2: Constructors, Destructors, and Assignment Operators

Item 5: Know what functions C++ silently writes and calls.

You get a bunch of things for free when creating a class, specifically, a default constructor, a copy constructor, a move constructor (C++11), copy assignment constructor, move assignment constructor (C++11), and destructor. Read more about that here.

All of the above functions will be both public and inline. Also note that the generated destructor is non-virtual (which can be a serious problem if you plan on inheriting the class – Item 7.

If you declare a constructor in your class, compilers won’t generate a default constructor.

 

Item 6: Explicitly disallow the use of compiler-generated functions you do not want.

For many classes, certain compiler-generated functions may not be wanted. Consider the copy constructor. If you have a class meant to act as a singleton instance, it might make sense to make sure no default copy constructor is generated.

To disallow the use of such compiler-generated functions, declare the corresponding member functions private.

Also consider privately inheriting from Boost’s noncopyable class.

Item 7: Declare destructors virtual in polymorphic base classes.

First, if a class does not contain virtual functions, it almost certainly indicates that the class is not meant to be inherited. There are several arguments as to why, but the short of it is that it provides both practical and theoretical consistency (for the long of it, see Item 36).

The case of declaring base class destructors virtual is a special case – one that can lead to detrimental results if you fail to heed this advice. When a derived class object is deleted through a pointer to a base class with a non-virtual destructor, results are undefined. The derived part of the object is never destroyed, and you may be left with a “partially destroyed” object, leading to memory leaks.

Item 8: Prevent exceptions from leaving destructors.

Item 9: Never call virtual functions during construction or destruction.

When called during construction or destruction, virtual functions behave in a way you may not necessarily expect. The calls will never go to a “more derived” class than that of the currently executing constructor or destructor.

Item 10: Have assignment operators return a reference to *this.

Item 11: Handle assignment to self in operator=.

Item 12: Copy all parts of an object.

Chapter 3: Resource Management

Item 13: Use objects to manage resources.

Using an object to manage resources allows the object to obtain resources on construction and release them on destruction. This offloads much of the responsibility from their callers. Also see Resource Allocation is Initialization (RAII).

Item 14: Think carefully about copying behavior in resource-managing classes.

Item 15: Provide access to raw resources in resource-managing classes.

Item 16: Use the same form in corresponding uses of new and delete.

Use new with delete, and new[] with delete[]. Don’t mix and match, okay? Okay.

Item 17: Store newed objects in smart pointers in standalone statements.

Chapter 4: Designs and Declarations

Item 18: Make interfaces easy to use correctly and hard to use incorrectly.

In C++, make interfaces hard to use incorrectly by making sure that clients will fail to compile if they use an interface incorrectly. There are many ways to make interfaces easy to use correctly, but one prominent discussion for this item is to effectively utilize types.

class Date {
Date(int Month, int Day, int Year);
};

Can be much more confusing to use compared to:

class Date {
Date(Month m, Day d, Year y);
};

Item 19: Treat class design as type design.

Item 20: Prefer pass-by-reference-to-const to pass-by-value.

Passing an argument by value means that the function will first copy the entire object by value upon entering the function’s execution. This can be quite expensive for large objects:

void someFunction(GiantMemoryHog obj);

Conversely, passing by reference does NOT copy the entire object:

void someFunction(GiantMemoryHog &obj);

However, this would still allow obj to get modified; this is problematic for clients that would hope that obj did not change state. The solution is to pass by reference to const:

void someFunction(const GiantMemoryHog &obj);

Item 21: Don’t try to return a reference when you must return an object.

Item 22: Declare data members private.

You get more abstraction/control and more encapsulation if you rely instead on method setters/getters:

class Example {
private:
int score;
std::string name;
public:
int getScore();
void setScore(int s);
std::string getScore();
};

Notice that this getter/setter pattern also allows us to allow for full read/write permissions (score) or read-only permissions (name). You could even work in write-only permissions, should you so choose.

Item 23: Prefer non-member non-friend functions to member functions.

This provides higher encapsulation; non-member non-friend functions provide clients with the promise that these functions cannot directly access the object’s private members.

It also allows you to separate convenience functions into separate headers from the class declaration, minimizing compilation dependencies.

// example.h
namespace ExampleSpace {
class Example{…};

};

// exampleConvenienceFunctions.h
namespace ExampleSpace {
void someExampleFunction(…);

};

Item 24: Declare non-member functions when type conversions should apply to all parameters.

Item 25: Consider support for a non-throwing swap.

Chapter 5: Implementations

Item 26: Postpone variable definitions as long as possible.

Item 27: Minimize casting.

Item 28: Avoid returning “handles” to object internals.

Item 29: Strive for exception-safe code.

Item 30: Understand the ins and outs of inlining.

Item 31: Minimize compilation dependencies between files.

Chapter 6: Inheritance and Object-Oriented Design

Item 32: Make sure public inheritance models “is-a”.

Item 33: Avoid hiding inherited names.

Item 34: Differentiate between inheritance of interface and inheritance of implementation.

Item 35: Consider alternatives to virtual functions.

Item 36: Never redefine an inherited non-virtual function.

Item 37: Never redefine a function’s inherited default parameter value.

Item 38: Model “has-a” or “is-implemented-in-terms-of” through composition.

Item 39: Use private inheritance judiciously.

Item 40: Use multiple inheritance judiciously.

Chapter 7: Templates and Generic Programming

Item 41: Understand implicit interfaces and compile-time polymorphism.

Item 42: Understand the two meanings of typename.

Item 43: Know how to access names in templatized base classes.

Item 44: Factor parameter-independent code out of templates.

Item 45: Use member function templates to accept “all compatible types.”

Item 46: Define non-member functions inside templates when type conversions are desired.

Item 47: Use traits classes for information about types.

Item 48: Be aware of template metaprogramming.

Chapter 8: Customizing new and delete

Item 49: Understand the behavior of the new-handler.

Item 50: Understand when it makes sense to replace new and delete.

Item 51: Adhere to convention when writing new and delete.

Item 52: Write placement delete if you write placement new.

Chapter 9: Miscellany

Item 53: Pay attention to compiler warnings.

Item 54: Familiarize yourself with the standard library, including TR1.

Item 55: Familiarize yourself with Boost.

Overall Impression

This is a pretty flippin’ awesome book. In my admittedly not-so-humble opinion, it is easily a must-read for C++ programmers looking to get serious with their C++ hacking skills. While definitely not essential knowledge to help software developers pass their technical interviews, I will say that the knowledge presented here can easily help candidates WOW their interviewers when the items are effectively demonstrated.

1 Comment »

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s