|
|||||
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
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++
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
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++
these
features or constraints don't always
appear during
analysis
and design.
13.
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
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++
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
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++
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
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++
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
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++
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
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++
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
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++
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
Table of Contents:
|
|||||