ZeePedia

B: Programming Guidelines

<< A: Coding Style
C: Recommended Reading:Depth & dark corners, Analysis & design >>
img
B: Programming Guidelines
This appendix is a collection of suggestions for C++
programming. They've been assembled over the course
of my teaching and programming experience and
797
img
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++, 2nd edition, Addison-Wesley, 1998),
and Rob Murray (author of C++ Strategies & Tactics, Addison-Wesley,
1993). Also, many of the tips are summarized from the pages of
Thinking in C++.
1.
First make it work, then make it fast. This is true even if you
are certain that a piece of code is really important and that it
will be a principal bottleneck in your system. Don't do it. Get
the system going first with as simple a design as possible.
Then if it isn't going fast enough, profile it. You'll almost
always discover that "your" bottleneck isn't the problem.
Save your time for the really important stuff.
2.
Elegance always pays off. It's not a frivolous pursuit. Not
only does it give you a program that's easier to build and
debug, but it's also easier to understand and maintain, and
that's where the financial value lies. This point can take some
experience to believe, because it can seem that while you're
making a piece of code elegant, you're not being productive.
The productivity comes when the code seamlessly integrates
into your system, and even more so when the code or system
is modified.
3.
Remember the "divide and conquer" principle. If the
problem you're looking at is too confusing, try to imagine
what the basic operation of the program would be, given the
existence of a magic "piece" that handles the hard parts. That
"piece" is an object ­ write the code that uses the object, then
look at the object and encapsulate its hard parts into other
objects, etc.
4.
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 C in C++ is a
valuable activity because it may reveal hidden bugs.
798
Thinking in C++
img
However, taking C code that works fine and rewriting it in
C++ may not be the best use of your time, unless the C++
version will provide a lot of opportunities for reuse as a class.
5.
If you do have a large body of C code that needs changing,
first isolate the parts of the code that will not be modified,
possibly wrapping those functions in an "API class" as static
member functions. Then focus on the code that will be
changed, refactoring it into classes to facilitate easy
modifications as your maintenance proceeds.
6.
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 write the class so that 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.
7.
When you create a class, make your names as clear as
possible. Your goal should be to make the client
programmer's interface conceptually simple. Attempt to
make your names so clear that comments are unnecessary. To
this end, use function overloading and default arguments to
create an intuitive, easy-to-use interface.
8.
Access control 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.
B: Programming Guidelines
799
img
9.
Don't fall into analysis paralysis. There are some things that
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.
10.
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
design methodology produces more than that, ask yourself if
all the pieces produced by that methodology 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.
11.
Write the test code first (before you write the class), and keep
it with the class. Automate the running of your tests through
a makefile or similar tool. This way, any changes can be
automatically verified by running the test code, and you'll
immediately discover errors. Because you know that you
have the safety net of your test framework, you will be bolder
about making sweeping changes when you discover the
need. Remember that the greatest improvements in
languages come from the built-in testing that type checking,
exception handling, etc., provide, but those features take you
only so far. You must go the rest of the way in creating a
robust system by filling in the tests that verify features that
are specific to your class or program.
12.
Write the test code first (before you write the class) in order
to verify that your class design is complete. If you can't write
test code, you don't know what your class looks like. In
addition, the act of writing the test code will often flush out
additional features or constraints that you need in the class ­
800
Thinking in C++
img
.
these features or constraints don't always appear during
analysis and design.
13.
Remember a fundamental rule of software engineering1: All
software design 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.
14.
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. The most obvious indicator of this is sheer size: if a
class is big, chances are it's doing too much and should be
broken up.
15.
Watch for long member function definitions. A function that
is long and complicated is difficult and expensive to
maintain, and is probably trying to do too much all by itself.
If you see such a function, it indicates that, at the least, it
should be broken up into multiple functions. It may also
suggest the creation of a new class.
16.
Watch for long argument lists. Function calls then become
difficult to write, read and maintain. Instead, try to move the
member function to a class where it is (more) appropriate,
and/or pass objects in as arguments.
17.
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. You can use an inline function
for efficiency. Sometimes the discovery of this common code
will add valuable functionality to your interface.
1 Explained to me by Andrew Koenig.
B: Programming Guidelines
801
img
18.
Watch for switch statements or chained if-else clauses. This
is typically an indicator of type-check coding, which means you
are choosing what code to execute based on some kind of
type information (the exact type may not be obvious at first).
You can usually replace this kind of code with inheritance
and polymorphism; a polymorphic function call will perform
the type checking for you, and allow for more reliable and
easier extensibility.
19.
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. You can learn significantly more about this concept in
the Design Patterns chapter in Volume 2 of this book,
available at .
20.
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 design.
21.
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.
802
Thinking in C++
img
22.
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.
23.
Less is more. Start with a minimal interface to a class, as
small and simple as you need to solve the problem at hand,
but don't try to anticipate all the ways that your class might
be used. 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 of the new arguments;
this way you won't disturb any existing calls to that function.
24.
Read your classes aloud to make sure they're logical,
referring to the relationship between a base class and derived
class as "is-a" and member objects as "has-a."
25.
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.
26.
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
B: Programming Guidelines
803
img
make that new class a member object inside any class that
needs to use it, rather than inheriting.
27.
Typically, a base class will be used primarily to create an
interface to classes derived from it. Thus, 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 override it), but remember to give the
destructor a function body, because all destructors in a
hierarchy are always called.
28.
When you put a virtual function in a class, make all functions
in that class virtual, and put in a virtual destructor. This
approach prevents surprises in the behavior of the interface.
Only start removing the virtual keyword when you're tuning
for efficiency and your profiler has pointed you in this
direction.
29.
Use data members for variation in value and virtual
functions for variation in behavior. That is, if you find a class
that uses state variables along with member functions that
switch behavior based on those variables, you should
probably redesign it to express the differences in behavior
within subclasses and overridden virtual functions.
30.
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 non-portability from being
distributed throughout your program.
31.
Avoid multiple inheritance. It's for getting you out of bad
situations, especially repairing class interfaces in which you
don't have control of the broken class (see Volume 2). You
should be an experienced programmer before designing
multiple inheritance into your system.
32.
Don't use private inheritance. Although it's in the language
and seems to have occasional functionality, it introduces
804
Thinking in C++
img
significant ambiguities when combined with run-time type
identification. Create a private member object instead of
using private inheritance.
33.
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 Standard
C++ Library does with iterators inside containers (examples
of this are shown in the latter part of Chapter 16). This not
only emphasizes the association between the classes, but it
allows the class name to be reused by nesting it within
another class. The Standard C++ Library does this by
defining a nested iteratorclass inside each container class,
thereby providing the containers 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 the class association and
prevention of namespace pollution noted above.
34.
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 12 when overloading operators.
35.
Don't fall prey to premature optimization. That way lies
madness. In particular, don't worry about writing (or
avoiding) 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.
36.
Normally, don't let the compiler create the constructors,
destructors, or the operator=for you. Class designers should
always say exactly what the class should do and keep the
B: Programming Guidelines
805
img
class entirely under control. If you don't want a copy-
constructor or operator= declare them as private. Remember
,
that if you create any constructor, it prevents the default
constructor from being synthesized.
37.
If your class contains pointers, you must create the copy-
constructor, operator= and destructor for the class to work
,
properly.
38.
When you write a copy-constructor for a derived class,
remember to call the base-class copy-constructor explicitly
(also the member-object versions). (See Chapter 14.) If you
don't, the default constructor will be called for the base class
(or member object) 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) { // ...
39.
When you write an assignment operator for a derived class,
remember to call the base-class version of the assignment
operator explicitly. (See Chapter 14.) If you don't, then
nothing will happen (the same is true for the member
objects). To call the base-class assignment operator, use the
base-class name and scope resolution:
Derived& operator=(const Derived& d) {
Base::operator=(d);
40.
If you need to minimize recompiles during development of a
large project, use the handle class/Cheshire cat technique
demonstrated in Chapter 5, and remove it only if runtime
efficiency is a problem.
41.
Avoid the preprocessor. Always use const for value
substitution and inlines for macros.
42.
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
806
Thinking in C++
img
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.
43.
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.
44.
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 in which they need to use only
pointers. (It can speed compilations.)
45.
When choosing the return type of an overloaded operator,
consider what will happen if expressions are chained
together. Return a copy or reference to the lvalue (return
*this) so it can be used in a chained expression (A = B = C
).
When defining operator= remember x=x.
,
46.
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, this practice is best because it has the
B: Programming Guidelines
807
img
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.
47.
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 (the so-called return-value
optimization) eliminates a copy-constructor call and destructor
call.
48.
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.
49.
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
808
Thinking in C++
img
your constructors are then less likely to throw exceptions or
cause problems.
50.
The responsibility of the destructor is to release resources
allocated during the lifetime of the object, not just during
construction.
51.
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, existing client code will still catch
the exception through the base type.
52.
Throw exceptions by value and catch exceptions by
reference. Let the exception-handling mechanism handle
memory management. If you throw pointers to exception
objects that have been 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.
53.
Don't write your own class templates unless you must. Look
first in the Standard C++ Library, then to vendors who create
special-purpose tools. Become proficient with their use and
you'll greatly increase your productivity.
54.
When creating templates, watch for code that does not
depend on type and put that code in a non-template 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.
55.
Don't use the <cstdio>functions, such as printf( ) Learn to
.
use iostreams instead; they are type-safe and type-extensible,
B: Programming Guidelines
809
img
and significantly more powerful. Your investment will be
rewarded regularly. In general, always use C++ libraries in
preference to C libraries.
56.
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.
57.
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.
58.
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). 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.
59.
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.
60.
Use the explicit casts described in Chapter 3. 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.
810
Thinking in C++
img
61.
For a program to be robust, each component must be robust.
Use all the tools provided by C++: access control, 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.
62.
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.
63.
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 compile-time 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 for runtime errors.
64.
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. (See Volume 2 for more
details.)
65.
If you're using exception specifications (see Volume 2 of this
book, downloadable from , to learn about
exception handling), 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 overridden and starts throwing
exceptions, you will have a record of the culprit and can
modify your calling code to handle the exception.
B: Programming Guidelines
811
img
66.
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.
67.
If a destructor calls any functions, those functions might
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.
68.
Don't create your own "decorated" private data member
names (prepending underscores, Hungarian notation, etc.),
unless you have a lot of pre-existing global values; otherwise,
let classes and namespaces do the name scoping for you.
69.
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.
70.
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. Even better, wrap a pointer inside a
class; if you still want it to look like a pointer, overload
operator->and operator* 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.
71.
Don't overload global new and delete; always do this 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 that you know the size
of the object; someone may be inheriting from you. Use the
812
Thinking in C++
img
provided argument. If you do anything special, consider the
effect it could have on inheritors.
72.
Prevent object slicing. It virtually never makes sense to
upcast an object by value. To prevent upcasting by value, put
pure virtual functions in your base class.
73.
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 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. Be aware
that simple aggregation is not a solution to be used often, but
it does happen.
B: Programming Guidelines
813