This appendix is a collection of suggestions for C++ programming. They've been collected over the course of my teaching and programming experience and
also from the insights of friends including Dan Saks (co-author with Tom Plum of C++ Programming Guidelines, Plum Hall, 1991), Scott Meyers (author of Effective C++, Addison-Wesley, 1992), and Rob Murray (author of C++ Strategies & Tactics, Addison-Wesley, 1993). Many of these tips are summarized from the pages of this book.
1. Don't automatically rewrite all your existing C code in C++ unless you need to significantly change its functionality (that is, don't fix it if it isn't broken). Recompiling in C++ is a very valuable activity because it may reveal hidden bugs. However, taking C code that works fine and rewriting it in C++ may not be the most valuable use of your time, unless the C++ version will provide a lot of opportunities for reuse as a class.
Separate the class creator from the class user (client programmer). The class user is the "customer" and doesn't need or want to know what's going on behind the scenes of the class. The class creator must be the expert in class design and w 10510d39k rite the class so it can be used by the most novice programmer possible, yet still work robustly in the application. Library use will be easy only if it's transparent.
When you create a class, make your names as clear as possible. Your goal should be to make the user's interface conceptually simple. To this end, use function overloading and default arguments to create a clear, easy-to-use interface.
Data hiding allows you (the class creator) to change as much as possible in the future without damaging client code in which the class is used. In this light, keep everything as private as possible, and make only the class interface public, always using functions rather than data. Make data public only when forced. If class users don't need to access a function, make it private. If a part of your class must be exposed to inheritors as protected, provide a function interface rather than expose the actual data. In this way, implementation changes will have minimal impact on derived classes.
Don't fall into analysis paralysis. Some things you don't learn until you start coding and get some kind of system working. C++ has built-in firewalls; let them work for you. Your mistakes in a class or set of classes won't destroy the integrity of the whole system.
Your analysis and design must produce, at minimum, the classes in your system, their public interfaces, and their relationships to other classes, especially base classes. If your method produces more than that, ask yourself if all the elements have value over the lifetime of the program. If they do not, maintaining them will cost you. Members of development teams tend not to maintain anything that does not contribute to their productivity; this is a fact of life that many design methods don't account for.
Remember the fundamental rule of software engineering: All problems can be simplified by introducing an extra level of conceptual indirection. This one idea is the basis of abstraction, the primary feature of object-oriented programming.
Make classes as atomic as possible; that is, give each class a single, clear purpose. If your classes or your system design grows too complicated, break complex classes into simpler ones.
From a design standpoint, look for and separate things that change from things that stay the same. That is, search for the elements in a system that you might want to change without forcing a redesign, then encapsulate those elements in classes.
Watch out for variance. Two semantically different objects may have identical actions, or responsibilities, and there is a natural temptation to try to make one a subclass of the other just to benefit from inheritance. This is called variance, but there's no real justification to force a superclass/subclass relationship where it doesn't exist. A better solution is to create a general base class that produces an interface for both as derived classes - it requires a bit more space, but you still benefit from inheritance and will probably make an important discovery about the natural language solution.
Watch out for limitation during inheritance. The clearest designs add new capabilities to inherited ones. A suspicious design removes old capabilities during inheritance without adding new ones. But rules are made to be broken, and if you are working from an old class library, it may be more efficient to restrict an existing class in its subclass than it would be to restructure the hierarchy so your new class fits in where it should, above the old class.
Don't extend fundamental functionality by subclassing. If an interface element is essential to a class it should be in the base class, not added during derivation. If you're adding member functions by inheriting, perhaps you should rethink the design.
Start with a minimal interface to a class, as small and simple as you need. As the class is used, you'll discover ways you must expand the interface. However, once a class is in use you cannot shrink the interface without disturbing client code. If you need to add more functions, that's fine; it won't disturb code, other than forcing recompiles. But even if new member functions replace the functionality of old ones, leave the existing interface alone (you can combine the functionality in the underlying implementation if you want). If you need to expand the interface of an existing function by adding more arguments, leave the existing arguments in their current order, and put default values on all the new arguments; this way you won't disturb any existing calls to that function.
Read your classes aloud to make sure they're logical, referring to base classes as "is-a" and member objects as "has-a.."
When deciding between inheritance and composition, ask if you need to upcast to the base type. If not, prefer composition (member objects) to inheritance. This can eliminate the perceived need for multiple inheritance. If you inherit, users will think they are supposed to upcast.
Sometimes you need to inherit in order to access protected members of the base class. This can lead to a perceived need for multiple inheritance. If you don't need to upcast, first derive a new class to perform the protected access. Then make that new class a member object inside any class that needs to use it, rather than inheriting.
Typically, a base class will only be an interface to classes derived from it. When you create a base class, default to making the member functions pure virtual. The destructor can also be pure virtual (to force inheritors to explicitly redefine it), but remember to give the destructor a function body, because all destructors in a hierarchy are always called.
When you put a virtual function in a class, make all functions in that class virtual, and put in a virtual destructor. Start removing the virtual keyword when you're tuning for efficiency. This approach prevents surprises in the behavior of the interface.
Use data members for variation in value and virtual functions for variation in behavior. That is, if you find a class with state variables and member functions that switch behavior on those variables, you should probably redesign it to express the differences in behavior within subclasses and virtual functions.
If you must do something nonportable, make an abstraction for that service and localize it within a class. This extra level of indirection prevents the nonportability from being distributed throughout your program.
Avoid multiple inheritance. It's for getting you out of bad situations, especially repairing class interfaces where you don't have control of the broken class (see Chapter XX). You should be an experienced programmer before designing multiple inheritance into your system.
Don't use private inheritance. Although it's in the language and seems to have occasional functionality, it introduces significant ambiguities when combined with run-time type identification. Create a private member object instead of using private inheritance.
If two classes are associated with each other in
some functional way (such as containers and iterators) try to make one a public
nested friend class of the other, as the STL does with iterators
inside containers. This not only emphasizes the association between the
classes, but it allows the class name to be reused by nesting it within another
class. Again, the STL does this by placing iterator inside each
container class, thereby providing them with a common interface.
The other reason you'll want to nest a class is as part of the private implementation.
Here, nesting is beneficial for implementation hiding rather than class
association and the prevention of namespace pollution as above.
Operator overloading is only "syntactic sugar": a different way to make a function call. If overloading an operator doesn't make the class interface clearer and easier to use, don't do it. Create only one automatic type conversion operator for a class. In general, follow the guidelines and format given in Chapter XX when overloading operators.
First make a program work, then optimize it. In particular, don't worry about writing inline functions, making some functions nonvirtual, or tweaking code to be efficient when you are first constructing the system. Your primary goal should be to prove the design, unless the design requires a certain efficiency.
Don't let the compiler create the constructors, destructors, or the operator= for you. Those are training wheels. Class designers should always say exactly what the class should do and keep the class entirely under control. If you don't want a copy-constructor or operator=, declare them private. Remember that if you create any constructor, it prevents the default constructor from being synthesized.
If your class contains pointers, you must create the copy-constructor, operator=, and destructor for the class to work properly.
When you write a copy-constructor for a derived
class, remember to call the base-class copy-constructor explicitly. If you
don't, the default constructor will be called for the base class and that
probably isn't what you want. To call the base-class copy-constructor, pass it
the derived object you're copying from:
Derived(const Derived& d) :
base(d) { // ...
To minimize recompiles during development of a large project, use the handle class/Cheshire cat technique demonstrated in Chapter XX, and remove it only if runtime efficiency is a problem.
Avoid the preprocessor. Always use const for value substitution and inlines for macros.
Keep scopes as small as possible so the visibility and lifetime of your objects are as small as possible. This reduces the chance of using an object in the wrong context and hiding a difficult-to-find bug. For example, suppose you have a container and a piece of code that iterates through it. If you copy that code to use with a new container, you may accidentally end up using the size of the old container as the upper bound of the new one. If, however, the old container is out of scope, the error will be caught at compile time.
Avoid global variables. Always strive to put data inside classes. Global functions are more likely to occur naturally than global variables, although you may later discover that a global function may fit better as a static member of a class.
If you need to declare a class or function from
a library, always do so by including a header file. For example, if you want to
create a function to write to an ostream, never declare ostream
yourself using an incomplete type specification like this,
class ostream;
This approach leaves your code vulnerable to changes in
representation. (For example, ostream could actually be a typedef.)
Instead, always use the header file:
#include <iostream>
When creating your own classes, if a library is big, provide your
users an abbreviated form of the header file with incomplete type
specifications (that is, class name declarations) for cases where they only
need to use pointers. (It can speed compilations.)
When choosing the return type of an overloaded operator, think about chaining expressions together. When defining operator=, remember x=x. Return a copy or reference to the lvalue (return *this) so it can be used in a chained expression (A = B = C).
When writing a function, pass arguments by const reference as your first choice. As long as you don't need to modify the object being passed in, this practice is best because it has the simplicity of pass-by-value syntax but doesn't require expensive constructions and destructions to create a local object, which occurs when passing by value. Normally you don't want to be worrying too much about efficiency issues when designing and building your system, but this habit is a sure win.
Be aware of temporaries. When tuning for
performance, watch out for temporary creation, especially with operator
overloading. If your constructors and destructors are complicated, the cost of
creating and destroying temporaries can be high. When returning a value from a
function, always try to build the object "in place" with a constructor call in
the return statement:
return MyType(i, j);
rather than
MyType x(i, j);
return x;
The former return statement eliminates a copy-constructor call and
destructor call.
When creating constructors, consider exceptions. In the best case, the constructor won't do anything that throws an exception. In the next-best scenario, the class will be composed and inherited from robust classes only, so they will automatically clean themselves up if an exception is thrown. If you must have naked pointers, you are responsible for catching your own exceptions and then deallocating any resources pointed to before you throw an exception in your constructor. If a constructor must fail, the appropriate action is to throw an exception.
Do only what is minimally necessary in your constructors. Not only does this produce a lower overhead for constructor calls (many of which may not be under your control) but your constructors are then less likely to throw exceptions or cause problems.
The responsibility of the destructor is to release resources allocated during the lifetime of the object, not just during construction.
Use exception hierarchies, preferably derived from the Standard C++ exception hierarchy and nested as public classes within the class that throws the exceptions. The person catching the exceptions can then catch the specific types of exceptions, followed by the base type. If you add new derived exceptions, client code will still catch the exception through the base type.
Throw exceptions by value and catch exceptions by reference. Let the exception-handling mechanism handle memory management. If you throw pointers to exceptions created on the heap, the catcher must know to destroy the exception, which is bad coupling. If you catch exceptions by value, you cause extra constructions and destructions; worse, the derived portions of your exception objects may be sliced during upcasting by value.
Don't write your own class templates unless you must. Look first in the Standard Template Library, then to vendors who create special-purpose tools. Become proficient with their use and you'll greatly increase your productivity.
When creating templates, watch for code that does not depend on type and put that code in a nontemplate base class to prevent needless code bloat. Using inheritance or composition, you can create templates in which the bulk of the code they contain is type-dependent and therefore essential.
Don't use the stdio.h functions such as printf( ). Learn to use iostreams instead; they are type-safe and type-extensible, and significantly more powerful. Your investment will be rewarded regularly (see Chapter XX). In general, always use C++ libraries in preference to C libraries.
Avoid C's built-in types. They are supported in C++ for backward compatibility, but they are much less robust than C++ classes, so your bug-hunting time will increase.
Whenever you use built-in types as globals or automatics, don't define them until you can also initialize them. Define variables one per line along with their initialization. When defining pointers, put the '*' next to the type name. You can safely do this if you define one variable per line. This style tends to be less confusing for the reader.
Guarantee that initialization occurs in all aspects of your code. Perform all member initialization in the constructor initializer list, even built-in types (using pseudo-constructor calls). Use any bookkeeping technique you can to guarantee no uninitialized objects are running around in your system. Using the constructor initializer list is often more efficient when initializing subobjects; otherwise the default constructor is called, and you end up calling other member functions - probably operator= - on top of that in order to get the initialization you want.
Don't use the form MyType a = b; to define an object. This one feature is a major source of confusion because it calls a constructor instead of the operator=. For clarity, always be specific and use the form MyType a(b); instead. The results are identical, but other programmers won't be confused.
Use the explicit casts in C++. A cast overrides the normal typing system and is a potential error spot. Since the explicit casts divide C's one-cast-does-all into classes of well-marked casts, anyone debugging and maintaining the code can easily find all the places where logical errors are most likely to happen.
For a program to be robust, each component must be robust. Use all the tools provided by C++: implementation hiding, exceptions, const-correctness, type checking, and so on in each class you create. That way you can safely move to the next level of abstraction when building your system.
Build in const-correctness. This allows the compiler to point out bugs that would otherwise be subtle and difficult to find. This practice takes a little discipline and must be used consistently throughout your classes, but it pays off.
Use compiler error checking to your advantage. Perform all compiles with full warnings, and fix your code to remove all warnings. Write code that utilizes the compiler errors and warnings rather than that which causes runtime errors (for example, don't use variadic argument lists, which disable all type checking). Use assert( ) for debugging, but use exceptions to work with runtime errors.
Prefer compile-time errors to runtime errors. Try to handle an error as close to the point of its occurrence as possible. Prefer dealing with the error at that point to throwing an exception. Catch any exceptions in the nearest handler that has enough information to deal with them. Do what you can with the exception at the current level; if that doesn't solve the problem, rethrow the exception.
If you're using exception specifications, install your own unexpected( ) function using set_unexpected( ). Your unexpected( ) should log the error and rethrow the current exception. That way, if an existing function gets redefined and starts throwing exceptions, you will have a record of the culprit and can modify your calling code to handle the exception.
Create a user-defined terminate( ) (indicating a programmer error) to log the error that caused the exception, then release system resources, and exit the program.
If a destructor calls any functions, those functions may throw exceptions. A destructor cannot throw an exception (this can result in a call to terminate( ), which indicates a programming error), so any destructor that calls functions must catch and manage its own exceptions.
Don't create your own "decorated" private data member names, unless you have a lot of pre-existing global values; otherwise, let classes and namespaces do that for you.
If you're going to use a loop variable after the end of a for loop, define the variable before the for control expression. This way, you won't have any surprises when implementations change to limit the lifetime of variables defined within for control-expressions to the controlled expression.
Watch for overloading. A function should not conditionally execute code based on the value of an argument, default or not. In this case, you should create two or more overloaded functions instead.
Hide your pointers inside container classes. Bring them out only when you are going to immediately perform operations on them. Pointers have always been a major source of bugs. When you use new, try to drop the resulting pointer into a container. Prefer that a container "own" its pointers so it's responsible for cleanup. If you must have a free-standing pointer, always initialize it, preferably to an object address, but to zero if necessary. Set it to zero when you delete it to prevent accidental multiple deletions.
Don't overload global new and delete; always do it on a class-by-class basis. Overloading the global versions affects the entire client programmer project, something only the creators of a project should control. When overloading new and delete for classes, don't assume you know the size of the object; someone may be inheriting from you. Use the provided argument. If you do anything special, consider the effect it could have on inheritors.
Don't repeat yourself. If a piece of code is recurring in many functions in derived classes, put that code into a single function in the base class and call it from the derived class functions. Not only do you save code space, you provide for easy propagation of changes. This is possible even for pure virtual functions (see Chapter XX). You can use an inline function for efficiency. Sometimes the discovery of this common code will add valuable functionality to your interface.
Prevent object slicing. It virtually never makes sense to upcast an object by value. To prevent this, put pure virtual functions in your base class.
Sometimes simple aggregation does the job. A "passenger comfort system" on an airline consists of disconnected elements: seat, air conditioning, video, etc., and yet you need to create many of these in a plane. Do you make private members and build a whole new interface? No - in this case, the components themselves are also part of the public interface, so you should create public member objects. Those objects have their own private implementations, which are still safe.
|